@lexho111/plainblog 0.0.5 → 0.0.8
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/.eslintignore +0 -0
- package/.eslintrc.json +0 -0
- package/Article.js +1 -0
- package/Blog.js +98 -34
- package/README.md +63 -9
- package/api-server.js +175 -0
- package/blog.db +0 -0
- package/eslint.config.js +45 -0
- package/index.js +2 -1
- package/package.json +16 -8
- package/test/article.test.js +31 -0
- package/test/blog.test.js +97 -0
- package/test/server.test.js +29 -0
package/.eslintignore
ADDED
|
File without changes
|
package/.eslintrc.json
ADDED
|
File without changes
|
package/Article.js
CHANGED
package/Blog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
+
import fetch from "node-fetch";
|
|
2
3
|
import { promises as fs } from "fs";
|
|
3
4
|
import { URLSearchParams } from "url";
|
|
4
5
|
import Article from "./Article.js";
|
|
@@ -11,6 +12,33 @@ export default class Blog {
|
|
|
11
12
|
this.filename = null;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
async fetchDataFromApi(apiUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(apiUrl);
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
return data;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Failed to fetch data:", error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async postDataToApi(apiUrl, postData) {
|
|
32
|
+
const response = await fetch(apiUrl, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: JSON.stringify(postData),
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
setTitle(title) {
|
|
15
43
|
this.title = title;
|
|
16
44
|
}
|
|
@@ -23,8 +51,15 @@ export default class Blog {
|
|
|
23
51
|
this.style = style;
|
|
24
52
|
}
|
|
25
53
|
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
APIUrl = "";
|
|
55
|
+
|
|
56
|
+
setAPI(APIUrl) {
|
|
57
|
+
this.APIUrl = APIUrl;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async startServer(port = 8080) {
|
|
61
|
+
if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
|
|
62
|
+
const server = http.createServer(async (req, res) => {
|
|
28
63
|
if (req.method === "POST") {
|
|
29
64
|
let body = "";
|
|
30
65
|
req.on("data", (chunk) => {
|
|
@@ -36,10 +71,16 @@ export default class Blog {
|
|
|
36
71
|
const content = params.get("content");
|
|
37
72
|
|
|
38
73
|
if (title && content) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
const newArticle = new Article(title, content);
|
|
75
|
+
this.addArticle(newArticle);
|
|
76
|
+
|
|
77
|
+
// Post the new article to the backend API
|
|
78
|
+
try {
|
|
79
|
+
const postUrl = new URL(this.APIUrl);
|
|
80
|
+
postUrl.pathname = "/blog/articles";
|
|
81
|
+
await this.postDataToApi(postUrl.toString(), newArticle);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Failed to post new article to API:", error);
|
|
43
84
|
}
|
|
44
85
|
}
|
|
45
86
|
|
|
@@ -48,8 +89,9 @@ export default class Blog {
|
|
|
48
89
|
res.end();
|
|
49
90
|
});
|
|
50
91
|
} else {
|
|
92
|
+
const html = await this.toHTML();
|
|
51
93
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
52
|
-
res.end(
|
|
94
|
+
res.end(html);
|
|
53
95
|
}
|
|
54
96
|
});
|
|
55
97
|
|
|
@@ -59,6 +101,7 @@ export default class Blog {
|
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
async save(filename = this.filename) {
|
|
104
|
+
if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
|
|
62
105
|
if (!filename) {
|
|
63
106
|
console.error("Error: Filename not provided and not set previously.");
|
|
64
107
|
return;
|
|
@@ -80,12 +123,30 @@ export default class Blog {
|
|
|
80
123
|
async load(filename) {
|
|
81
124
|
this.filename = filename;
|
|
82
125
|
const data = await fs.readFile(filename, "utf8");
|
|
83
|
-
const
|
|
84
|
-
this.title = title;
|
|
85
|
-
this.articles = articles
|
|
126
|
+
const jsonData = JSON.parse(data);
|
|
127
|
+
this.title = jsonData.title;
|
|
128
|
+
this.articles = jsonData.articles.map(
|
|
129
|
+
(article) => new Article(article.title, article.content)
|
|
130
|
+
);
|
|
86
131
|
}
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
async loadFromAPI(apiUrl) {
|
|
134
|
+
const data = await this.fetchDataFromApi(apiUrl);
|
|
135
|
+
if (data) {
|
|
136
|
+
this.articles = []; // Clear existing articles before loading new ones
|
|
137
|
+
this.setTitle(data.title);
|
|
138
|
+
// Assuming the API returns an array of objects with title and content
|
|
139
|
+
if (data.articles && Array.isArray(data.articles)) {
|
|
140
|
+
for (const articleData of data.articles) {
|
|
141
|
+
this.addArticle(new Article(articleData.title, articleData.content));
|
|
142
|
+
}
|
|
143
|
+
this.apiDataLoaded = true; // Mark that we have successfully loaded data
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async print() {
|
|
149
|
+
if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
|
|
89
150
|
console.log(`# ${this.title}`);
|
|
90
151
|
for (const article of this.articles) {
|
|
91
152
|
console.log(`## ${article.title}`);
|
|
@@ -93,37 +154,40 @@ export default class Blog {
|
|
|
93
154
|
}
|
|
94
155
|
}
|
|
95
156
|
|
|
96
|
-
toHTML() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
async toHTML() {
|
|
158
|
+
// If we have an API URL and haven't loaded data yet, load it now.
|
|
159
|
+
if (this.APIUrl && !this.apiDataLoaded) {
|
|
160
|
+
await this.loadFromAPI(this.APIUrl);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `<!DOCTYPE html>
|
|
164
|
+
<html lang="de">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="UTF-8">
|
|
167
|
+
<title>${this.title}</title>
|
|
168
|
+
<style>${this.style}</style>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<h1>${this.title}</h1>
|
|
172
|
+
<div style="width: 500px;">
|
|
109
173
|
<form action="/" method="POST">
|
|
110
174
|
<h3>Add a New Article</h3>
|
|
111
175
|
<input type="text" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
|
|
112
176
|
<textarea name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
|
|
113
177
|
<button type="submit">Add Article</button>
|
|
114
178
|
</form>
|
|
115
|
-
<hr
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
<hr>
|
|
180
|
+
${this.articles
|
|
181
|
+
.map(
|
|
182
|
+
(article) => `
|
|
119
183
|
<article>
|
|
120
184
|
<h2>${article.title}</h2>
|
|
121
185
|
<p>${article.getContentShort()}</p>
|
|
122
|
-
</article
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
186
|
+
</article>`
|
|
187
|
+
)
|
|
188
|
+
.join("")}
|
|
189
|
+
</div>
|
|
190
|
+
</body>
|
|
191
|
+
</html>`;
|
|
128
192
|
}
|
|
129
193
|
}
|
package/README.md
CHANGED
|
@@ -1,21 +1,69 @@
|
|
|
1
|
+
# Plainblog
|
|
2
|
+
|
|
3
|
+
Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser. Articles are stored in a file or a database specified via an API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
1
7
|
```
|
|
2
|
-
|
|
8
|
+
npm install @lexho111/plainblog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
```
|
|
14
|
+
import { Blog } from "@lexho111/plainblog";
|
|
15
|
+
|
|
16
|
+
const blog = new Blog();
|
|
5
17
|
blog.setTitle("My Blog");
|
|
6
18
|
blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
blog.startServer(8080);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Now you can start to add articles to your blog via your webbrowser on `http://localhost:8080`.
|
|
24
|
+
|
|
25
|
+
## More Features
|
|
26
|
+
|
|
27
|
+
set an API to fetch data from an external database
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
blog.setAPI("http://localhost:8081/blog")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### run api server with sqlite database
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
import { Blog, storageserver} from "@lexho111/plainblog";
|
|
37
|
+
await storageserver("sqlite", 8081); // you can use a postgres db too
|
|
38
|
+
const blog = new Blog();
|
|
39
|
+
blog.setAPI(storageserver.getAPIURL());
|
|
40
|
+
blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
|
|
10
41
|
|
|
11
|
-
const article2 = new Article(
|
|
12
|
-
"Good Evening!",
|
|
13
|
-
"Good Evening, Ladies and Gentleman!"
|
|
14
|
-
);
|
|
15
|
-
blog.addArticle(article2);
|
|
16
42
|
blog.startServer(8080);
|
|
17
43
|
```
|
|
18
44
|
|
|
45
|
+
### run api server with postgres database
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
docker run -p 5432:5432 --name postgresdb --restart always -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=blog -v db_data:/var/lib/postgresql/data -d postgres:13
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
import { Blog, storageserver} from "@lexho111/plainblog";
|
|
53
|
+
|
|
54
|
+
storageserver.setUsername("username");
|
|
55
|
+
storageserver.setPassword("password");
|
|
56
|
+
storageserver.setHost("localhost");
|
|
57
|
+
await storageserver.start("postgres", 8081);
|
|
58
|
+
const blog = new Blog();
|
|
59
|
+
blog.setAPI(storageserver.getAPIURL());
|
|
60
|
+
blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
|
|
61
|
+
|
|
62
|
+
blog.startServer(8080);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
save data to file
|
|
66
|
+
|
|
19
67
|
```
|
|
20
68
|
# save your blog to 'myblog.json'
|
|
21
69
|
await blog.save("myblog.json");
|
|
@@ -23,3 +71,9 @@ await blog.save("myblog.json");
|
|
|
23
71
|
# load data from 'myblog.json'
|
|
24
72
|
await blog.load("myblog.json");
|
|
25
73
|
```
|
|
74
|
+
|
|
75
|
+
print your blog articles in markdown
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
blog.print()
|
|
79
|
+
```
|
package/api-server.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { Sequelize, DataTypes } from "sequelize";
|
|
3
|
+
|
|
4
|
+
// This is a simple API server that provides blog data.
|
|
5
|
+
// This is the data that our API will serve, similar to a myblog.json file.
|
|
6
|
+
const blogData = {
|
|
7
|
+
title: "My Blog from the API",
|
|
8
|
+
articles: [
|
|
9
|
+
{
|
|
10
|
+
title: "First API Post",
|
|
11
|
+
content: "This article was loaded from our new external API server!",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: "REST is Cool",
|
|
15
|
+
content:
|
|
16
|
+
"Using a REST API allows us to separate our frontend from our backend.",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let username;
|
|
22
|
+
let password;
|
|
23
|
+
let host;
|
|
24
|
+
const dbport = 5432;
|
|
25
|
+
const dbname = process.env.POSTGRES_DB || "blog";
|
|
26
|
+
|
|
27
|
+
export function setUsername(username1) {
|
|
28
|
+
username = username1;
|
|
29
|
+
}
|
|
30
|
+
export function setPassword(password1) {
|
|
31
|
+
password = password1;
|
|
32
|
+
}
|
|
33
|
+
export function setHost(host1) {
|
|
34
|
+
host = host1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let serverPort = 8081; // Default port
|
|
38
|
+
|
|
39
|
+
export async function start(databasetype, port = 8081) {
|
|
40
|
+
let sequelize;
|
|
41
|
+
serverPort = port; // Update the port for getAPIURL
|
|
42
|
+
|
|
43
|
+
if (databasetype === "sqlite") {
|
|
44
|
+
sequelize = new Sequelize({
|
|
45
|
+
dialect: "sqlite",
|
|
46
|
+
storage: "./blog.db",
|
|
47
|
+
logging: false,
|
|
48
|
+
});
|
|
49
|
+
} else if (databasetype === "postgres") {
|
|
50
|
+
if (!username || !password || !host) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"PostgreSQL credentials not set. Please use setUsername(), setPassword(), and setHost() before starting the server."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
sequelize = new Sequelize(
|
|
57
|
+
`postgres://${username}:${password}@${host}:${dbport}/${dbname}`,
|
|
58
|
+
{ logging: false }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Article = sequelize.define(
|
|
63
|
+
"Article",
|
|
64
|
+
{
|
|
65
|
+
title: DataTypes.STRING,
|
|
66
|
+
content: DataTypes.STRING,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// The connectWithRetry logic is now integrated here and properly awaited.
|
|
75
|
+
let retries = 5;
|
|
76
|
+
while (retries) {
|
|
77
|
+
try {
|
|
78
|
+
await sequelize.authenticate();
|
|
79
|
+
console.log("Connection has been established successfully.");
|
|
80
|
+
|
|
81
|
+
// Sync all models and populate data
|
|
82
|
+
await sequelize.sync({ alter: true });
|
|
83
|
+
console.log("All models were synchronized successfully.");
|
|
84
|
+
|
|
85
|
+
if ((await Article.count()) === 0) {
|
|
86
|
+
for (const articleData of blogData.articles) {
|
|
87
|
+
await Article.create(articleData);
|
|
88
|
+
}
|
|
89
|
+
console.log("Initial blog data populated.");
|
|
90
|
+
}
|
|
91
|
+
break; // Success, exit retry loop
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// For sqlite, we don't need to retry, just throw the error.
|
|
94
|
+
if (databasetype === "sqlite") throw err;
|
|
95
|
+
|
|
96
|
+
console.error("Unable to connect to the database:", err.name);
|
|
97
|
+
retries -= 1;
|
|
98
|
+
console.log(`Retries left: ${retries}`);
|
|
99
|
+
if (retries === 0) throw err; // Throw error if max retries reached
|
|
100
|
+
await new Promise((res) => setTimeout(res, 5000));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const server = http.createServer(async (req, res) => {
|
|
105
|
+
// Set CORS headers for all responses
|
|
106
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
107
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
108
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
109
|
+
|
|
110
|
+
// Handle preflight CORS requests
|
|
111
|
+
if (req.method === "OPTIONS") {
|
|
112
|
+
res.writeHead(204);
|
|
113
|
+
res.end();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Health check endpoint
|
|
118
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
119
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
120
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GET all blog data
|
|
125
|
+
if (req.method === "GET" && req.url === "/blog") {
|
|
126
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
127
|
+
const dbArticles = await Article.findAll();
|
|
128
|
+
const responseData = {
|
|
129
|
+
title: blogData.title, // Keep the title from the original constant
|
|
130
|
+
articles: dbArticles,
|
|
131
|
+
};
|
|
132
|
+
res.end(JSON.stringify(responseData));
|
|
133
|
+
}
|
|
134
|
+
// POST a new article
|
|
135
|
+
else if (req.method === "POST" && req.url === "/blog/articles") {
|
|
136
|
+
let body = "";
|
|
137
|
+
req.on("data", (chunk) => (body += chunk.toString()));
|
|
138
|
+
req.on("end", async () => {
|
|
139
|
+
const newArticle = JSON.parse(body);
|
|
140
|
+
await Article.create(newArticle);
|
|
141
|
+
console.log("Added new article:", newArticle);
|
|
142
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
143
|
+
res.end(JSON.stringify(newArticle));
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
147
|
+
res.end(JSON.stringify({ message: "Not Found" }));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
server.listen(port, () => {
|
|
153
|
+
console.log(`API server running at http://localhost:${port}/`);
|
|
154
|
+
resolve(server);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("Failed to initialize database or start server:", error);
|
|
159
|
+
process.exit(1); // Exit the process if there's a critical error
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getAPIURL() {
|
|
164
|
+
return `http://localhost:${serverPort}/blog`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const storageserver = {
|
|
168
|
+
start,
|
|
169
|
+
getAPIURL,
|
|
170
|
+
setUsername,
|
|
171
|
+
setPassword,
|
|
172
|
+
setHost,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export default storageserver;
|
package/blog.db
ADDED
|
Binary file
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import pluginJest from "eslint-plugin-jest";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
// Configuration for all JavaScript files
|
|
8
|
+
files: ["**/*.js"],
|
|
9
|
+
languageOptions: {
|
|
10
|
+
ecmaVersion: 2022, // Supports modern JavaScript features
|
|
11
|
+
sourceType: "module",
|
|
12
|
+
globals: {
|
|
13
|
+
...globals.node, // Defines Node.js global variables (e.g., `process`, `require`)
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
// ESLint's recommended rules for general JavaScript
|
|
17
|
+
rules: {
|
|
18
|
+
...pluginJs.configs.recommended.rules,
|
|
19
|
+
// Add or override general JavaScript rules here.
|
|
20
|
+
// Example: Enforce semicolons at the end of statements
|
|
21
|
+
semi: ["error", "always"],
|
|
22
|
+
// Example: Prefer `const` over `let` where variables are not reassigned
|
|
23
|
+
"prefer-const": "error",
|
|
24
|
+
// Example: Prevent unused variables (can be configured further)
|
|
25
|
+
"no-unused-vars": ["warn", { args: "none" }],
|
|
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
|
+
];
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"files": [
|
|
8
|
-
"index.js",
|
|
9
|
-
"Blog.js",
|
|
10
|
-
"Article.js"
|
|
11
|
-
],
|
|
12
7
|
"scripts": {
|
|
13
|
-
"test": "
|
|
8
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
9
|
+
"lint": "eslint ."
|
|
14
10
|
},
|
|
15
11
|
"keywords": [
|
|
16
12
|
"blog",
|
|
@@ -18,5 +14,17 @@
|
|
|
18
14
|
"server"
|
|
19
15
|
],
|
|
20
16
|
"author": "lexho111",
|
|
21
|
-
"license": "ISC"
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"node-fetch": "^3.3.2",
|
|
20
|
+
"pg": "^8.16.3",
|
|
21
|
+
"pg-hstore": "^2.3.4",
|
|
22
|
+
"sequelize": "^6.37.7",
|
|
23
|
+
"sqlite3": "^5.1.7"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"eslint": "^9.8.0",
|
|
27
|
+
"eslint-plugin-jest": "^28.6.0",
|
|
28
|
+
"jest": "^29.7.0"
|
|
29
|
+
}
|
|
22
30
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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", () => {
|
|
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", () => {
|
|
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
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Blog from "../Blog.js";
|
|
2
|
+
import Article from "../Article.js";
|
|
3
|
+
import { jest } from "@jest/globals";
|
|
4
|
+
|
|
5
|
+
describe("test blog", () => {
|
|
6
|
+
test("is valid html", async () => {
|
|
7
|
+
const myblog = new Blog();
|
|
8
|
+
const html = await myblog.toHTML();
|
|
9
|
+
expect(html).toContain("html>");
|
|
10
|
+
expect(html).toContain("</html>");
|
|
11
|
+
expect(html).toContain("<body");
|
|
12
|
+
expect(html).toContain("</body>");
|
|
13
|
+
expect(html).toContain("<div"); // container
|
|
14
|
+
expect(html).toContain("</div>");
|
|
15
|
+
});
|
|
16
|
+
test("creates Blog with specified title", async () => {
|
|
17
|
+
const titles = ["TestBlog", "Blog1234", "abcdfg", "kiwi"];
|
|
18
|
+
for (const title of titles) {
|
|
19
|
+
const myblog = new Blog();
|
|
20
|
+
myblog.setTitle(title);
|
|
21
|
+
const html = await myblog.toHTML();
|
|
22
|
+
expect(html).toContain(title);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
test("empty blog without any article", async () => {
|
|
26
|
+
const myblog = new Blog();
|
|
27
|
+
const html = await myblog.toHTML();
|
|
28
|
+
expect(html).not.toContain("<article");
|
|
29
|
+
});
|
|
30
|
+
test("add article", async () => {
|
|
31
|
+
const myblog = new Blog();
|
|
32
|
+
const article = new Article("", "");
|
|
33
|
+
myblog.addArticle(article);
|
|
34
|
+
const html = await myblog.toHTML();
|
|
35
|
+
expect(html).toContain("<article");
|
|
36
|
+
expect(myblog.articles.length).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
test("add articles", async () => {
|
|
39
|
+
const myblog = new Blog();
|
|
40
|
+
expect(myblog.articles.length).toBe(0);
|
|
41
|
+
const size = 10;
|
|
42
|
+
for (let i = 1; i <= size; i++) {
|
|
43
|
+
const article = new Article("", "");
|
|
44
|
+
myblog.addArticle(article);
|
|
45
|
+
expect(myblog.articles.length).toBe(i);
|
|
46
|
+
}
|
|
47
|
+
const html = await myblog.toHTML();
|
|
48
|
+
expect(html).toContain("<article");
|
|
49
|
+
expect(myblog.articles.length).toBe(size);
|
|
50
|
+
});
|
|
51
|
+
test("set style", async () => {
|
|
52
|
+
const myblog = new Blog();
|
|
53
|
+
const styles = [
|
|
54
|
+
"body { font-family: Courier; }",
|
|
55
|
+
"body { background-color: black; color: white; }",
|
|
56
|
+
"body { font-size: 1.2em; }",
|
|
57
|
+
];
|
|
58
|
+
for (const style of styles) {
|
|
59
|
+
myblog.setStyle(style);
|
|
60
|
+
const html = await myblog.toHTML();
|
|
61
|
+
expect(html).toContain(style);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test("print method logs title and articles to console", () => {
|
|
65
|
+
// Arrange: Set up the blog with a title and articles
|
|
66
|
+
const myblog = new Blog();
|
|
67
|
+
myblog.setTitle("My Test Blog");
|
|
68
|
+
myblog.addArticle(new Article("Article 1", "Content 1"));
|
|
69
|
+
|
|
70
|
+
// Spy on console.log to capture its output without printing to the test runner
|
|
71
|
+
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
72
|
+
|
|
73
|
+
// Act: Call the method we want to test
|
|
74
|
+
myblog.print();
|
|
75
|
+
|
|
76
|
+
// Assert: Check if console.log was called with the correct strings
|
|
77
|
+
expect(consoleSpy).toHaveBeenCalledWith("# My Test Blog");
|
|
78
|
+
expect(consoleSpy).toHaveBeenCalledWith("## Article 1");
|
|
79
|
+
expect(consoleSpy).toHaveBeenCalledWith("Content 1");
|
|
80
|
+
|
|
81
|
+
// Clean up the spy to restore the original console.log
|
|
82
|
+
consoleSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
test Blog.js
|
|
88
|
+
ok - new Blog()
|
|
89
|
+
ok - set title
|
|
90
|
+
ok - set style
|
|
91
|
+
ok - add article
|
|
92
|
+
- set api
|
|
93
|
+
- fetch/post
|
|
94
|
+
- save/load
|
|
95
|
+
ok - print
|
|
96
|
+
ok - toHTML
|
|
97
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { start as startAPIServer } from "../api-server.js";
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
|
|
4
|
+
describe("API Server Health Check", () => {
|
|
5
|
+
let server;
|
|
6
|
+
const PORT = 8085;
|
|
7
|
+
const apiBaseUrl = `http://localhost:${PORT}`;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
// how about to use in-memory database?
|
|
11
|
+
server = await startAPIServer("sqlite", PORT);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll((done) => {
|
|
15
|
+
server.close(() => {
|
|
16
|
+
done();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("should respond with 200 OK on the /health endpoint", async () => {
|
|
21
|
+
const response = await fetch(`${apiBaseUrl}/health`);
|
|
22
|
+
|
|
23
|
+
// Check for a successful status code
|
|
24
|
+
expect(response.status).toBe(200);
|
|
25
|
+
|
|
26
|
+
const body = await response.json();
|
|
27
|
+
expect(body).toEqual({ status: "ok" });
|
|
28
|
+
});
|
|
29
|
+
});
|