oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1import { test } from 'node:test'
2import assert from 'node:assert/strict'
3
4import { createUploader, checkFile, sha256Hex, refusal, UploadError, MAX_BYTES } from './uploads.mjs'
5
6// fakeFile is enough of a File for this module: a name, a type, a size,
7// and bytes when something wants to hash them.
8function fakeFile (name, type, bytes = new Uint8Array([1, 2, 3])) {
9 return {
10 name,
11 type,
12 size: bytes.length,
13 arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
14 }
15}
16
17// rig wires an uploader over fakes and records what each step was
18// asked, so a test can assert on the ORDER as well as the arguments.
19function rig (over = {}) {
20 const calls = []
21 const states = []
22 const uploader = createUploader({
23 sign: async body => { calls.push(['sign', body]); return { ref: 'abc123', url: '/put/abc123', headers: { 'Content-Type': body.type } } },
24 put: async (url, file, headers) => { calls.push(['put', url, file.name, headers]) },
25 register: async body => { calls.push(['register', body]); return { id: 'a1', ref: 'abc123', name: body.name, width: body.width, height: body.height, alt: body.alt } },
26 applyOp: async record => { calls.push(['op', record]) },
27 measure: async () => ({ width: 40, height: 30 }),
28 hash: async () => 'abc123',
29 onState: (s, d) => states.push([s, d]),
30 ...over
31 })
32 return { uploader, calls, states }
33}
34
35test('the state machine runs choose → sign → put → register → op', async () => {
36 const { uploader, calls, states } = rig()
37 const record = await uploader.upload(fakeFile('chart.png', 'image/png'), { alt: 'A bar chart' })
38
39 assert.deepEqual(calls.map(c => c[0]), ['sign', 'put', 'register', 'op'])
40 assert.deepEqual(states.map(s => s[0]), ['sign', 'put', 'register', 'op', 'done'])
41 assert.deepEqual(calls[0][1], { name: 'chart.png', size: 3, type: 'image/png', hash: 'abc123' })
42 assert.equal(calls[1][1], '/put/abc123')
43 assert.deepEqual(calls[2][1], {
44 ref: 'abc123', name: 'chart.png', width: 40, height: 30, alt: 'A bar chart', decorative: false
45 })
46 // The op is applied with the SERVER's record, never with what the
47 // client thought it uploaded.
48 assert.deepEqual(calls[3][1], record)
49 assert.equal(record.id, 'a1')
50})
51
52test('a file the deck will not take never reaches the network', async () => {
53 const { uploader, calls } = rig()
54 await assert.rejects(() => uploader.upload(fakeFile('notes.pdf', 'application/pdf'), { alt: 'x' }),
55 err => err instanceof UploadError && err.code === 'type')
56
57 const big = fakeFile('huge.png', 'image/png')
58 big.size = MAX_BYTES + 1
59 await assert.rejects(() => uploader.upload(big, { alt: 'x' }),
60 err => err.code === 'toolarge')
61
62 assert.deepEqual(calls, [], 'nothing was signed, put or registered')
63})
64
65test('alt text is required before the bytes move, unless the image is decorative', async () => {
66 const { uploader, calls } = rig()
67 await assert.rejects(() => uploader.upload(fakeFile('x.png', 'image/png'), { alt: ' ' }),
68 err => err.code === 'altfirst')
69 assert.deepEqual(calls, [])
70
71 await uploader.upload(fakeFile('x.png', 'image/png'), { alt: '', decorative: true })
72 assert.deepEqual(calls.map(c => c[0]), ['sign', 'put', 'register', 'op'])
73 assert.equal(calls[2][1].decorative, true)
74 assert.equal(calls[2][1].alt, '')
75})
76
77test('a step that fails stops the ones after it and reports its own code', async () => {
78 for (const [failing, code] of [['sign', 'no_store'], ['put', 'failed'], ['register', 'unsafe_svg']]) {
79 const { uploader, calls, states } = rig({
80 [failing]: async () => { throw new UploadError(code, 'in words') }
81 })
82 await assert.rejects(() => uploader.upload(fakeFile('x.png', 'image/png'), { alt: 'a' }),
83 err => err.code === code && err.serverMessage === 'in words')
84 assert.ok(!calls.some(c => c[0] === 'op'), `${failing}: the op ran anyway`)
85 assert.equal(states.at(-1)[0], 'error')
86 }
87})
88
89test('a failure that is not ours still becomes an UploadError', async () => {
90 const { uploader } = rig({ put: async () => { throw new TypeError('network down') } })
91 await assert.rejects(() => uploader.upload(fakeFile('x.png', 'image/png'), { alt: 'a' }),
92 err => err instanceof UploadError && err.code === 'failed' && err.serverMessage === 'network down')
93})
94
95test('a second pick while one is in flight is refused, not raced', async () => {
96 // The first PUT hangs until the test lets it go; every later one
97 // resolves at once, so the "and then the next one works" half of this
98 // is not itself a hang.
99 const held = []
100 const { uploader, calls } = rig({
101 put: () => (held.length ? Promise.resolve() : new Promise(r => held.push(r)))
102 })
103 const first = uploader.upload(fakeFile('a.png', 'image/png'), { alt: 'a' })
104 await assert.rejects(() => uploader.upload(fakeFile('b.png', 'image/png'), { alt: 'b' }),
105 err => err.code === 'busy')
106 held[0]()
107 await first
108 await uploader.upload(fakeFile('c.png', 'image/png'), { alt: 'c' })
109 assert.deepEqual(calls.filter(c => c[0] === 'op').length, 2, 'b.png never became an op')
110})
111
112test('an unreadable size is zero rather than a failure', async () => {
113 const { uploader, calls } = rig({ measure: async () => ({ width: 0, height: 0 }) })
114 await uploader.upload(fakeFile('x.svg', 'image/svg+xml'), { alt: 'a' })
115 assert.equal(calls[2][1].width, 0)
116 assert.equal(calls[2][1].height, 0)
117})
118
119test('a per-call applyOp wins, so an upload lands on the block it was chosen for', async () => {
120 const { uploader, calls } = rig()
121 const landed = []
122 await uploader.upload(fakeFile('x.png', 'image/png'),
123 { alt: 'a', applyOp: record => landed.push(record.id) })
124 assert.deepEqual(landed, ['a1'])
125 assert.ok(!calls.some(c => c[0] === 'op'), 'the constructor\'s applyOp ran as well')
126})
127
128test('checkFile takes exactly the four types', () => {
129 for (const type of ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']) {
130 assert.doesNotThrow(() => checkFile(fakeFile('x', type)))
131 }
132 for (const type of ['image/gif', 'text/html', 'image/svg', '']) {
133 assert.throws(() => checkFile(fakeFile('x', type)), /type/)
134 }
135 assert.throws(() => checkFile(null), /nofile/)
136})
137
138test('sha256Hex is the hex digest of the bytes', async () => {
139 // sha256("abc"), the vector everybody knows.
140 const file = fakeFile('abc.png', 'image/png', new TextEncoder().encode('abc'))
141 assert.equal(await sha256Hex(file),
142 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
143})
144
145test('refusal reads this app\'s JSON error shape, and survives one that is not', async () => {
146 const json = { status: 503, json: async () => ({ error: 'no_store', message: 'No object store.' }) }
147 const a = await refusal(json)
148 assert.equal(a.code, 'no_store')
149 assert.equal(a.serverMessage, 'No object store.')
150
151 const html = { status: 502, json: async () => { throw new SyntaxError('not JSON') } }
152 const b = await refusal(html)
153 assert.equal(b.code, 'http_502')
154 assert.equal(b.serverMessage, '')
155})
156