@lexho111/plainblog 0.5.18 → 0.5.20
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 -2
- package/package.json +1 -1
- package/public/styles.min.css +1 -0
- package/test/article.test.js +0 -31
- package/test/blog.test.js +0 -231
- package/test/model.test.js +0 -229
- package/test/server.test.js +0 -123
- package/test/simpleServer.js +0 -41
- package/test/styles.test.js +0 -82
- package/test/stylesheets/styles.css +0 -29
- package/test/stylesheets/styles.scss +0 -34
package/Blog.js
CHANGED
|
@@ -197,7 +197,7 @@ export default class Blog {
|
|
|
197
197
|
//this.loadStyles();
|
|
198
198
|
//this.loadScripts();
|
|
199
199
|
// if there is a stylesheet path provided, process it
|
|
200
|
-
if (this.#stylesheetPath != null) {
|
|
200
|
+
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
201
201
|
// read file from stylesheet path, compare checksums and write to public/styles.min.css
|
|
202
202
|
await this.#processStylesheets(this.#stylesheetPath);
|
|
203
203
|
}
|
|
@@ -384,7 +384,7 @@ export default class Blog {
|
|
|
384
384
|
|
|
385
385
|
// reload styles and scripts on (every) request
|
|
386
386
|
if (this.reloadStylesOnGET) {
|
|
387
|
-
if (this.#stylesheetPath) {
|
|
387
|
+
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
388
388
|
await this.#processStylesheets(this.#stylesheetPath);
|
|
389
389
|
}
|
|
390
390
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
body{font-family:Arial}.grid{border:0 solid #000;display:grid;grid-gap:.25rem;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;word-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;-webkit-text-decoration:underline;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}#wrapper{max-width:500px;width:100%}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}
|
package/test/article.test.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import Article from "../Article.js";
|
|
2
|
-
|
|
3
|
-
test("get content", () => {
|
|
4
|
-
const article = new Article("title", "text text text");
|
|
5
|
-
const content = article.getContent();
|
|
6
|
-
expect(article.title).toContain("title");
|
|
7
|
-
expect(content).toContain("text text text");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("get shortened content 1", () => {
|
|
11
|
-
const article = new Article("title", "text text text");
|
|
12
|
-
const content = article.getContentShort();
|
|
13
|
-
expect(article.title).toContain("title");
|
|
14
|
-
const text_short = "text text text";
|
|
15
|
-
expect(content).toContain(text_short);
|
|
16
|
-
expect(content).not.toContain("...");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("get shortened content 2", () => {
|
|
20
|
-
let text = "text";
|
|
21
|
-
for (let i = 0; i < 500; i++) {
|
|
22
|
-
text += " ";
|
|
23
|
-
text += "text";
|
|
24
|
-
}
|
|
25
|
-
const text_long = text;
|
|
26
|
-
const article = new Article("title", text_long);
|
|
27
|
-
const content = article.getContentShort();
|
|
28
|
-
expect(article.title).toContain("title");
|
|
29
|
-
expect(content).toContain("text text text");
|
|
30
|
-
expect(content).toContain("...");
|
|
31
|
-
});
|
package/test/blog.test.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { fileURLToPath } from "url";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import Blog from "../Blog.js";
|
|
5
|
-
import Article from "../Article.js";
|
|
6
|
-
import { jest } from "@jest/globals";
|
|
7
|
-
import DatabaseModel from "../model/DatabaseModel.js";
|
|
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
|
-
});
|
|
50
|
-
test("is valid html", async () => {
|
|
51
|
-
const myblog = new Blog();
|
|
52
|
-
const html = await myblog.toHTML();
|
|
53
|
-
expect(html).toContain("html>");
|
|
54
|
-
expect(html).toContain("</html>");
|
|
55
|
-
expect(html).toContain("<body");
|
|
56
|
-
expect(html).toContain("</body>");
|
|
57
|
-
expect(html).toContain("<div"); // container
|
|
58
|
-
expect(html).toContain("</div>");
|
|
59
|
-
});
|
|
60
|
-
test("creates Blog with specified title", async () => {
|
|
61
|
-
const titles = ["TestBlog", "Blog1234", "abcdfg", "kiwi"];
|
|
62
|
-
for (const title of titles) {
|
|
63
|
-
const myblog = new Blog();
|
|
64
|
-
myblog.setTitle(title);
|
|
65
|
-
const html = await myblog.toHTML();
|
|
66
|
-
expect(html).toContain(title);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
test("empty blog without any article", async () => {
|
|
70
|
-
const myblog = new Blog();
|
|
71
|
-
const html = await myblog.toHTML();
|
|
72
|
-
expect(html).not.toContain("<article");
|
|
73
|
-
});
|
|
74
|
-
test("add article", async () => {
|
|
75
|
-
const myblog = new Blog();
|
|
76
|
-
const article = new Article("", "");
|
|
77
|
-
myblog.addArticle(article);
|
|
78
|
-
const html = await myblog.toHTML();
|
|
79
|
-
const json = myblog.json();
|
|
80
|
-
expect(html).toContain("<article");
|
|
81
|
-
expect(json.articles).toHaveLength(1);
|
|
82
|
-
});
|
|
83
|
-
test("add articles", async () => {
|
|
84
|
-
const myblog = new Blog();
|
|
85
|
-
const json = myblog.json();
|
|
86
|
-
expect(json.articles).toHaveLength(0);
|
|
87
|
-
const size = 10;
|
|
88
|
-
for (let i = 1; i <= size; i++) {
|
|
89
|
-
const article = new Article("", "");
|
|
90
|
-
myblog.addArticle(article);
|
|
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);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
102
|
-
const __dirname = path.dirname(__filename);
|
|
103
|
-
const publicDir = path.join(__dirname, "../public");
|
|
104
|
-
test("set style", async () => {
|
|
105
|
-
const styles = [
|
|
106
|
-
{
|
|
107
|
-
style: "body { font-family: Courier; }",
|
|
108
|
-
expected: "font-family:Courier",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
style: "body{ background-color:black; color:white; }",
|
|
112
|
-
expected: "background-color:#000;color:#fff;",
|
|
113
|
-
},
|
|
114
|
-
{ style: "body{ font-size: 1.2em; }", expected: "font-size:1.2em" },
|
|
115
|
-
];
|
|
116
|
-
for (const style of styles) {
|
|
117
|
-
const cssPath = path.join(publicDir, "styles.min.css");
|
|
118
|
-
if (fs.existsSync(cssPath)) {
|
|
119
|
-
await fs.promises.unlink(cssPath);
|
|
120
|
-
}
|
|
121
|
-
const myblog = new Blog();
|
|
122
|
-
myblog.setStyle(style.style);
|
|
123
|
-
await myblog.init();
|
|
124
|
-
const publicCSS = await fs.promises.readFile(cssPath, "utf8");
|
|
125
|
-
expect(publicCSS).toContain(style.expected);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
test("print method logs title and articles to console", async () => {
|
|
129
|
-
// Arrange: Set up the blog with a title and articles
|
|
130
|
-
const myblog = new Blog();
|
|
131
|
-
myblog.setTitle("My Test Blog");
|
|
132
|
-
myblog.addArticle(new Article("Article 1", "Content 1"));
|
|
133
|
-
|
|
134
|
-
// Spy on console.log to capture its output without printing to the test runner
|
|
135
|
-
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
136
|
-
|
|
137
|
-
// Act: Call the method we want to test
|
|
138
|
-
myblog.print();
|
|
139
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
140
|
-
|
|
141
|
-
// Assert: Check if console.log was called with the correct strings
|
|
142
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
143
|
-
const output = consoleSpy.mock.calls.map((call) => call[0]).join("\n");
|
|
144
|
-
expect(output).toContain("# My Test Blog");
|
|
145
|
-
expect(output).toContain("## Article 1");
|
|
146
|
-
expect(output).toContain("Content 1");
|
|
147
|
-
|
|
148
|
-
// Clean up the spy to restore the original console.log
|
|
149
|
-
consoleSpy.mockRestore();
|
|
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
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
/*
|
|
221
|
-
test Blog.js
|
|
222
|
-
ok - new Blog()
|
|
223
|
-
ok - set title
|
|
224
|
-
ok - set style
|
|
225
|
-
ok - add article
|
|
226
|
-
- set api
|
|
227
|
-
- fetch/post
|
|
228
|
-
- save/load
|
|
229
|
-
ok - print
|
|
230
|
-
ok - toHTML
|
|
231
|
-
*/
|
package/test/model.test.js
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import Blog from "../Blog.js";
|
|
2
|
-
import Article from "../Article.js";
|
|
3
|
-
import { fetchData, postData } from "../model/APIModel.js";
|
|
4
|
-
import { server } from "./simpleServer.js";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
saveInfo,
|
|
9
|
-
loadInfo,
|
|
10
|
-
appendArticle,
|
|
11
|
-
loadArticles,
|
|
12
|
-
initFiles,
|
|
13
|
-
} from "../model/FileModel.js";
|
|
14
|
-
|
|
15
|
-
import SqliteAdapter from "../model/SqliteAdapter.js";
|
|
16
|
-
import FileAdapter from "../model/FileAdapter.js";
|
|
17
|
-
|
|
18
|
-
function generateRandomContent(length) {
|
|
19
|
-
let str = "";
|
|
20
|
-
const char = "A";
|
|
21
|
-
for (let i = 0; i < length; i++) {
|
|
22
|
-
const rnd = Math.random() * ("z".charCodeAt(0) - "A".charCodeAt(0)); // A z.charCodeAt(0)"
|
|
23
|
-
const char1 = String.fromCharCode(char.charCodeAt(0) + rnd);
|
|
24
|
-
str += char1;
|
|
25
|
-
}
|
|
26
|
-
return str;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("API Model test", () => {
|
|
30
|
-
beforeAll((done) => {
|
|
31
|
-
const port = 8081;
|
|
32
|
-
server.listen(port, () => {
|
|
33
|
-
console.log(`Simple server running at http://localhost:${port}/`);
|
|
34
|
-
done();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
afterAll(() => {
|
|
38
|
-
return new Promise((done) => {
|
|
39
|
-
server.close(done);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("should load blog data from API", async () => {
|
|
44
|
-
const blog = new Blog();
|
|
45
|
-
blog.setAPI("http://localhost:8081/blog");
|
|
46
|
-
blog.setStyle(
|
|
47
|
-
"body { font-family: Arial, sans-serif; } h1 { color: #333; }"
|
|
48
|
-
);
|
|
49
|
-
await blog.init();
|
|
50
|
-
|
|
51
|
-
const content = generateRandomContent(200);
|
|
52
|
-
|
|
53
|
-
await new Promise((resolve) => {
|
|
54
|
-
const req = {
|
|
55
|
-
on: (event, cb) => {
|
|
56
|
-
if (event === "data") {
|
|
57
|
-
setTimeout(() => cb(`title=hello&content=${content}`), 0);
|
|
58
|
-
}
|
|
59
|
-
if (event === "end") {
|
|
60
|
-
setTimeout(() => cb(), 10);
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
off: () => {},
|
|
64
|
-
};
|
|
65
|
-
const res = {
|
|
66
|
-
writeHead: () => {},
|
|
67
|
-
end: resolve,
|
|
68
|
-
};
|
|
69
|
-
blog.postArticle(req, res);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
expect(await blog.toHTML()).toContain(content);
|
|
73
|
-
expect(blog).toBeDefined();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("should fetch data from API", async () => {
|
|
77
|
-
const API = "http://localhost:8081/blog";
|
|
78
|
-
const data = await fetchData(API);
|
|
79
|
-
expect(data).toBeDefined();
|
|
80
|
-
expect(data.title).toBe("Mock Blog Title");
|
|
81
|
-
expect(data.articles).toHaveLength(2);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("should post blog data to API", async () => {
|
|
85
|
-
const API = "http://localhost:8081/blog";
|
|
86
|
-
const newArticle = { title: "New Post", content: "New Content" };
|
|
87
|
-
const data = await postData(API, newArticle);
|
|
88
|
-
expect(data).toEqual(newArticle);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("Database Model test", () => {
|
|
93
|
-
describe("Sqlite Database Adapter test", () => {
|
|
94
|
-
it("should be empty", async () => {
|
|
95
|
-
const blog = new Blog();
|
|
96
|
-
const sqliteAdapter = new SqliteAdapter({
|
|
97
|
-
dbname: "blog",
|
|
98
|
-
});
|
|
99
|
-
blog.setDatabaseAdapter(sqliteAdapter);
|
|
100
|
-
await blog.init();
|
|
101
|
-
|
|
102
|
-
expect(await blog.toHTML()).not.toContain("<article>");
|
|
103
|
-
expect(blog).toBeDefined();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should load blog data from sqlite database", async () => {
|
|
107
|
-
const blog = new Blog();
|
|
108
|
-
const sqliteAdapter = new SqliteAdapter({
|
|
109
|
-
dbname: "blog",
|
|
110
|
-
});
|
|
111
|
-
blog.setDatabaseAdapter(sqliteAdapter);
|
|
112
|
-
await blog.init();
|
|
113
|
-
|
|
114
|
-
const content = generateRandomContent(200);
|
|
115
|
-
|
|
116
|
-
await new Promise((resolve) => {
|
|
117
|
-
const req = {
|
|
118
|
-
on: (event, cb) => {
|
|
119
|
-
if (event === "data") {
|
|
120
|
-
setTimeout(() => cb(`title=hello&content=${content}`), 0);
|
|
121
|
-
}
|
|
122
|
-
if (event === "end") {
|
|
123
|
-
setTimeout(() => cb(), 20);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
off: () => {},
|
|
127
|
-
};
|
|
128
|
-
const res = {
|
|
129
|
-
writeHead: () => {},
|
|
130
|
-
end: resolve,
|
|
131
|
-
};
|
|
132
|
-
blog.postArticle(req, res);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(await blog.toHTML()).toContain(content);
|
|
136
|
-
expect(blog).toBeDefined();
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe("Postgres Adapter test", () => {});
|
|
140
|
-
describe("File Adapter test", () => {
|
|
141
|
-
it("should update blog title", async () => {
|
|
142
|
-
const title_org = "Test Blog Title";
|
|
143
|
-
const fileAdapter = new FileAdapter();
|
|
144
|
-
await fileAdapter.initialize();
|
|
145
|
-
await fileAdapter.updateBlogTitle(title_org);
|
|
146
|
-
const title = await fileAdapter.getBlogTitle();
|
|
147
|
-
console.log(title);
|
|
148
|
-
expect(title).toBe(title_org);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe("File Model test", () => {
|
|
152
|
-
it("should init files and load info", async () => {
|
|
153
|
-
const infoFile = "test_bloginfo.json";
|
|
154
|
-
const articlesFile = "test_articles.txt";
|
|
155
|
-
|
|
156
|
-
// Ensure clean state
|
|
157
|
-
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
|
|
158
|
-
if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
|
|
159
|
-
|
|
160
|
-
await initFiles(infoFile, articlesFile);
|
|
161
|
-
|
|
162
|
-
const info = await loadInfo(infoFile);
|
|
163
|
-
expect(info.title).toBe("Blog");
|
|
164
|
-
|
|
165
|
-
// Cleanup
|
|
166
|
-
fs.unlinkSync(infoFile);
|
|
167
|
-
fs.unlinkSync(articlesFile);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("should read and write to articles.txt file", async () => {
|
|
171
|
-
const content = generateRandomContent(200);
|
|
172
|
-
const article = new Article("hello", content, new Date().toISOString());
|
|
173
|
-
const filename = "test_articles.txt";
|
|
174
|
-
|
|
175
|
-
await appendArticle(filename, article);
|
|
176
|
-
const articles = await loadArticles(filename);
|
|
177
|
-
|
|
178
|
-
// Compare properties since 'articles' contains plain objects, not Article instances
|
|
179
|
-
expect(articles.length).toBeGreaterThan(0);
|
|
180
|
-
const lastArticle = articles[articles.length - 1];
|
|
181
|
-
expect(lastArticle.title).toBe(article.title);
|
|
182
|
-
expect(lastArticle.content).toBe(article.content);
|
|
183
|
-
|
|
184
|
-
// Cleanup
|
|
185
|
-
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("should be empty articles.txt", async () => {
|
|
189
|
-
const filename = "test_articles_empty.txt";
|
|
190
|
-
const articles = await loadArticles(filename);
|
|
191
|
-
expect(articles.length).toBe(0);
|
|
192
|
-
// Cleanup
|
|
193
|
-
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("should read and write multiple articles to articles.txt file", async () => {
|
|
197
|
-
const filename = "test_articles.txt";
|
|
198
|
-
|
|
199
|
-
// Ensure clean state
|
|
200
|
-
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const articles_org = [];
|
|
204
|
-
const count = 20;
|
|
205
|
-
for (let i = 0; i < count; i++) {
|
|
206
|
-
const title = generateRandomContent(20);
|
|
207
|
-
const content = generateRandomContent(200);
|
|
208
|
-
const article = new Article(title, content, new Date().toISOString());
|
|
209
|
-
articles_org.push(article);
|
|
210
|
-
|
|
211
|
-
await appendArticle(filename, article);
|
|
212
|
-
}
|
|
213
|
-
const articles = await loadArticles(filename);
|
|
214
|
-
|
|
215
|
-
// Compare properties since 'articles' contains plain objects, not Article instances
|
|
216
|
-
expect(articles).toHaveLength(count);
|
|
217
|
-
|
|
218
|
-
for (let i = 0; i < articles.length; i++) {
|
|
219
|
-
expect(articles[i].title).toBe(articles_org[i].title);
|
|
220
|
-
expect(articles[i].content).toBe(articles_org[i].content);
|
|
221
|
-
expect(articles[i].createdAt).toBe(articles_org[i].createdAt);
|
|
222
|
-
}
|
|
223
|
-
} finally {
|
|
224
|
-
// Cleanup
|
|
225
|
-
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
});
|
package/test/server.test.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import fetch from "node-fetch";
|
|
2
|
-
import Blog from "../Blog.js";
|
|
3
|
-
|
|
4
|
-
/** this is a test for
|
|
5
|
-
* import Blog from "@lexho111/plainblog";
|
|
6
|
-
*
|
|
7
|
-
* const blog = new Blog();
|
|
8
|
-
* blog.setTitle("My Blog");
|
|
9
|
-
* blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
|
|
10
|
-
*
|
|
11
|
-
* blog.startServer(8080);
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const PORT = 8090;
|
|
15
|
-
const apiBaseUrl = `http://localhost:${PORT}`;
|
|
16
|
-
|
|
17
|
-
describe("server test", () => {
|
|
18
|
-
const blog = new Blog(); // Create one blog instance for all tests
|
|
19
|
-
blog.setTitle("My Blog");
|
|
20
|
-
|
|
21
|
-
// Use beforeAll to set up and start the server once before any tests run
|
|
22
|
-
beforeAll(async () => {
|
|
23
|
-
//blog.database.type = "sqlite";
|
|
24
|
-
blog.database.dbname = "test_server_db";
|
|
25
|
-
blog.setStyle(
|
|
26
|
-
"body { font-family: Arial, sans-serif; } h1 { color: #333; }"
|
|
27
|
-
);
|
|
28
|
-
await blog.init();
|
|
29
|
-
// Await it to ensure the server is running before tests start.
|
|
30
|
-
await blog.startServer(PORT);
|
|
31
|
-
}, 10000);
|
|
32
|
-
|
|
33
|
-
// Use afterAll to shut down the server once after all tests have finished
|
|
34
|
-
afterAll(async () => {
|
|
35
|
-
// Await the closing of the server to ensure a clean shutdown.
|
|
36
|
-
await blog.closeServer();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("check if server responds to /", async () => {
|
|
40
|
-
// get articles from blog api
|
|
41
|
-
const response = await fetch(`${apiBaseUrl}/`, {
|
|
42
|
-
method: "GET",
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(response.status).toBe(200);
|
|
46
|
-
const html = await response.text();
|
|
47
|
-
expect(html).toContain("My Blog");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("check if server responds to /api", async () => {
|
|
51
|
-
// get articles from blog api
|
|
52
|
-
const response = await fetch(`${apiBaseUrl}/api`, {
|
|
53
|
-
method: "GET",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(response.status).toBe(200);
|
|
57
|
-
const responseData = await response.json();
|
|
58
|
-
expect(responseData.title).toBe("My Blog");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("should post a new article via /api/articles not logged in", async () => {
|
|
62
|
-
const content = "This is the content of the test article." + new Date();
|
|
63
|
-
const newArticle = {
|
|
64
|
-
title: "Test Title from Jest",
|
|
65
|
-
content: content,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// post new article to the blog
|
|
69
|
-
// WITHOUT Login
|
|
70
|
-
|
|
71
|
-
const response = await fetch(`${apiBaseUrl}/api/articles`, {
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: { "Content-Type": "application/json" },
|
|
74
|
-
body: JSON.stringify(newArticle),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(response.status).not.toBe(201);
|
|
78
|
-
const responseData = await response.json();
|
|
79
|
-
expect(responseData).not.toEqual(newArticle);
|
|
80
|
-
|
|
81
|
-
expect(await blog.toHTML()).not.toContain(newArticle.content); // does blog contain my new article?
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("should post a new article via /api/articles logged in", async () => {
|
|
85
|
-
const newArticle = {
|
|
86
|
-
title: "Test Title from Jest",
|
|
87
|
-
content: "This is the content of the test article.",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// post new article to the blog
|
|
91
|
-
// Login to get session cookie
|
|
92
|
-
const loginResponse = await fetch(`${apiBaseUrl}/login`, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
body: "password=admin",
|
|
95
|
-
redirect: "manual",
|
|
96
|
-
});
|
|
97
|
-
const cookie = loginResponse.headers.get("set-cookie");
|
|
98
|
-
|
|
99
|
-
const response = await fetch(`${apiBaseUrl}/api/articles`, {
|
|
100
|
-
method: "POST",
|
|
101
|
-
headers: { "Content-Type": "application/json", Cookie: cookie },
|
|
102
|
-
body: JSON.stringify(newArticle),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(response.status).toBe(201);
|
|
106
|
-
const responseData = await response.json();
|
|
107
|
-
expect(responseData.title).toEqual(newArticle.title);
|
|
108
|
-
expect(responseData.content).toEqual(newArticle.content);
|
|
109
|
-
|
|
110
|
-
expect(await blog.toHTML()).toContain(newArticle.content); // does blog contain my new article?
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("performance: server should respond within acceptable time (e.g. 500ms)", async () => {
|
|
114
|
-
const start = Date.now();
|
|
115
|
-
const response = await fetch(`${apiBaseUrl}/api/articles`);
|
|
116
|
-
const end = Date.now();
|
|
117
|
-
const duration = end - start;
|
|
118
|
-
|
|
119
|
-
expect(response.status).toBe(200);
|
|
120
|
-
console.log(`Request took ${duration}ms`);
|
|
121
|
-
expect(duration).toBeLessThan(500); // Fail if request takes longer than 500ms
|
|
122
|
-
});
|
|
123
|
-
});
|
package/test/simpleServer.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import http from "http";
|
|
2
|
-
|
|
3
|
-
export const server = http.createServer((req, res) => {
|
|
4
|
-
// Set CORS headers to allow requests from different origins if needed
|
|
5
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
6
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
7
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
8
|
-
|
|
9
|
-
if (req.method === "OPTIONS") {
|
|
10
|
-
res.writeHead(204);
|
|
11
|
-
res.end();
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (req.url === "/blog") {
|
|
16
|
-
if (req.method === "GET") {
|
|
17
|
-
const data = {
|
|
18
|
-
title: "Mock Blog Title",
|
|
19
|
-
articles: [
|
|
20
|
-
{ title: "Mock Article 1", content: "Content of mock article 1" },
|
|
21
|
-
{ title: "Mock Article 2", content: "Content of mock article 2" },
|
|
22
|
-
],
|
|
23
|
-
};
|
|
24
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
25
|
-
res.end(JSON.stringify(data));
|
|
26
|
-
} else if (req.method === "POST") {
|
|
27
|
-
let body = "";
|
|
28
|
-
req.on("data", (chunk) => (body += chunk.toString()));
|
|
29
|
-
req.on("end", () => {
|
|
30
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
31
|
-
res.end(body); // Echo the received data back
|
|
32
|
-
});
|
|
33
|
-
} else {
|
|
34
|
-
res.writeHead(405); // Method Not Allowed
|
|
35
|
-
res.end();
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
res.writeHead(404); // Not Found
|
|
39
|
-
res.end();
|
|
40
|
-
}
|
|
41
|
-
});
|
package/test/styles.test.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import Blog from "../Blog.js";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
const publicDir = path.join(__dirname, "../public");
|
|
9
|
-
|
|
10
|
-
describe("Blog Stylesheet Test", () => {
|
|
11
|
-
it("should load the stylesheet (.css) file", async () => {
|
|
12
|
-
const filepath = path.join(__dirname, "stylesheets/styles.css");
|
|
13
|
-
|
|
14
|
-
const data = await fs.promises.readFile(filepath, "utf8");
|
|
15
|
-
console.log(data);
|
|
16
|
-
expect(data).toContain("body");
|
|
17
|
-
expect(data).toContain("nav a");
|
|
18
|
-
expect(data).toContain(".datetime");
|
|
19
|
-
expect(data).toContain("color: darkgray");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("should load the stylesheet (.css) file from public", async () => {
|
|
23
|
-
const filepath = path.join(__dirname, "../public/styles.min.css");
|
|
24
|
-
|
|
25
|
-
const data = await fs.promises.readFile(filepath, "utf8");
|
|
26
|
-
console.log(data);
|
|
27
|
-
expect(data).toContain("font-family:Arial");
|
|
28
|
-
expect(data).toContain("h1");
|
|
29
|
-
expect(data).toContain(".grid{");
|
|
30
|
-
expect(data).toContain(".grid article");
|
|
31
|
-
expect(data).toContain("nav a");
|
|
32
|
-
expect(data).toContain(".datetime");
|
|
33
|
-
expect(data).toContain("nav a:visited{");
|
|
34
|
-
expect(data).toContain("@media screen");
|
|
35
|
-
expect(data).toContain("#wrapper{");
|
|
36
|
-
expect(data).not.toContain("color:darkgray");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("should load and compile the stylesheet (.css) correctly", async () => {
|
|
40
|
-
const blog = new Blog();
|
|
41
|
-
blog.title = "My Blog";
|
|
42
|
-
blog.database.dbname = "test_styles_3";
|
|
43
|
-
blog.stylesheetPath = "test/stylesheets/styles.css";
|
|
44
|
-
await blog.init();
|
|
45
|
-
|
|
46
|
-
const publicCSS = await fs.promises.readFile(
|
|
47
|
-
path.join(publicDir, "styles.min.css"),
|
|
48
|
-
"utf8"
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
expect(publicCSS).not.toContain(
|
|
52
|
-
"<style>body { font-family: Arial; }</style>"
|
|
53
|
-
);
|
|
54
|
-
expect(publicCSS).toContain("body");
|
|
55
|
-
expect(publicCSS).toContain("nav a");
|
|
56
|
-
expect(publicCSS).toContain(".datetime");
|
|
57
|
-
expect(publicCSS).toContain("font-style:normal");
|
|
58
|
-
expect(publicCSS).toContain("color:");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("should load and compile the stylesheet (.css) correctly [Array]", async () => {
|
|
62
|
-
const blog = new Blog();
|
|
63
|
-
blog.title = "My Blog";
|
|
64
|
-
blog.database.dbname = "test_styles_4";
|
|
65
|
-
blog.stylesheetPath = ["test/stylesheets/styles.css"];
|
|
66
|
-
await blog.init();
|
|
67
|
-
|
|
68
|
-
const publicCSS = await fs.promises.readFile(
|
|
69
|
-
path.join(publicDir, "styles.min.css"),
|
|
70
|
-
"utf8"
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(publicCSS).not.toContain(
|
|
74
|
-
"<style>body { font-family: Arial; }</style>"
|
|
75
|
-
);
|
|
76
|
-
expect(publicCSS).toContain("body");
|
|
77
|
-
expect(publicCSS).toContain("nav a");
|
|
78
|
-
expect(publicCSS).toContain(".datetime");
|
|
79
|
-
expect(publicCSS).toContain("font-style:normal");
|
|
80
|
-
expect(publicCSS).toContain("color:");
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
background-color: rgb(253, 253, 253);
|
|
3
|
-
font-family: Arial;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
nav a {
|
|
7
|
-
text-decoration: underline;
|
|
8
|
-
color: rgb(59, 64, 193);
|
|
9
|
-
font-size: 20px;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.datetime {
|
|
13
|
-
font-style: normal;
|
|
14
|
-
color: rgb(67, 67, 67);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
h2 {
|
|
18
|
-
margin: 0;
|
|
19
|
-
margin-bottom: 5px;
|
|
20
|
-
color: darkgray;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
p {
|
|
24
|
-
margin-top: 10px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
span {
|
|
28
|
-
margin: 0;
|
|
29
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
.grid {
|
|
2
|
-
display: grid;
|
|
3
|
-
grid-template-columns: 1fr;
|
|
4
|
-
gap: 0.25rem;
|
|
5
|
-
border: 0px solid black;
|
|
6
|
-
|
|
7
|
-
article {
|
|
8
|
-
padding: 0.25rem;
|
|
9
|
-
border: 0px solid #ccc;
|
|
10
|
-
border-radius: 4px;
|
|
11
|
-
min-width: 0; /* Allow grid items to shrink */
|
|
12
|
-
overflow-wrap: break-word; /* Break long words */
|
|
13
|
-
p {
|
|
14
|
-
margin-bottom: 0;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
h1 {
|
|
20
|
-
color: rgb(105, 105, 105);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
nav {
|
|
24
|
-
a {
|
|
25
|
-
text-decoration: underline;
|
|
26
|
-
color: rgb(59, 64, 193);
|
|
27
|
-
font-size: 20px;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
a:visited {
|
|
31
|
-
color: rgb(59, 64, 193);
|
|
32
|
-
text-decoration-color: rgb(59, 64, 193);
|
|
33
|
-
}
|
|
34
|
-
}
|