Monorepo for Aesthetic.Computer aesthetic.computer
at main 203 lines 6.1 kB view raw
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();