@lexho111/plainblog 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Blog.js +2 -8
- package/blog_test_empty.db +0 -0
- package/blog_test_load.db +0 -0
- package/coverage/clover.xml +485 -485
- package/coverage/coverage-final.json +10 -10
- package/coverage/lcov-report/index.html +1 -1
- package/coverage/lcov-report/package/Article.js.html +5 -5
- package/coverage/lcov-report/package/Blog.js.html +171 -171
- package/coverage/lcov-report/package/Formatter.js.html +1 -1
- package/coverage/lcov-report/package/build-scripts.js.html +1 -1
- package/coverage/lcov-report/package/build-styles.js.html +1 -1
- package/coverage/lcov-report/package/index.html +1 -1
- package/coverage/lcov-report/package/model/APIModel.js.html +1 -1
- package/coverage/lcov-report/package/model/DataModel.js.html +7 -4
- package/coverage/lcov-report/package/model/DatabaseModel.js.html +6 -3
- package/coverage/lcov-report/package/model/FileAdapter.js.html +10 -7
- package/coverage/lcov-report/package/model/FileModel.js.html +9 -6
- package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +1 -1
- package/coverage/lcov-report/package/model/SqliteAdapter.js.html +1 -1
- package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +2 -2
- package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +1 -1
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +42 -39
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +8 -5
- package/coverage/lcov-report/package/model/datastructures/FileList.js.html +1 -1
- package/coverage/lcov-report/package/model/datastructures/index.html +1 -1
- package/coverage/lcov-report/package/model/index.html +1 -1
- package/coverage/lcov-report/package/utilities.js.html +19 -19
- package/coverage/lcov.info +916 -916
- package/debug-loader.js +26 -0
- package/model/DataModel.js +1 -2
- package/model/DatabaseModel.js +1 -2
- package/model/FileAdapter.js +1 -2
- package/model/FileModel.js +2 -3
- package/model/datastructures/BinarySearchTree.js +1 -2
- package/model/datastructures/BinarySearchTreeHashMap.js +1 -2
- package/package.json +3 -3
- package/public/styles.min.css +2 -2
|
@@ -951,6 +951,7 @@
|
|
|
951
951
|
<span class="cline-any cline-neutral"> </span>
|
|
952
952
|
<span class="cline-any cline-neutral"> </span>
|
|
953
953
|
<span class="cline-any cline-neutral"> </span>
|
|
954
|
+
<span class="cline-any cline-neutral"> </span>
|
|
954
955
|
<span class="cline-any cline-yes">5x</span>
|
|
955
956
|
<span class="cline-any cline-neutral"> </span>
|
|
956
957
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -1089,7 +1090,7 @@
|
|
|
1089
1090
|
<span class="cline-any cline-neutral"> </span>
|
|
1090
1091
|
<span class="cline-any cline-neutral"> </span>
|
|
1091
1092
|
<span class="cline-any cline-neutral"> </span>
|
|
1092
|
-
<span class="cline-any cline-yes">
|
|
1093
|
+
<span class="cline-any cline-yes">164663x</span>
|
|
1093
1094
|
<span class="cline-any cline-neutral"> </span>
|
|
1094
1095
|
<span class="cline-any cline-neutral"> </span>
|
|
1095
1096
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -1178,12 +1179,12 @@
|
|
|
1178
1179
|
<span class="cline-any cline-neutral"> </span>
|
|
1179
1180
|
<span class="cline-any cline-neutral"> </span>
|
|
1180
1181
|
<span class="cline-any cline-neutral"> </span>
|
|
1181
|
-
<span class="cline-any cline-yes">
|
|
1182
|
+
<span class="cline-any cline-yes">18x</span>
|
|
1182
1183
|
<span class="cline-any cline-neutral"> </span>
|
|
1183
1184
|
<span class="cline-any cline-neutral"> </span>
|
|
1184
|
-
<span class="cline-any cline-yes">
|
|
1185
|
+
<span class="cline-any cline-yes">18x</span>
|
|
1185
1186
|
<span class="cline-any cline-neutral"> </span>
|
|
1186
|
-
<span class="cline-any cline-yes">
|
|
1187
|
+
<span class="cline-any cline-yes">4x</span>
|
|
1187
1188
|
<span class="cline-any cline-neutral"> </span>
|
|
1188
1189
|
<span class="cline-any cline-neutral"> </span>
|
|
1189
1190
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -1483,18 +1484,18 @@
|
|
|
1483
1484
|
<span class="cline-any cline-yes">25x</span>
|
|
1484
1485
|
<span class="cline-any cline-yes">24x</span>
|
|
1485
1486
|
<span class="cline-any cline-yes">24x</span>
|
|
1486
|
-
<span class="cline-any cline-yes">
|
|
1487
|
-
<span class="cline-any cline-yes">
|
|
1488
|
-
<span class="cline-any cline-yes">
|
|
1489
|
-
<span class="cline-any cline-yes">
|
|
1487
|
+
<span class="cline-any cline-yes">311088x</span>
|
|
1488
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1489
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1490
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1490
1491
|
<span class="cline-any cline-neutral"> </span>
|
|
1491
1492
|
<span class="cline-any cline-neutral"> </span>
|
|
1492
1493
|
<span class="cline-any cline-neutral"> </span>
|
|
1493
1494
|
<span class="cline-any cline-neutral"> </span>
|
|
1494
1495
|
<span class="cline-any cline-neutral"> </span>
|
|
1495
|
-
<span class="cline-any cline-yes">
|
|
1496
|
-
<span class="cline-any cline-yes">
|
|
1497
|
-
<span class="cline-any cline-yes">
|
|
1496
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1497
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1498
|
+
<span class="cline-any cline-yes">155532x</span>
|
|
1498
1499
|
<span class="cline-any cline-neutral"> </span>
|
|
1499
1500
|
<span class="cline-any cline-yes">24x</span>
|
|
1500
1501
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -1788,7 +1789,6 @@
|
|
|
1788
1789
|
<span class="cline-any cline-neutral"> </span>
|
|
1789
1790
|
<span class="cline-any cline-neutral"> </span>
|
|
1790
1791
|
<span class="cline-any cline-neutral"> </span>
|
|
1791
|
-
<span class="cline-any cline-neutral"> </span>
|
|
1792
1792
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import http from "http";
|
|
1793
1793
|
import crypto from "node:crypto";
|
|
1794
1794
|
import fs from "fs";
|
|
@@ -1797,7 +1797,7 @@ import process from "node:process";
|
|
|
1797
1797
|
import path from "path";
|
|
1798
1798
|
import { fileURLToPath } from "url";
|
|
1799
1799
|
import pkg from "./package.json" with { type: "json" };
|
|
1800
|
-
import createDebug from "
|
|
1800
|
+
import { debuglog as createDebug } from "node:util";
|
|
1801
1801
|
import Article from "./Article.js";
|
|
1802
1802
|
import DatabaseModel from "./model/DatabaseModel.js";
|
|
1803
1803
|
import { fetchData, postData } from "./model/APIModel.js";
|
|
@@ -1893,9 +1893,9 @@ export default class Blog {
|
|
|
1893
1893
|
this.#title = title;
|
|
1894
1894
|
}
|
|
1895
1895
|
|
|
1896
|
-
|
|
1897
|
-
<span class="
|
|
1898
|
-
}
|
|
1896
|
+
setPassword(password) {
|
|
1897
|
+
<span class="fstat-no" title="function not covered" ></span> this.#password = password;
|
|
1898
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
1899
1899
|
|
|
1900
1900
|
/**
|
|
1901
1901
|
* Appends CSS rules to the \<style\>-tag.
|
|
@@ -1923,9 +1923,9 @@ export default class Blog {
|
|
|
1923
1923
|
* @param {*} adapter a database adapter like PostgresAdapter or SqliteAdapter
|
|
1924
1924
|
*/
|
|
1925
1925
|
setDatabaseAdapter(adapter) {
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1926
|
+
if (!this.#databaseModel) {
|
|
1927
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> if (this.database.type === "file") {
|
|
1928
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> const adapter = new FileAdapter(this.database);
|
|
1929
1929
|
this.#databaseModel = new DatabaseModel(adapter);
|
|
1930
1930
|
}
|
|
1931
1931
|
}
|
|
@@ -1979,10 +1979,10 @@ export default class Blog {
|
|
|
1979
1979
|
Location: "/",
|
|
1980
1980
|
});
|
|
1981
1981
|
res.end();
|
|
1982
|
-
} else
|
|
1983
|
-
<span class="
|
|
1982
|
+
} else {
|
|
1983
|
+
res.w<span class="missing-if-branch" title="else path not taken" >E</span>riteHead(401, { "Content-Type": "text/html" });
|
|
1984
1984
|
<span class="cstat-no" title="statement not covered" > res.end(`${header("My Blog")}</span>
|
|
1985
|
-
<body>
|
|
1985
|
+
<body><span class="cstat-no" title="statement not covered" ></span>
|
|
1986
1986
|
<h1>Unauthorized</h1><div class="box"><p>Please enter the password.<form method="POST">
|
|
1987
1987
|
<input type="password" name="password" placeholder="Password" />
|
|
1988
1988
|
<button style="margin: 2px;">Login</button></form></div>
|
|
@@ -1991,22 +1991,22 @@ export default class Blog {
|
|
|
1991
1991
|
}
|
|
1992
1992
|
|
|
1993
1993
|
#handleLogout(req, res) {
|
|
1994
|
-
|
|
1995
|
-
<span class="cstat-no" title="statement not covered" > if (req.headers.cookie)
|
|
1996
|
-
|
|
1997
|
-
req.headers.
|
|
1994
|
+
debug("handle logout");
|
|
1995
|
+
<span class="cstat-no" title="statement not covered" > if (req.headers.cookie) </span>{
|
|
1996
|
+
<span class="cstat-no" title="statement not covered" > const params = new URLSearchParams(</span>
|
|
1997
|
+
req.headers.c<span class="cstat-no" title="statement not covered" >ookie.replace(/; /g, "&"),</span>
|
|
1998
1998
|
);
|
|
1999
|
-
const sessionId =
|
|
2000
|
-
|
|
1999
|
+
const sessionId = params.get("session");
|
|
2000
|
+
if (this.sessions.<span class="cstat-no" title="statement not covered" >has(sessionId)) {</span>
|
|
2001
2001
|
<span class="cstat-no" title="statement not covered" > this.sessions.delete(sessionId);</span>
|
|
2002
|
-
}
|
|
2002
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2003
2003
|
}
|
|
2004
|
-
|
|
2005
|
-
"Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0"
|
|
2004
|
+
res.writeHead(303, {
|
|
2005
|
+
<span class="cstat-no" title="statement not covered" > "Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",</span>
|
|
2006
2006
|
Location: "/",
|
|
2007
2007
|
});
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2008
|
+
res.end();
|
|
2009
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2010
2010
|
|
|
2011
2011
|
/** initializes database */
|
|
2012
2012
|
async init() {
|
|
@@ -2044,17 +2044,17 @@ export default class Blog {
|
|
|
2044
2044
|
const match = publicCSS.match(
|
|
2045
2045
|
/\/\* source-hash: ([a-f0-9]{64}) \*\//,
|
|
2046
2046
|
);
|
|
2047
|
-
|
|
2048
|
-
})
|
|
2047
|
+
if (match) publicHash = match[1];
|
|
2048
|
+
})<span class="missing-if-branch" title="else path not taken" >E</span>
|
|
2049
2049
|
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
2050
2050
|
fs.promises
|
|
2051
2051
|
.readFile(srcStylePath, "utf8")
|
|
2052
2052
|
.then((content) => {
|
|
2053
2053
|
srcStyles = content;
|
|
2054
2054
|
})
|
|
2055
|
-
.catch(
|
|
2056
|
-
<span class="
|
|
2057
|
-
})
|
|
2055
|
+
.catch((err) => {
|
|
2056
|
+
if (e<span class="fstat-no" title="function not covered" >rr</span>.code !== "ENOENT") console.error(err);
|
|
2057
|
+
})<span class="cstat-no" title="statement not covered" >, // ignore if src/styles.c<span class="cstat-no" title="statement not covered" >ss doesn't exist</span></span>
|
|
2058
2058
|
]);
|
|
2059
2059
|
|
|
2060
2060
|
const combinedStyles = this.#styles + srcStyles;
|
|
@@ -2075,8 +2075,8 @@ export default class Blog {
|
|
|
2075
2075
|
finalStyles + `\n/* source-hash: ${srcHash} */`,
|
|
2076
2076
|
);
|
|
2077
2077
|
} catch (err) {
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2078
|
+
console.error("Failed to write styles to public folder:", err);
|
|
2079
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2080
2080
|
}
|
|
2081
2081
|
}
|
|
2082
2082
|
|
|
@@ -2092,8 +2092,8 @@ export default class Blog {
|
|
|
2092
2092
|
} else {
|
|
2093
2093
|
console.log(`database: ${this.database.type}`);
|
|
2094
2094
|
if (!this.#databaseModel) {
|
|
2095
|
-
|
|
2096
|
-
|
|
2095
|
+
if (this.database.type === "file") {
|
|
2096
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> const adapter = new FileAdapter(this.database);
|
|
2097
2097
|
this.#databaseModel = new DatabaseModel(adapter);
|
|
2098
2098
|
}
|
|
2099
2099
|
}
|
|
@@ -2131,9 +2131,9 @@ export default class Blog {
|
|
|
2131
2131
|
),
|
|
2132
2132
|
);
|
|
2133
2133
|
}
|
|
2134
|
-
|
|
2135
|
-
<span class="
|
|
2136
|
-
|
|
2134
|
+
if (this.reloadStylesOnGET)
|
|
2135
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> console.log("reload scripts and styles on GET-Request");
|
|
2136
|
+
le<span class="cstat-no" title="statement not covered" >t title = "";</span>
|
|
2137
2137
|
if (this.#title != null && this.#title.length > 0)
|
|
2138
2138
|
title = this.#title; // use blog title if set
|
|
2139
2139
|
else title = dbTitle; // use title from the database
|
|
@@ -2158,8 +2158,8 @@ export default class Blog {
|
|
|
2158
2158
|
const title = params.get("title");
|
|
2159
2159
|
const content = params.get("content");
|
|
2160
2160
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2161
|
+
if (title && content) {
|
|
2162
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> const newArticleData = { title, content };
|
|
2163
2163
|
try {
|
|
2164
2164
|
// Save the new article to the database via the ApiServer
|
|
2165
2165
|
const promises = [];
|
|
@@ -2174,8 +2174,8 @@ export default class Blog {
|
|
|
2174
2174
|
this.#articles.remove(1); // "Sample Entry #1"
|
|
2175
2175
|
this.#articles.remove(2); // "Sample Entry #2"
|
|
2176
2176
|
} catch (error) {
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2177
|
+
console.error("Failed to post new article to API:", error);
|
|
2178
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2179
2179
|
}
|
|
2180
2180
|
|
|
2181
2181
|
res.writeHead(303, { Location: "/" });
|
|
@@ -2184,8 +2184,8 @@ export default class Blog {
|
|
|
2184
2184
|
}
|
|
2185
2185
|
|
|
2186
2186
|
/** start a http server with default port 8080 */
|
|
2187
|
-
async startServer(port =
|
|
2188
|
-
await this.init()
|
|
2187
|
+
async startServer(port = 8080) {
|
|
2188
|
+
await this.init();<span class="branch-0 cbranch-no" title="branch not covered" ></span>
|
|
2189
2189
|
|
|
2190
2190
|
const server = http.createServer(async (req, res) => {
|
|
2191
2191
|
//debug("query %s", req.url);
|
|
@@ -2197,82 +2197,82 @@ export default class Blog {
|
|
|
2197
2197
|
|
|
2198
2198
|
// Web Page Routes
|
|
2199
2199
|
if (req.url === "/login") {
|
|
2200
|
-
|
|
2201
|
-
<span class="
|
|
2200
|
+
if (req.method === "GET") {
|
|
2201
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> res.writeHead(200, { "Content-Type": "text/html" });
|
|
2202
2202
|
<span class="cstat-no" title="statement not covered" > res.end(`${header("My Blog")}</span>
|
|
2203
|
-
<body>
|
|
2203
|
+
<body><span class="cstat-no" title="statement not covered" ></span>
|
|
2204
2204
|
<h1>Login</h1><div class="box"><form method="POST">
|
|
2205
2205
|
<input type="password" name="password" placeholder="Password" />
|
|
2206
2206
|
<button style="margin: 2px;">Login</button></form></div>
|
|
2207
2207
|
</body></html>`);
|
|
2208
|
-
|
|
2209
|
-
}
|
|
2210
|
-
await this.#handleLogin(req, res);
|
|
2208
|
+
return;
|
|
2209
|
+
} <span class="cstat-no" title="statement not covered" >else if </span>(req.method === "POST") {
|
|
2210
|
+
await<span class="missing-if-branch" title="else path not taken" >E</span> this.#handleLogin(req, res);
|
|
2211
2211
|
return;
|
|
2212
2212
|
}
|
|
2213
2213
|
}
|
|
2214
2214
|
|
|
2215
|
-
|
|
2216
|
-
<span class="
|
|
2215
|
+
if (req.url === "/logout") {
|
|
2216
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> this.#handleLogout(req, res);
|
|
2217
2217
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2218
|
-
}
|
|
2218
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2219
2219
|
// POST new article
|
|
2220
|
-
|
|
2221
|
-
<span class="
|
|
2220
|
+
if (req.method === "POST" && req.url === "/") {
|
|
2221
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> if (!this.#isAuthenticated(<span class="branch-1 cbranch-no" title="branch not covered" >req)) {</span>
|
|
2222
2222
|
<span class="cstat-no" title="statement not covered" > res.writeHead(403, { "Content-Type": "text/plain" });</span>
|
|
2223
2223
|
<span class="cstat-no" title="statement not covered" > res.end("Forbidden");</span>
|
|
2224
2224
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
// GET artciles
|
|
2225
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2226
|
+
await this.postArticle(req, res);
|
|
2227
|
+
<span class="cstat-no" title="statement not covered" > // GET artciles</span>
|
|
2228
2228
|
} else if (req.method === "GET" && req.url === "/") {
|
|
2229
2229
|
// load articles
|
|
2230
2230
|
|
|
2231
2231
|
// reload styles and scripts on (every) request
|
|
2232
|
-
|
|
2233
|
-
<span class="
|
|
2232
|
+
if (this.reloadStylesOnGET) {
|
|
2233
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> if (this.#stylesheetPath != null && this.compilestyle) {
|
|
2234
2234
|
<span class="cstat-no" title="statement not covered" > await this.#processStylesheets(this.#stylesheetPath);</span>
|
|
2235
|
-
}
|
|
2236
|
-
const __filename =
|
|
2237
|
-
const __dirname = <span class="cstat-no" title="statement not covered" >
|
|
2238
|
-
const
|
|
2239
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2240
|
-
}
|
|
2235
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2236
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2237
|
+
const __dirname = p<span class="cstat-no" title="statement not covered" >ath.dirname(__filename);</span>
|
|
2238
|
+
const srcScriptPat<span class="cstat-no" title="statement not covered" >h = path.join(__dirname, </span>"src", "fetchData.js");
|
|
2239
|
+
await this.#processScr<span class="cstat-no" title="statement not covered" >ipts(srcScriptPath);</span>
|
|
2240
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2241
2241
|
|
|
2242
2242
|
let loggedin = false;
|
|
2243
2243
|
if (!this.#isAuthenticated(req)) {
|
|
2244
2244
|
// login
|
|
2245
2245
|
loggedin = false;
|
|
2246
|
-
} else
|
|
2247
|
-
//
|
|
2248
|
-
|
|
2249
|
-
}
|
|
2246
|
+
} else {
|
|
2247
|
+
// lo<span class="missing-if-branch" title="else path not taken" >E</span>gout
|
|
2248
|
+
loggedin = true;
|
|
2249
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2250
2250
|
|
|
2251
2251
|
try {
|
|
2252
2252
|
const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
2253
2253
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
2254
2254
|
res.end(html);
|
|
2255
2255
|
} catch (err) {
|
|
2256
|
-
|
|
2257
|
-
<span class="cstat-no" title="statement not covered" > res.writeHead(500, { "Content-Type": "text/plain" })
|
|
2256
|
+
console.error(err);
|
|
2257
|
+
<span class="cstat-no" title="statement not covered" > res.writeHead(500, {</span> "Content-Type": "text/plain" });
|
|
2258
2258
|
<span class="cstat-no" title="statement not covered" > res.end("Internal Server Error");</span>
|
|
2259
|
-
}
|
|
2260
|
-
} else
|
|
2261
|
-
//
|
|
2262
|
-
|
|
2263
|
-
|
|
2259
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2260
|
+
} else {
|
|
2261
|
+
// Tr<span class="missing-if-branch" title="else path not taken" >E</span>y to serve static files from public folder
|
|
2262
|
+
try {
|
|
2263
|
+
<span class="cstat-no" title="statement not covered" > const publicDir = path.join(process.cwd(), "public");</span>
|
|
2264
2264
|
const parsedUrl = <span class="cstat-no" title="statement not covered" >new URL(</span>
|
|
2265
|
-
req.url
|
|
2265
|
+
req.url,<span class="cstat-no" title="statement not covered" ></span>
|
|
2266
2266
|
`http://${req.headers.host || "localhost"}`,
|
|
2267
2267
|
);
|
|
2268
|
-
const filePath =
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
".html": "text/
|
|
2268
|
+
const filePath = path.join(publicDir, parsedUrl.pathname);
|
|
2269
|
+
<span class="cstat-no" title="statement not covered" ></span>
|
|
2270
|
+
if (filePath.startsWith(publicDir)) {
|
|
2271
|
+
<span class="cstat-no" title="statement not covered" > const stats = await fs.promises.stat(filePath);</span>
|
|
2272
|
+
if (stats.isFi<span class="cstat-no" title="statement not covered" >le()) {</span>
|
|
2273
|
+
<span class="cstat-no" title="statement not covered" > const ext = path.extname(filePath).toLowerCase();</span>
|
|
2274
|
+
const mimeTy<span class="cstat-no" title="statement not covered" >pes = {</span>
|
|
2275
|
+
".html": "text/h<span class="cstat-no" title="statement not covered" >tml",</span>
|
|
2276
2276
|
".js": "text/javascript",
|
|
2277
2277
|
".css": "text/css",
|
|
2278
2278
|
".json": "application/json",
|
|
@@ -2282,31 +2282,31 @@ export default class Blog {
|
|
|
2282
2282
|
".svg": "image/svg+xml",
|
|
2283
2283
|
".ico": "image/x-icon",
|
|
2284
2284
|
};
|
|
2285
|
-
const contentType =
|
|
2286
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2285
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
2286
|
+
res.writeHead(200, {<span class="cstat-no" title="statement not covered" > "Content-Type": contentType });</span>
|
|
2287
2287
|
<span class="cstat-no" title="statement not covered" > fs.createReadStream(filePath).pipe(res);</span>
|
|
2288
2288
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2289
|
-
}
|
|
2289
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2290
2290
|
}
|
|
2291
2291
|
} catch (err) {
|
|
2292
2292
|
// ENOENT (file not found) is expected, leading to a 404.
|
|
2293
2293
|
// Only log other, unexpected errors.
|
|
2294
|
-
|
|
2294
|
+
if (err.code !== "ENOENT") {
|
|
2295
2295
|
<span class="cstat-no" title="statement not covered" > console.error(err);</span>
|
|
2296
|
-
}
|
|
2296
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2297
2297
|
}
|
|
2298
2298
|
|
|
2299
2299
|
// Error 404
|
|
2300
|
-
|
|
2300
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2301
2301
|
<span class="cstat-no" title="statement not covered" > res.end(JSON.stringify({ message: "Not Found" }));</span>
|
|
2302
|
-
}
|
|
2302
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2303
2303
|
});
|
|
2304
2304
|
|
|
2305
2305
|
this.#server = server;
|
|
2306
2306
|
|
|
2307
2307
|
return new Promise((resolve, reject) => {
|
|
2308
|
-
const errorHandler =
|
|
2309
|
-
this.#server.once("
|
|
2308
|
+
const errorHandler = (err) => reject(err);
|
|
2309
|
+
this.#server.once("er<span class="fstat-no" title="function not covered" >ro</span>r", err<span class="cstat-no" title="statement not covered" >orHandler);</span>
|
|
2310
2310
|
this.#server.listen(port, "0.0.0.0", () => {
|
|
2311
2311
|
// <-- for docker 0.0.0.0, localhost 127.0.0.1
|
|
2312
2312
|
this.#server.removeListener("error", errorHandler);
|
|
@@ -2321,8 +2321,8 @@ export default class Blog {
|
|
|
2321
2321
|
if (this.#server) {
|
|
2322
2322
|
// if server is running
|
|
2323
2323
|
this.#server.close((err) => {
|
|
2324
|
-
|
|
2325
|
-
console.log("Server closed.")
|
|
2324
|
+
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") return reject(err);
|
|
2325
|
+
<span class="missing-if-branch" title="if path not taken" >I</span>console.log<span class="branch-1 cbranch-no" title="branch not covered" >("Server closed.");<span class="cstat-no" title="statement not covered" ></span></span>
|
|
2326
2326
|
resolve();
|
|
2327
2327
|
});
|
|
2328
2328
|
} else {
|
|
@@ -2337,13 +2337,13 @@ export default class Blog {
|
|
|
2337
2337
|
debug("applyBlogData");
|
|
2338
2338
|
if (this.#articles.storage.clear) {
|
|
2339
2339
|
this.#articles.storage.clear();
|
|
2340
|
-
} else
|
|
2341
|
-
//
|
|
2340
|
+
} else {
|
|
2341
|
+
//thi<span class="missing-if-branch" title="else path not taken" >E</span>s.#articles.setDataModel(this.#makeDataModel()); // Fallback if clear() isn't implemented
|
|
2342
2342
|
}
|
|
2343
2343
|
this.#title = data.title;
|
|
2344
2344
|
// Assuming data contains a title and an array of articles with title and content
|
|
2345
|
-
|
|
2346
|
-
|
|
2345
|
+
if (this.#articles && data.articles && Array.isArray(data.articles)) {
|
|
2346
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> if (this.#articles.getStorageName().includes("BinarySearchTree")) {
|
|
2347
2347
|
debug("using special insert method for BST");
|
|
2348
2348
|
const insertBalanced = (start, end) => {
|
|
2349
2349
|
if (start > end) return;
|
|
@@ -2376,16 +2376,16 @@ export default class Blog {
|
|
|
2376
2376
|
|
|
2377
2377
|
async #loadFromAPI() {
|
|
2378
2378
|
const data = await fetchData(this.#apiUrl);
|
|
2379
|
-
|
|
2380
|
-
|
|
2379
|
+
if (data) {
|
|
2380
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> this.#applyBlogData(data);
|
|
2381
2381
|
}
|
|
2382
2382
|
}
|
|
2383
2383
|
|
|
2384
2384
|
// controller
|
|
2385
2385
|
/** everything that happens in /api */
|
|
2386
2386
|
async #jsonAPI(req, res) {
|
|
2387
|
-
const url = new URL(req.url, `http://${req.headers.host ||
|
|
2388
|
-
const pathname = url.pathname
|
|
2387
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
2388
|
+
const pathname = url.pathname;<span class="branch-1 cbranch-no" title="branch not covered" ></span>
|
|
2389
2389
|
|
|
2390
2390
|
if (req.method === "GET") {
|
|
2391
2391
|
if (pathname === "/api" || pathname === "/api/") {
|
|
@@ -2396,19 +2396,19 @@ export default class Blog {
|
|
|
2396
2396
|
res.end(JSON.stringify(data));
|
|
2397
2397
|
}
|
|
2398
2398
|
// Search
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2402
|
-
|
|
2403
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2399
|
+
if (url.searchParams.has("q")) {
|
|
2400
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> const query = url.searchParams.get("q");
|
|
2401
|
+
res.writeHead(<span class="cstat-no" title="statement not covered" >200, { "Content-Type": "ap</span>plication/json" });
|
|
2402
|
+
<span class="cstat-no" title="statement not covered" > const results = this.#articles.search(query);</span>
|
|
2403
|
+
res.end(JSON.str<span class="cstat-no" title="statement not covered" >ingify({ articles: results })</span>);
|
|
2404
2404
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2405
|
-
}
|
|
2405
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2406
2406
|
// GET all blog data
|
|
2407
2407
|
if (pathname === "/api/articles") {
|
|
2408
2408
|
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
2409
2409
|
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
2410
|
-
const limit = !isNaN(pLimit) ?
|
|
2411
|
-
|
|
2410
|
+
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
2411
|
+
<span class="branch-0 cbranch-no" title="branch not covered" ></span>
|
|
2412
2412
|
const start = url.searchParams.get("startdate");
|
|
2413
2413
|
const end = url.searchParams.get("enddate");
|
|
2414
2414
|
//const parsedStart = parseDateParam(qStartdate);
|
|
@@ -2446,41 +2446,41 @@ export default class Blog {
|
|
|
2446
2446
|
// extern
|
|
2447
2447
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
2448
2448
|
res.end(JSON.stringify(newArticle));
|
|
2449
|
-
} else
|
|
2450
|
-
|
|
2449
|
+
} else if (req.method === "DELETE" && pathname === "/api/articles") {
|
|
2450
|
+
if (!<span class="cstat-no" title="statement not covered" ><span class="missing-if-branch" title="else path not taken" >E</span>this.#isAuthenticated(req)) {</span>
|
|
2451
2451
|
<span class="cstat-no" title="statement not covered" > res.writeHead(403, { "Content-Type": "application/json" });</span>
|
|
2452
2452
|
<span class="cstat-no" title="statement not covered" > res.end(JSON.stringify({ error: "Forbidden" }));</span>
|
|
2453
2453
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2454
|
-
}
|
|
2455
|
-
const id =
|
|
2456
|
-
<span class="cstat-no" title="statement not covered"
|
|
2454
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2455
|
+
const id = url.searchParams.get("id");
|
|
2456
|
+
if (id) {<span class="cstat-no" title="statement not covered" ></span>
|
|
2457
2457
|
<span class="cstat-no" title="statement not covered" > this.#articles.remove(parseInt(id));</span>
|
|
2458
2458
|
<span class="cstat-no" title="statement not covered" > if (this.#databaseModel) {</span>
|
|
2459
2459
|
<span class="cstat-no" title="statement not covered" > await this.#databaseModel.remove(parseInt(id));</span>
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2460
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2461
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2462
2462
|
<span class="cstat-no" title="statement not covered" > res.end(JSON.stringify({ status: "deleted", id }));</span>
|
|
2463
|
-
}
|
|
2464
|
-
} else
|
|
2465
|
-
|
|
2463
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2464
|
+
} else if (req.method === "PUT" && pathname === "/api/articles") {
|
|
2465
|
+
if (!<span class="cstat-no" title="statement not covered" >this.#isAuthenticated(req)) {</span>
|
|
2466
2466
|
<span class="cstat-no" title="statement not covered" > res.writeHead(403, { "Content-Type": "application/json" });</span>
|
|
2467
2467
|
<span class="cstat-no" title="statement not covered" > res.end(JSON.stringify({ error: "Forbidden" }));</span>
|
|
2468
2468
|
<span class="cstat-no" title="statement not covered" > return;</span>
|
|
2469
|
-
}
|
|
2470
|
-
const id =
|
|
2471
|
-
const body
|
|
2472
|
-
let data = <span class="cstat-no" title="statement not covered" >""
|
|
2473
|
-
|
|
2474
|
-
<span class="cstat-no" title="statement not covered" > req.on("end", <span class="fstat-no" title="function not covered" >
|
|
2475
|
-
})
|
|
2476
|
-
const { title, content } =
|
|
2477
|
-
<span class="cstat-no" title="statement not covered" >
|
|
2469
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2470
|
+
const id = url.searchParams.get("id");
|
|
2471
|
+
const body <span class="cstat-no" title="statement not covered" >= await new Promise((resolv</span>e) => {
|
|
2472
|
+
let data = <span class="cstat-no" title="statement not covered" >"";<span class="fstat-no" title="function not covered" ></span></span>
|
|
2473
|
+
req.on("dat<span class="cstat-no" title="statement not covered" >a",</span> (chunk) => (data += chunk));
|
|
2474
|
+
<span class="cstat-no" title="statement not covered" > req.on("end", (<span class="fstat-no" title="function not covered" >) </span>=> resolve<span class="cstat-no" title="statement not covered" >(data));</span></span>
|
|
2475
|
+
})<span class="cstat-no" title="statement not covered" >;<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" ></span></span></span>
|
|
2476
|
+
const { title, content } = JSON.parse(body);
|
|
2477
|
+
this.#articles.update(parse<span class="cstat-no" title="statement not covered" >Int(id), title, c</span>ontent);
|
|
2478
2478
|
<span class="cstat-no" title="statement not covered" > if (this.#databaseModel) {</span>
|
|
2479
2479
|
<span class="cstat-no" title="statement not covered" > await this.#databaseModel.update(parseInt(id), { title, content });</span>
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2480
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2481
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2482
2482
|
<span class="cstat-no" title="statement not covered" > res.end(JSON.stringify({ status: "updated", id }));</span>
|
|
2483
|
-
}
|
|
2483
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2484
2484
|
}
|
|
2485
2485
|
|
|
2486
2486
|
/** set external API */
|
|
@@ -2512,8 +2512,8 @@ export default class Blog {
|
|
|
2512
2512
|
login: "",
|
|
2513
2513
|
};
|
|
2514
2514
|
|
|
2515
|
-
|
|
2516
|
-
else data.
|
|
2515
|
+
if (loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
2516
|
+
<span class="missing-if-branch" title="if path not taken" >I</span>else data.logi<span class="cstat-no" title="statement not covered" >n = `<a href="/login">login</a>`;</span>
|
|
2517
2517
|
|
|
2518
2518
|
//debug("typeof data: %o", typeof data);
|
|
2519
2519
|
//debug("typeof data.articles: %o", typeof data.articles);
|
|
@@ -2522,9 +2522,9 @@ export default class Blog {
|
|
|
2522
2522
|
debug("first article: ");
|
|
2523
2523
|
debug("%d %s %s", article.id, article.title, article.getContentVeryShort());
|
|
2524
2524
|
const html = formatHTML(data);
|
|
2525
|
-
|
|
2526
|
-
<span class="
|
|
2527
|
-
}
|
|
2525
|
+
if (validate(html)) return html;
|
|
2526
|
+
<span class="missing-if-branch" title="else path not taken" >E</span>throw new Error("Error. Invalid HTML!");
|
|
2527
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2528
2528
|
|
|
2529
2529
|
/**
|
|
2530
2530
|
* read files, compare checksums, compile and write to public/styles.min.css
|
|
@@ -2551,8 +2551,8 @@ export default class Blog {
|
|
|
2551
2551
|
const fileData = await Promise.all(
|
|
2552
2552
|
styleFiles.sort().map(async (f) => {
|
|
2553
2553
|
const content = await fs.promises.readFile(f, "utf-8");
|
|
2554
|
-
|
|
2555
|
-
return { path: f,
|
|
2554
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
2555
|
+
<span class="missing-if-branch" title="if path not taken" >I</span>return { path: f, c<span class="cstat-no" title="statement not covered" >ontent };</span>
|
|
2556
2556
|
}),
|
|
2557
2557
|
);
|
|
2558
2558
|
|
|
@@ -2584,9 +2584,9 @@ export default class Blog {
|
|
|
2584
2584
|
path.join(publicDir, "styles.min.css"),
|
|
2585
2585
|
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
2586
2586
|
);
|
|
2587
|
-
} else
|
|
2588
|
-
<span class="
|
|
2589
|
-
}
|
|
2587
|
+
} else {
|
|
2588
|
+
conso<span class="missing-if-branch" title="else path not taken" >E</span>le.log("styles are up-to-date");
|
|
2589
|
+
}<span class="cstat-no" title="statement not covered" ></span>
|
|
2590
2590
|
}
|
|
2591
2591
|
}
|
|
2592
2592
|
|
|
@@ -2596,18 +2596,18 @@ export default class Blog {
|
|
|
2596
2596
|
*/
|
|
2597
2597
|
async #processScripts(files) {
|
|
2598
2598
|
// Normalize input to array
|
|
2599
|
-
const fileList = Array.isArray(files) ?
|
|
2600
|
-
const scriptFiles = fileList.filter(
|
|
2599
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
2600
|
+
const scriptFiles = fileList.filter(<span class="branch-0 cbranch-no" title="branch not covered" ></span>
|
|
2601
2601
|
(f) =>
|
|
2602
2602
|
typeof f === "string" && f.endsWith(".js") && !f.endsWith(".min.js"),
|
|
2603
2603
|
);
|
|
2604
2604
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2605
|
+
if (scriptFiles.length > 0) {
|
|
2606
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> const fileData = await Promise.all(
|
|
2607
2607
|
scriptFiles.map(async (f) => {
|
|
2608
2608
|
const content = await fs.promises.readFile(f, "utf-8");
|
|
2609
|
-
|
|
2610
|
-
return { path: f,
|
|
2609
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
2610
|
+
<span class="missing-if-branch" title="if path not taken" >I</span>return { path: f, c<span class="cstat-no" title="statement not covered" >ontent };</span>
|
|
2611
2611
|
}),
|
|
2612
2612
|
);
|
|
2613
2613
|
|
|
@@ -2622,30 +2622,30 @@ export default class Blog {
|
|
|
2622
2622
|
)
|
|
2623
2623
|
.digest("hex");
|
|
2624
2624
|
|
|
2625
|
-
|
|
2626
|
-
|
|
2625
|
+
if (!this.#scriptsHash) {
|
|
2626
|
+
<span class="missing-if-branch" title="else path not taken" >E</span> try {
|
|
2627
2627
|
const publicDir = path.join(process.cwd(), "public");
|
|
2628
2628
|
const existing = await fs.promises.readFile(
|
|
2629
2629
|
path.join(publicDir, "scripts.min.js"),
|
|
2630
2630
|
"utf-8",
|
|
2631
2631
|
);
|
|
2632
2632
|
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
2633
|
-
|
|
2634
|
-
} catch (err) {
|
|
2633
|
+
if (match) this.#scriptsHash = match[1];
|
|
2634
|
+
} <span class="missing-if-branch" title="else path not taken" >E</span>catch (err) {
|
|
2635
2635
|
// ignore
|
|
2636
2636
|
}
|
|
2637
2637
|
}
|
|
2638
2638
|
|
|
2639
|
-
|
|
2640
|
-
<span class="
|
|
2639
|
+
if (currentHash !== this.#scriptsHash) {
|
|
2640
|
+
<span class="missing-if-branch" title="if path not taken" >I</span> console.log("Script assets have changed. Recompiling...");
|
|
2641
2641
|
<span class="cstat-no" title="statement not covered" > this.#scriptsHash = currentHash;</span>
|
|
2642
|
-
|
|
2643
|
-
const compiledScripts =
|
|
2644
|
-
|
|
2645
|
-
const publicDir =
|
|
2646
|
-
|
|
2642
|
+
<span class="cstat-no" title="statement not covered" ></span>
|
|
2643
|
+
const compiledScripts = await compileScripts(fileData);
|
|
2644
|
+
<span class="cstat-no" title="statement not covered" ></span>
|
|
2645
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
2646
|
+
await fs.promises.<span class="cstat-no" title="statement not covered" >mkdir(publicDir, { recursive: true </span>});
|
|
2647
2647
|
<span class="cstat-no" title="statement not covered" > await fs.promises.writeFile(</span>
|
|
2648
|
-
path.join(publicDir, "scripts.min.js")
|
|
2648
|
+
<span class="cstat-no" title="statement not covered" > path.join(publicDir, "scripts.min.js"),</span>
|
|
2649
2649
|
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
2650
2650
|
);
|
|
2651
2651
|
}
|
|
@@ -2659,7 +2659,7 @@ export default class Blog {
|
|
|
2659
2659
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
2660
2660
|
Code coverage generated by
|
|
2661
2661
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
2662
|
-
at 2026-01-18T09:
|
|
2662
|
+
at 2026-01-18T09:48:57.520Z
|
|
2663
2663
|
</div>
|
|
2664
2664
|
<script src="../prettify.js"></script>
|
|
2665
2665
|
<script>
|