api.test.js (6413B)
1 import assert from 'node:assert'; 2 import { after, before, describe, test } from 'node:test'; 3 import { createApp } from '../src/app.js'; 4 import { getDb, initDb } from '../src/db.js'; 5 const ALLOWED_ORIGIN = 'http://localhost:41737'; 6 7 let server; 8 let baseUrl; 9 10 function api(path, { method = 'GET', headers = {}, body } = {}) { 11 return fetch(`${baseUrl}${path}`, { 12 method, 13 headers: { 14 'Content-Type': 'application/json', 15 ...headers, 16 }, 17 body: body === undefined ? undefined : JSON.stringify(body), 18 }); 19 } 20 21 async function readJson(res) { 22 const text = await res.text(); 23 return text ? JSON.parse(text) : null; 24 } 25 26 async function createDocument(overrides = {}) { 27 const res = await api('/api/documents', { 28 method: 'POST', 29 headers: { Origin: ALLOWED_ORIGIN }, 30 body: { 31 title: 'Test doc', 32 mode: 'markdown', 33 content: '# Hello', 34 expiresIn: '7d', 35 ...overrides, 36 }, 37 }); 38 assert.equal(res.status, 201); 39 return readJson(res); 40 } 41 42 before(() => { 43 initDb(':memory:'); 44 const app = createApp({ 45 shareAllowedOrigins: ALLOWED_ORIGIN, 46 }); 47 server = app.listen(0); 48 const { port } = server.address(); 49 baseUrl = `http://127.0.0.1:${port}`; 50 }); 51 52 after(() => { 53 server.close(); 54 }); 55 56 describe('Snow Editor API', () => { 57 test('GET /api/health returns ok with db check', async () => { 58 const res = await api('/api/health'); 59 const data = await readJson(res); 60 61 assert.equal(res.status, 200); 62 assert.equal(data.ok, true); 63 assert.equal(data.db, 'ok'); 64 assert.equal(typeof data.uptime, 'number'); 65 assert.equal(data.version, '0.0.1'); 66 }); 67 68 test('POST /documents without Origin is rejected', async () => { 69 const res = await api('/api/documents', { 70 method: 'POST', 71 body: { 72 title: 'Blocked', 73 mode: 'markdown', 74 content: 'nope', 75 expiresIn: '7d', 76 }, 77 }); 78 const data = await readJson(res); 79 80 assert.equal(res.status, 403); 81 assert.equal(data.error, 'ORIGIN_NOT_ALLOWED'); 82 }); 83 84 test('POST /documents with allowed Origin succeeds', async () => { 85 const data = await createDocument({ title: 'Allowed' }); 86 assert.ok(data.viewToken); 87 assert.ok(data.editToken); 88 }); 89 90 test('GET /view/:token returns document', async () => { 91 const doc = await createDocument({ content: '# View me' }); 92 const res = await api(`/api/documents/view/${doc.viewToken}`); 93 const data = await readJson(res); 94 95 assert.equal(res.status, 200); 96 assert.equal(data.content, '# View me'); 97 }); 98 99 test('POST lock acquires edit lock', async () => { 100 const doc = await createDocument(); 101 const res = await api(`/api/documents/edit/${doc.editToken}/lock`, { 102 method: 'POST', 103 body: { clientId: 'client-a' }, 104 }); 105 const data = await readJson(res); 106 107 assert.equal(res.status, 200); 108 assert.equal(data.locked, true); 109 assert.ok(data.lockToken); 110 }); 111 112 test('second clientId on lock returns 423', async () => { 113 const doc = await createDocument(); 114 await api(`/api/documents/edit/${doc.editToken}/lock`, { 115 method: 'POST', 116 body: { clientId: 'client-a' }, 117 }); 118 119 const res = await api(`/api/documents/edit/${doc.editToken}/lock`, { 120 method: 'POST', 121 body: { clientId: 'client-b' }, 122 }); 123 const data = await readJson(res); 124 125 assert.equal(res.status, 423); 126 assert.equal(data.error, 'DOCUMENT_LOCKED'); 127 }); 128 129 test('PUT without lock returns 403', async () => { 130 const doc = await createDocument(); 131 const res = await api(`/api/documents/edit/${doc.editToken}`, { 132 method: 'PUT', 133 body: { 134 clientId: 'ghost', 135 lockToken: 'missing', 136 title: 'Nope', 137 mode: 'markdown', 138 content: 'fail', 139 }, 140 }); 141 const data = await readJson(res); 142 143 assert.equal(res.status, 403); 144 assert.equal(data.error, 'LOCK_REQUIRED'); 145 }); 146 147 test('expired document returns 410', async () => { 148 const doc = await createDocument(); 149 const past = new Date(Date.now() - 60_000).toISOString(); 150 getDb() 151 .prepare('UPDATE documents SET expires_at = ? WHERE edit_token = ?') 152 .run(past, doc.editToken); 153 154 const res = await api(`/api/documents/edit/${doc.editToken}`); 155 const data = await readJson(res); 156 157 assert.equal(res.status, 410); 158 assert.equal(data.error, 'EXPIRED'); 159 }); 160 161 test('body larger than 1 MB returns 413', async () => { 162 const doc = await createDocument(); 163 const lockRes = await api(`/api/documents/edit/${doc.editToken}/lock`, { 164 method: 'POST', 165 body: { clientId: 'big-body' }, 166 }); 167 const lock = await readJson(lockRes); 168 169 const huge = 'x'.repeat(1024 * 1024 + 1); 170 const res = await api(`/api/documents/edit/${doc.editToken}`, { 171 method: 'PUT', 172 body: { 173 clientId: 'big-body', 174 lockToken: lock.lockToken, 175 title: 'Huge', 176 mode: 'markdown', 177 content: huge, 178 }, 179 }); 180 const data = await readJson(res); 181 182 assert.equal(res.status, 413); 183 assert.equal(data.error, 'CONTENT_TOO_LARGE'); 184 }); 185 186 test('version list and restore require active lock', async () => { 187 const doc = await createDocument({ content: 'v1' }); 188 const lockRes = await api(`/api/documents/edit/${doc.editToken}/lock`, { 189 method: 'POST', 190 body: { clientId: 'version-client' }, 191 }); 192 const lock = await readJson(lockRes); 193 194 await api(`/api/documents/edit/${doc.editToken}`, { 195 method: 'PUT', 196 body: { 197 clientId: 'version-client', 198 lockToken: lock.lockToken, 199 title: doc.title, 200 mode: 'markdown', 201 content: 'v2', 202 }, 203 }); 204 205 const versionsRes = await api( 206 `/api/documents/edit/${doc.editToken}/versions?clientId=version-client&lockToken=${lock.lockToken}`, 207 ); 208 const versionsData = await readJson(versionsRes); 209 210 assert.equal(versionsRes.status, 200); 211 assert.ok(versionsData.versions.length >= 1); 212 213 const versionId = versionsData.versions[0].id; 214 const restoreRes = await api( 215 `/api/documents/edit/${doc.editToken}/versions/${versionId}/restore`, 216 { 217 method: 'POST', 218 body: { 219 clientId: 'version-client', 220 lockToken: lock.lockToken, 221 }, 222 }, 223 ); 224 const restored = await readJson(restoreRes); 225 226 assert.equal(restoreRes.status, 200); 227 assert.equal(restored.content, 'v1'); 228 }); 229 });