Monorepo for Aesthetic.Computer
aesthetic.computer
1import assert from "node:assert/strict";
2import { createHandler } from "../system/netlify/functions/news-api.mjs";
3
4function respond(statusCode, body, headers = {}) {
5 const isJson = typeof body === "object" && body !== null;
6 return {
7 statusCode,
8 headers: {
9 "Content-Type": isJson ? "application/json" : "text/plain",
10 ...headers,
11 },
12 body: isJson ? JSON.stringify(body) : body,
13 };
14}
15
16function makeCollection(name, initial = []) {
17 const docs = [...initial];
18 let nextId = 1;
19
20 function matches(doc, query) {
21 if (!query || Object.keys(query).length === 0) return true;
22 return Object.entries(query).every(([key, value]) => {
23 if (value && typeof value === "object" && !Array.isArray(value)) {
24 if (Object.hasOwn(value, "$ne")) {
25 return doc[key] !== value.$ne;
26 }
27 if (Object.hasOwn(value, "$in")) {
28 return value.$in.includes(doc[key]);
29 }
30 }
31 return doc[key] === value;
32 });
33 }
34
35 function applySort(items, sortSpec) {
36 if (!sortSpec) return items;
37 const entries = Object.entries(sortSpec);
38 return items.sort((a, b) => {
39 for (const [field, dir] of entries) {
40 if (a[field] === b[field]) continue;
41 return dir < 0 ? (b[field] ?? 0) - (a[field] ?? 0) : (a[field] ?? 0) - (b[field] ?? 0);
42 }
43 return 0;
44 });
45 }
46
47 return {
48 docs,
49 async createIndex() {
50 return null;
51 },
52 async findOne(query) {
53 return docs.find((doc) => matches(doc, query)) || null;
54 },
55 find(query) {
56 let sortSpec = null;
57 let limitValue = null;
58 const api = {
59 sort(spec) {
60 sortSpec = spec;
61 return api;
62 },
63 limit(value) {
64 limitValue = value;
65 return api;
66 },
67 async toArray() {
68 let results = docs.filter((doc) => matches(doc, query));
69 results = applySort(results, sortSpec);
70 if (limitValue !== null) results = results.slice(0, limitValue);
71 return results;
72 },
73 };
74 return api;
75 },
76 async insertOne(doc) {
77 if (name === "news-votes") {
78 const duplicate = docs.find(
79 (existing) => existing.itemType === doc.itemType && existing.itemId === doc.itemId && existing.user === doc.user,
80 );
81 if (duplicate) {
82 const error = new Error("Duplicate key");
83 error.code = 11000;
84 throw error;
85 }
86 }
87 if (!doc._id) {
88 doc._id = `doc-${nextId++}`;
89 }
90 docs.push(doc);
91 return { insertedId: doc._id };
92 },
93 async updateOne(filter, update) {
94 const target = docs.find((doc) => matches(doc, filter));
95 if (!target) return { matchedCount: 0, modifiedCount: 0 };
96 if (update?.$inc) {
97 Object.entries(update.$inc).forEach(([key, value]) => {
98 target[key] = (target[key] ?? 0) + value;
99 });
100 }
101 return { matchedCount: 1, modifiedCount: 1 };
102 },
103 };
104}
105
106function makeDatabase() {
107 const collections = new Map([
108 ["news-posts", makeCollection("news-posts")],
109 ["news-comments", makeCollection("news-comments")],
110 ["news-votes", makeCollection("news-votes")],
111 ]);
112 return {
113 db: {
114 collection(name) {
115 if (!collections.has(name)) collections.set(name, makeCollection(name));
116 return collections.get(name);
117 },
118 },
119 disconnect: async () => null,
120 };
121}
122
123const database = makeDatabase();
124const handler = createHandler({
125 connect: async () => database,
126 respond,
127 authorize: async () => ({ sub: "user-1" }),
128 generateUniqueCode: async () => "abc123",
129});
130
131async function testSubmitRequiresTitle() {
132 const res = await handler({
133 httpMethod: "POST",
134 headers: {},
135 queryStringParameters: { path: "submit" },
136 body: "text=hello",
137 });
138 assert.equal(res.statusCode, 400, "submit should require title");
139}
140
141async function testSubmitCreatesPostAndVote() {
142 const res = await handler({
143 httpMethod: "POST",
144 headers: {},
145 queryStringParameters: { path: "submit" },
146 body: "title=Hello&url=https%3A%2F%2Fexample.com&text=Hi",
147 });
148 const payload = JSON.parse(res.body);
149 assert.equal(res.statusCode, 200, "submit should return ok");
150 assert.equal(payload.code, "nabc123", "submit should return n-prefixed code");
151 const posts = database.db.collection("news-posts").docs;
152 const votes = database.db.collection("news-votes").docs;
153 assert.equal(posts.length, 1, "post should be inserted");
154 assert.equal(votes.length, 1, "vote should be inserted");
155}
156
157async function testCommentIncrementsCount() {
158 const res = await handler({
159 httpMethod: "POST",
160 headers: {},
161 queryStringParameters: { path: "comment" },
162 body: "postCode=nabc123&text=Nice",
163 });
164 assert.equal(res.statusCode, 200, "comment should succeed");
165 const post = database.db.collection("news-posts").docs[0];
166 assert.equal(post.commentCount, 1, "comment should increment commentCount");
167}
168
169async function testDuplicateVoteReturnsDuplicateFlag() {
170 const event = {
171 httpMethod: "POST",
172 headers: {},
173 queryStringParameters: { path: "vote" },
174 body: "itemType=post&itemId=nabc123&dir=1",
175 };
176 const first = await handler(event);
177 assert.equal(first.statusCode, 200, "first vote should succeed");
178 const second = await handler(event);
179 const payload = JSON.parse(second.body);
180 assert.equal(payload.duplicate, true, "duplicate vote should be flagged");
181}
182
183async function testGetPostsReturnsList() {
184 const res = await handler({
185 httpMethod: "GET",
186 headers: {},
187 queryStringParameters: { path: "posts", limit: "10" },
188 });
189 const payload = JSON.parse(res.body);
190 assert.equal(res.statusCode, 200, "GET posts should succeed");
191 assert.equal(payload.posts.length, 1, "GET posts should return inserted posts");
192}
193
194async function run() {
195 await testSubmitRequiresTitle();
196 await testSubmitCreatesPostAndVote();
197 await testCommentIncrementsCount();
198 await testDuplicateVoteReturnsDuplicateFlag();
199 await testGetPostsReturnsList();
200 console.log("✅ news-api tests passed");
201}
202
203run();