@lexho111/plainblog 0.5.10 → 0.5.12
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 +242 -157
- package/Formatter.js +20 -3
- package/build-styles.js +24 -17
- package/eslint.config.js +27 -45
- package/knip.json +10 -0
- package/model/FileAdapter.js +5 -5
- package/model/FileModel.js +4 -0
- package/model/PostgresAdapter.js +2 -2
- package/model/SequelizeAdapter.js +25 -20
- package/model/SqliteAdapter.js +2 -2
- package/package.json +10 -14
- package/public/styles.min.css +2 -2
- package/test/blog.test.js +120 -6
- package/test/simpleServer.js +0 -2
- package/.eslintignore +0 -0
- package/.eslintrc.json +0 -0
- package/articles.txt +0 -11
- package/blog.db +0 -0
- package/blog.json +0 -110
- package/bloginfo.json +0 -3
- package/lexho111-plainblog-0.4.1.tgz +0 -0
- package/lexho111-plainblog-0.5.7.tgz +0 -0
package/Formatter.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generates the header of the generated html file
|
|
3
|
+
* @param {*} title title of the blog
|
|
4
|
+
* @returns the header for the generated html file
|
|
5
|
+
*/
|
|
1
6
|
export function header(title) {
|
|
2
7
|
return `<!DOCTYPE html>
|
|
3
8
|
<html lang="de">
|
|
@@ -11,10 +16,12 @@ export function header(title) {
|
|
|
11
16
|
</head>`;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* renders content like articles into a browser-ready HTML string.
|
|
21
|
+
* @param {*} data blog data like blogtitle, articles, login information
|
|
22
|
+
* @returns valid html code with article data implanted
|
|
23
|
+
*/
|
|
15
24
|
export function formatHTML(data) {
|
|
16
|
-
//console.log(`${data} ${script} ${style}`);
|
|
17
|
-
//export function formatHTML(data) {
|
|
18
25
|
//const button = `<button type="button" onClick="fillWithContent();" style="margin: 4px;">generate random text</button>`;
|
|
19
26
|
const button = "";
|
|
20
27
|
let form1 = "";
|
|
@@ -59,6 +66,11 @@ export function formatHTML(data) {
|
|
|
59
66
|
</html>`;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
/**
|
|
70
|
+
* format content like articles to markdown
|
|
71
|
+
* @param {*} data blog data like blogtitle and articles
|
|
72
|
+
* @returns valid markdown
|
|
73
|
+
*/
|
|
62
74
|
export function formatMarkdown(data) {
|
|
63
75
|
let markdown = "";
|
|
64
76
|
markdown += `# ${data.title}\n`;
|
|
@@ -70,6 +82,11 @@ export function formatMarkdown(data) {
|
|
|
70
82
|
return markdown;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
/**
|
|
86
|
+
* html validator
|
|
87
|
+
* @param {*} html
|
|
88
|
+
* @returns true if param html is valid html
|
|
89
|
+
*/
|
|
73
90
|
export function validate(html) {
|
|
74
91
|
let test = true; // all tests passed
|
|
75
92
|
if (!(html.includes("<html") && html.includes("</html"))) {
|
package/build-styles.js
CHANGED
|
@@ -1,37 +1,35 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { pathToFileURL } from "url";
|
|
3
2
|
import postcss from "postcss";
|
|
4
3
|
import autoprefixer from "autoprefixer";
|
|
5
4
|
import cssnano from "cssnano";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Compiles CSS styles from file content objects.
|
|
8
|
+
* @param {Array<{path: string, content: string}>} fileData - An array of objects containing file paths and content.
|
|
9
|
+
* @returns {Promise<string>} The compiled and minified CSS.
|
|
10
|
+
*/
|
|
8
11
|
export async function compileStyles(fileData) {
|
|
9
12
|
try {
|
|
10
13
|
let combinedCss = "";
|
|
11
14
|
|
|
15
|
+
// 1. filter out css files
|
|
12
16
|
if (fileData) {
|
|
13
17
|
const scssFiles = fileData.filter(
|
|
14
18
|
(f) =>
|
|
15
19
|
f.path.endsWith(".scss") && !path.basename(f.path).startsWith("_")
|
|
16
20
|
);
|
|
21
|
+
if (scssFiles.length > 0) console.error("sass files are not supported.");
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
console.error("sass files are not supported.");
|
|
20
|
-
}
|
|
21
|
-
|
|
23
|
+
// make one big css file
|
|
22
24
|
const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
|
|
23
25
|
for (const file of cssFiles) {
|
|
24
26
|
combinedCss += file.content + "\n";
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
// 2.
|
|
30
|
+
// 2. minify and uglify with PostCSS
|
|
29
31
|
if (combinedCss) {
|
|
30
|
-
|
|
31
|
-
const result = await postcss(plugins).process(combinedCss, {
|
|
32
|
-
from: undefined,
|
|
33
|
-
});
|
|
34
|
-
return result.css;
|
|
32
|
+
return postcss2(combinedCss);
|
|
35
33
|
}
|
|
36
34
|
return "";
|
|
37
35
|
} catch (error) {
|
|
@@ -40,16 +38,25 @@ export async function compileStyles(fileData) {
|
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
async function postcss2(css) {
|
|
42
|
+
const plugins = [autoprefixer(), cssnano()];
|
|
43
|
+
const result = await postcss(plugins).process(css, {
|
|
44
|
+
from: undefined, // do not print source map warning
|
|
45
|
+
});
|
|
46
|
+
return result.css; // final result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Merges and minifies multiple CSS content strings.
|
|
51
|
+
* @param {...string} cssContents - CSS strings to merge.
|
|
52
|
+
* @returns {Promise<string>} The merged and minified CSS.
|
|
53
|
+
*/
|
|
43
54
|
export async function mergeStyles(...cssContents) {
|
|
44
55
|
try {
|
|
45
56
|
const combinedCss = cssContents.join("\n");
|
|
46
57
|
|
|
47
58
|
if (combinedCss) {
|
|
48
|
-
|
|
49
|
-
const result = await postcss(plugins).process(combinedCss, {
|
|
50
|
-
from: undefined,
|
|
51
|
-
});
|
|
52
|
-
return result.css;
|
|
59
|
+
return postcss2(combinedCss);
|
|
53
60
|
}
|
|
54
61
|
return "";
|
|
55
62
|
} catch (error) {
|
package/eslint.config.js
CHANGED
|
@@ -1,45 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
// Configuration specifically for Jest test files
|
|
30
|
-
files: ["**/*.test.js", "**/*.spec.js"],
|
|
31
|
-
languageOptions: {
|
|
32
|
-
globals: {
|
|
33
|
-
...globals.jest, // Defines Jest global variables (e.g., `describe`, `it`, `expect`)
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
plugins: {
|
|
37
|
-
jest: pluginJest,
|
|
38
|
-
},
|
|
39
|
-
// Recommended Jest rules from `eslint-plugin-jest`
|
|
40
|
-
rules: {
|
|
41
|
-
...pluginJest.configs.recommended.rules,
|
|
42
|
-
// Add or override Jest-specific rules here.
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
];
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import { defineConfig } from "eslint/config";
|
|
4
|
+
import pluginJest from "eslint-plugin-jest";
|
|
5
|
+
|
|
6
|
+
export default defineConfig([
|
|
7
|
+
{
|
|
8
|
+
// Must be in a separate object to apply globally
|
|
9
|
+
ignores: ["public/scripts.min.js", "dist/**/*"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
13
|
+
plugins: { js },
|
|
14
|
+
extends: ["js/recommended"],
|
|
15
|
+
languageOptions: { globals: globals.browser },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
files: ["**/*.test.js", "**/*.spec.js"],
|
|
19
|
+
plugins: { jest: pluginJest },
|
|
20
|
+
languageOptions: {
|
|
21
|
+
globals: pluginJest.environments.globals.globals, // Loads all Jest globals
|
|
22
|
+
},
|
|
23
|
+
rules: {
|
|
24
|
+
...pluginJest.configs["flat/recommended"].rules,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]);
|
package/knip.json
ADDED
package/model/FileAdapter.js
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export default class FileAdapter {
|
|
10
10
|
dbtype = "file";
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.infoFile = "bloginfo.json";
|
|
13
|
-
this.articlesFile = "articles.txt";
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.infoFile = options.infoFilename || "bloginfo.json";
|
|
13
|
+
this.articlesFile = options.articlesFilename || "articles.txt";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async initialize() {
|
|
@@ -41,8 +41,8 @@ export default class FileAdapter {
|
|
|
41
41
|
async findAll(
|
|
42
42
|
limit = 4,
|
|
43
43
|
offset = 0,
|
|
44
|
-
startId = null,
|
|
45
|
-
endId = null,
|
|
44
|
+
//startId = null,
|
|
45
|
+
//endId = null,
|
|
46
46
|
order = "DESC"
|
|
47
47
|
) {
|
|
48
48
|
let dbArticles = [];
|
package/model/FileModel.js
CHANGED
|
@@ -11,6 +11,7 @@ export async function loadInfo(filename) {
|
|
|
11
11
|
const data = await fs.readFile(filename, "utf8");
|
|
12
12
|
return JSON.parse(data);
|
|
13
13
|
} catch (err) {
|
|
14
|
+
console.error(err);
|
|
14
15
|
return { title: "Blog" };
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -29,6 +30,7 @@ export async function loadArticles(filename) {
|
|
|
29
30
|
.filter((line) => line.trim() !== "")
|
|
30
31
|
.map((line) => JSON.parse(line));
|
|
31
32
|
} catch (err) {
|
|
33
|
+
console.error(err);
|
|
32
34
|
return [];
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -38,12 +40,14 @@ export async function initFiles(infoFilename, articlesFilename) {
|
|
|
38
40
|
try {
|
|
39
41
|
await fs.access(infoFilename);
|
|
40
42
|
} catch (err) {
|
|
43
|
+
console.error(err);
|
|
41
44
|
await saveInfo(infoFilename, { title: "Blog" });
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
try {
|
|
45
48
|
await fs.access(articlesFilename);
|
|
46
49
|
} catch (err) {
|
|
50
|
+
console.error(err);
|
|
47
51
|
await fs.writeFile(articlesFilename, "");
|
|
48
52
|
}
|
|
49
53
|
}
|
package/model/PostgresAdapter.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Sequelize, DataTypes, Op } from "sequelize";
|
|
2
1
|
import SequelizeAdapter from "./SequelizeAdapter.js";
|
|
3
2
|
|
|
4
3
|
export default class PostgresAdapter extends SequelizeAdapter {
|
|
@@ -22,6 +21,7 @@ export default class PostgresAdapter extends SequelizeAdapter {
|
|
|
22
21
|
|
|
23
22
|
async initialize() {
|
|
24
23
|
console.log("initialize database");
|
|
24
|
+
await this.loadSequelize();
|
|
25
25
|
const maxRetries = 10;
|
|
26
26
|
const retryDelay = 3000;
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ export default class PostgresAdapter extends SequelizeAdapter {
|
|
|
31
31
|
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
this.sequelize = new Sequelize(
|
|
34
|
+
this.sequelize = new this.Sequelize(
|
|
35
35
|
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
|
|
36
36
|
{ logging: false }
|
|
37
37
|
);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Sequelize, DataTypes, Op } from "sequelize";
|
|
2
|
-
|
|
3
1
|
export default class SequelizeAdapter {
|
|
4
2
|
username;
|
|
5
3
|
password;
|
|
@@ -11,38 +9,44 @@ export default class SequelizeAdapter {
|
|
|
11
9
|
Article;
|
|
12
10
|
BlogInfo;
|
|
13
11
|
|
|
12
|
+
// Dynamic properties
|
|
13
|
+
Sequelize;
|
|
14
|
+
DataTypes;
|
|
15
|
+
Op;
|
|
16
|
+
|
|
14
17
|
constructor(options = {}) {
|
|
15
18
|
console.log(JSON.stringify(options));
|
|
19
|
+
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
async loadSequelize() {
|
|
22
|
+
if (this.Sequelize) return;
|
|
23
|
+
try {
|
|
19
24
|
const sequelizePkg = await import("sequelize");
|
|
20
|
-
Sequelize = sequelizePkg.Sequelize;
|
|
21
|
-
DataTypes = sequelizePkg.DataTypes;
|
|
22
|
-
|
|
23
|
-
//this.#Op = Op;
|
|
25
|
+
this.Sequelize = sequelizePkg.Sequelize;
|
|
26
|
+
this.DataTypes = sequelizePkg.DataTypes;
|
|
27
|
+
this.Op = sequelizePkg.Op;
|
|
24
28
|
} catch (err) {
|
|
29
|
+
console.error(err);
|
|
25
30
|
throw new Error(
|
|
26
31
|
"Sequelize is not installed. Please install it to use PostgresAdapter: npm install sequelize"
|
|
27
32
|
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// throw new Error(`Error! ${databasetype} is an unknown database type.`);
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async initializeModels() {
|
|
37
|
+
await this.loadSequelize();
|
|
34
38
|
this.Article = this.sequelize.define(
|
|
35
39
|
"Article",
|
|
36
40
|
{
|
|
37
|
-
title: DataTypes.STRING,
|
|
38
|
-
content: DataTypes.TEXT,
|
|
41
|
+
title: this.DataTypes.STRING,
|
|
42
|
+
content: this.DataTypes.TEXT,
|
|
39
43
|
createdAt: {
|
|
40
|
-
type: DataTypes.DATE,
|
|
41
|
-
defaultValue: DataTypes.NOW,
|
|
44
|
+
type: this.DataTypes.DATE,
|
|
45
|
+
defaultValue: this.DataTypes.NOW,
|
|
42
46
|
},
|
|
43
47
|
updatedAt: {
|
|
44
|
-
type: DataTypes.DATE,
|
|
45
|
-
defaultValue: DataTypes.NOW,
|
|
48
|
+
type: this.DataTypes.DATE,
|
|
49
|
+
defaultValue: this.DataTypes.NOW,
|
|
46
50
|
},
|
|
47
51
|
},
|
|
48
52
|
{
|
|
@@ -53,7 +57,7 @@ export default class SequelizeAdapter {
|
|
|
53
57
|
this.BlogInfo = this.sequelize.define(
|
|
54
58
|
"BlogInfo",
|
|
55
59
|
{
|
|
56
|
-
title: DataTypes.STRING,
|
|
60
|
+
title: this.DataTypes.STRING,
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
timestamps: false,
|
|
@@ -80,13 +84,14 @@ export default class SequelizeAdapter {
|
|
|
80
84
|
endId = null,
|
|
81
85
|
order = "DESC"
|
|
82
86
|
) {
|
|
87
|
+
await this.loadSequelize();
|
|
83
88
|
const where = {};
|
|
84
89
|
if (startId !== null && endId !== null) {
|
|
85
90
|
where.id = {
|
|
86
|
-
[Op.between]: [Math.min(startId, endId), Math.max(startId, endId)],
|
|
91
|
+
[this.Op.between]: [Math.min(startId, endId), Math.max(startId, endId)],
|
|
87
92
|
};
|
|
88
93
|
} else if (startId !== null) {
|
|
89
|
-
where.id = { [order === "DESC" ? Op.lte : Op.gte]: startId };
|
|
94
|
+
where.id = { [order === "DESC" ? this.Op.lte : this.Op.gte]: startId };
|
|
90
95
|
}
|
|
91
96
|
const options = {
|
|
92
97
|
where,
|
package/model/SqliteAdapter.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Sequelize } from "sequelize";
|
|
2
1
|
import SequelizeAdapter from "./SequelizeAdapter.js";
|
|
3
2
|
|
|
4
3
|
export default class SqliteAdapter extends SequelizeAdapter {
|
|
@@ -11,8 +10,9 @@ export default class SqliteAdapter extends SequelizeAdapter {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
async initialize() {
|
|
13
|
+
await this.loadSequelize();
|
|
14
14
|
try {
|
|
15
|
-
this.sequelize = new Sequelize({
|
|
15
|
+
this.sequelize = new this.Sequelize({
|
|
16
16
|
dialect: "sqlite",
|
|
17
17
|
storage: this.dbname + ".db",
|
|
18
18
|
logging: false,
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "node index.js",
|
|
9
9
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
10
|
-
"lint": "eslint ."
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"knip": "knip"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"blog",
|
|
@@ -18,26 +19,21 @@
|
|
|
18
19
|
"license": "ISC",
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"autoprefixer": "^10.4.23",
|
|
21
|
-
"child_process": "^1.0.2",
|
|
22
22
|
"cssnano": "^7.1.2",
|
|
23
|
-
"fs": "^0.0.1-security",
|
|
24
|
-
"http": "^0.0.1-security",
|
|
25
23
|
"node-fetch": "^3.3.2",
|
|
26
|
-
"
|
|
27
|
-
"postcss": "^8.5.6",
|
|
28
|
-
"postcss-preset-env": "^10.6.0",
|
|
29
|
-
"sass": "^1.97.1",
|
|
30
|
-
"url": "^0.11.4",
|
|
31
|
-
"util": "^0.12.5"
|
|
24
|
+
"postcss": "^8.5.6"
|
|
32
25
|
},
|
|
33
26
|
"devDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
27
|
+
"@eslint/js": "^9.39.2",
|
|
28
|
+
"@types/node": "^25.0.3",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
36
30
|
"eslint-plugin-jest": "^28.6.0",
|
|
31
|
+
"globals": "^17.0.0",
|
|
37
32
|
"jest": "^29.7.0",
|
|
38
|
-
"
|
|
33
|
+
"typescript": "^5.9.3"
|
|
39
34
|
},
|
|
40
35
|
"optionalDependencies": {
|
|
36
|
+
"sqlite3": "^5.1.7",
|
|
41
37
|
"pg": "^8.16.3",
|
|
42
38
|
"pg-hstore": "^2.3.4",
|
|
43
39
|
"sequelize": "^6.37.7"
|
package/public/styles.min.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
body{
|
|
2
|
-
/* source-hash:
|
|
1
|
+
body{background-color:#fdfdfd;font-family:Arial}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}.datetime{color:#434343;font-style:normal}h2{color:#a9a9a9;margin:0 0 5px}p{margin-top:10px}span{margin:0}
|
|
2
|
+
/* source-hash: fa9deb7a7f0781f463cd3e8fd3c3ceddec518535b0a6d13af7309ef9a2f76c32 */
|
package/test/blog.test.js
CHANGED
|
@@ -4,8 +4,49 @@ import fs from "node:fs";
|
|
|
4
4
|
import Blog from "../Blog.js";
|
|
5
5
|
import Article from "../Article.js";
|
|
6
6
|
import { jest } from "@jest/globals";
|
|
7
|
+
import DatabaseModel from "../model/DatabaseModel.js";
|
|
7
8
|
|
|
8
9
|
describe("test blog", () => {
|
|
10
|
+
test("blog bootstrap stage 1", () => {
|
|
11
|
+
const myblog = new Blog();
|
|
12
|
+
const json = myblog.json();
|
|
13
|
+
expect(json.version).toContain(".");
|
|
14
|
+
const database = json.database;
|
|
15
|
+
expect(database.type).toBe("file");
|
|
16
|
+
expect(database.username).toBe("user");
|
|
17
|
+
expect(database.password).toBe("password");
|
|
18
|
+
expect(database.host).toBe("localhost");
|
|
19
|
+
expect(database.dbname).toBe("articles.txt"); //TODO switch blog.json to articles.txt, bloginfo.json
|
|
20
|
+
expect(json.password).toBe("admin");
|
|
21
|
+
expect(json.styles).toContain("body { font-family: Arial; }");
|
|
22
|
+
expect(json.reloadStylesOnGET).not.toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
test("blog bootstrap stage 2", async () => {
|
|
25
|
+
const myblog = new Blog();
|
|
26
|
+
await myblog.init();
|
|
27
|
+
const json = myblog.json();
|
|
28
|
+
console.log(json);
|
|
29
|
+
expect(json.title).toBe("Test Blog Title");
|
|
30
|
+
expect(json.articles.length).toBeGreaterThan(2);
|
|
31
|
+
expect(json.server).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
test("blog bootstrap stage 3", async () => {
|
|
34
|
+
const myblog = new Blog();
|
|
35
|
+
await myblog.init();
|
|
36
|
+
try {
|
|
37
|
+
await myblog.startServer(8080);
|
|
38
|
+
const json = myblog.json();
|
|
39
|
+
console.log(json);
|
|
40
|
+
expect(json.title).toBe("Test Blog Title"); // from bloginfo.json
|
|
41
|
+
expect(json.articles.length).toBeGreaterThan(2);
|
|
42
|
+
expect(json.server.listening).toBeTruthy();
|
|
43
|
+
expect(json.server.address.address).toBe("127.0.0.1");
|
|
44
|
+
expect(json.server.address.family).toBe("IPv4");
|
|
45
|
+
expect(json.server.address.port).toBe(8080);
|
|
46
|
+
} finally {
|
|
47
|
+
await myblog.closeServer();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
9
50
|
test("is valid html", async () => {
|
|
10
51
|
const myblog = new Blog();
|
|
11
52
|
const html = await myblog.toHTML();
|
|
@@ -35,21 +76,27 @@ describe("test blog", () => {
|
|
|
35
76
|
const article = new Article("", "");
|
|
36
77
|
myblog.addArticle(article);
|
|
37
78
|
const html = await myblog.toHTML();
|
|
79
|
+
const json = myblog.json();
|
|
38
80
|
expect(html).toContain("<article");
|
|
39
|
-
expect(
|
|
81
|
+
expect(json.articles).toHaveLength(1);
|
|
40
82
|
});
|
|
41
83
|
test("add articles", async () => {
|
|
42
84
|
const myblog = new Blog();
|
|
43
|
-
|
|
85
|
+
const json = myblog.json();
|
|
86
|
+
expect(json.articles).toHaveLength(0);
|
|
44
87
|
const size = 10;
|
|
45
88
|
for (let i = 1; i <= size; i++) {
|
|
46
89
|
const article = new Article("", "");
|
|
47
90
|
myblog.addArticle(article);
|
|
48
|
-
|
|
91
|
+
const json = myblog.json();
|
|
92
|
+
expect(json.articles).toHaveLength(i);
|
|
93
|
+
}
|
|
94
|
+
{
|
|
95
|
+
const html = await myblog.toHTML();
|
|
96
|
+
expect(html).toContain("<article");
|
|
97
|
+
const json = myblog.json();
|
|
98
|
+
expect(json.articles).toHaveLength(size);
|
|
49
99
|
}
|
|
50
|
-
const html = await myblog.toHTML();
|
|
51
|
-
expect(html).toContain("<article");
|
|
52
|
-
expect(myblog.articles.length).toBe(size);
|
|
53
100
|
});
|
|
54
101
|
const __filename = fileURLToPath(import.meta.url);
|
|
55
102
|
const __dirname = path.dirname(__filename);
|
|
@@ -101,6 +148,73 @@ describe("test blog", () => {
|
|
|
101
148
|
// Clean up the spy to restore the original console.log
|
|
102
149
|
consoleSpy.mockRestore();
|
|
103
150
|
});
|
|
151
|
+
test("json() returns a deep copy (immutability check)", () => {
|
|
152
|
+
const myblog = new Blog();
|
|
153
|
+
const article = new Article("Original Title", "Content", new Date());
|
|
154
|
+
myblog.addArticle(article);
|
|
155
|
+
|
|
156
|
+
const json = myblog.json();
|
|
157
|
+
|
|
158
|
+
// Attempt to modify the returned JSON
|
|
159
|
+
json.title = "Modified Title";
|
|
160
|
+
json.articles[0].title = "Modified Article";
|
|
161
|
+
json.database.host = "evil.com";
|
|
162
|
+
|
|
163
|
+
// Verify the internal state is unchanged
|
|
164
|
+
const newJson = myblog.json();
|
|
165
|
+
expect(newJson.title).not.toBe("Modified Title");
|
|
166
|
+
expect(newJson.articles[0].title).toBe("Original Title");
|
|
167
|
+
expect(newJson.database.host).toBe("localhost");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("closeServer handles non-running server gracefully", async () => {
|
|
171
|
+
const myblog = new Blog();
|
|
172
|
+
// Should not throw even if server was never started
|
|
173
|
+
await expect(myblog.closeServer()).resolves.not.toThrow();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("init() runs database operations concurrently", async () => {
|
|
177
|
+
const myblog = new Blog();
|
|
178
|
+
// Avoid file operations to isolate database timing
|
|
179
|
+
myblog.stylesheetPath = [];
|
|
180
|
+
|
|
181
|
+
const delay = 100;
|
|
182
|
+
|
|
183
|
+
// Mock DatabaseModel methods to simulate slow DB operations
|
|
184
|
+
const getTitleSpy = jest
|
|
185
|
+
.spyOn(DatabaseModel.prototype, "getBlogTitle")
|
|
186
|
+
.mockImplementation(async () => {
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
188
|
+
return "Mock Title";
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const findAllSpy = jest
|
|
192
|
+
.spyOn(DatabaseModel.prototype, "findAll")
|
|
193
|
+
.mockImplementation(async () => {
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
195
|
+
return [];
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Mock initialize to avoid side effects
|
|
199
|
+
const initSpy = jest
|
|
200
|
+
.spyOn(DatabaseModel.prototype, "initialize")
|
|
201
|
+
.mockResolvedValue();
|
|
202
|
+
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
await myblog.init();
|
|
205
|
+
const end = Date.now();
|
|
206
|
+
const duration = end - start;
|
|
207
|
+
|
|
208
|
+
// If sequential: delay + delay = 200ms. If concurrent: ~100ms.
|
|
209
|
+
expect(duration).toBeLessThan(delay * 1.8);
|
|
210
|
+
expect(getTitleSpy).toHaveBeenCalled();
|
|
211
|
+
expect(findAllSpy).toHaveBeenCalled();
|
|
212
|
+
|
|
213
|
+
// Cleanup
|
|
214
|
+
getTitleSpy.mockRestore();
|
|
215
|
+
findAllSpy.mockRestore();
|
|
216
|
+
initSpy.mockRestore();
|
|
217
|
+
});
|
|
104
218
|
});
|
|
105
219
|
|
|
106
220
|
/*
|
package/test/simpleServer.js
CHANGED
package/.eslintignore
DELETED
|
File without changes
|
package/.eslintrc.json
DELETED
|
File without changes
|
package/articles.txt
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:19:39.939Z"}
|
|
2
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:34:38.866Z"}
|
|
3
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:43:32.343Z"}
|
|
4
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:48:23.123Z"}
|
|
5
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:50:06.993Z"}
|
|
6
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:56:28.369Z"}
|
|
7
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:57:53.780Z"}
|
|
8
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:58:54.261Z"}
|
|
9
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:02.613Z"}
|
|
10
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:30.473Z"}
|
|
11
|
-
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:03:33.773Z"}
|
package/blog.db
DELETED
|
Binary file
|