1import { readFileSync } from 'node:fs'; 2import { describe, expect, it } from 'vitest'; 3import { bucketNames, kvTitle, migrate, parseList, provision } from './resources.mjs'; 4 5const production = () => JSON.parse(readFileSync(new URL('../../worker/wrangler.json', import.meta.url), 'utf8')); 6 7// A wrangler that knows `account`'s databases and namespaces, and records 8// every call. 9function fakeWrangler(account) { 10 const calls = []; 11 const wrangler = (args) => { 12 calls.push(args.join(' ')); 13 const [a, b, c, d] = args; 14 if (a === 'd1' && b === 'list') return `⛅️ wrangler\n${JSON.stringify(account.d1, null, 2)}\n`; 15 if (a === 'd1' && b === 'create') return void account.d1.push({ name: c, uuid: `uuid-${c}` }); 16 if (a === 'kv' && c === 'list') return JSON.stringify(account.kv); 17 if (a === 'kv' && c === 'create') return void account.kv.push({ title: d, id: `id-${d}` }); 18 if (a === 'r2' && c === 'list') { 19 return `Listing buckets...\n${(account.r2 ?? []).map((n) => `name: ${n}\ncreation_date: 2026-09-25T00:00:00.000Z`).join('\n\n')}\n`; 20 } 21 if (a === 'r2' && c === 'create') return void (account.r2 ??= []).push(d); 22 return ''; 23 }; 24 return { wrangler, calls }; 25} 26 27describe('provision: the deploy finds or creates its storage, by name', () => { 28 it('writes the ids of what the account already has, creating nothing', () => { 29 const { wrangler, calls } = fakeWrangler({ 30 d1: [{ name: 'other', uuid: 'u0' }, { name: 'jevstrudel', uuid: 'u1' }], 31 kv: [ 32 { title: 'jevstrudel-cache', id: 'k1' }, 33 { title: 'jevstrudel-oauth-kv', id: 'k2' }, 34 ], 35 r2: ['other', 'jevstrudel-covers'], 36 }); 37 const config = provision(production(), wrangler, () => {}); 38 expect(config.d1_databases.find((d) => d.binding === 'DB').database_id).toBe('u1'); 39 expect(Object.fromEntries(config.kv_namespaces.map((ns) => [ns.binding, ns.id]))).toEqual({ CACHE: 'k1', OAUTH_KV: 'k2' }); 40 expect(calls.some((c) => c.includes('create'))).toBe(false); 41 }); 42 43 it('creates what is missing, once, and binds the created ids', () => { 44 const account = { d1: [], kv: [] }; 45 const { wrangler, calls } = fakeWrangler(account); 46 const config = provision(production(), wrangler, () => {}); 47 expect(config.d1_databases[0].database_id).toBe('uuid-jevstrudel'); 48 expect(calls.filter((c) => c.startsWith('d1 create'))).toEqual(['d1 create jevstrudel --update-config=false']); 49 for (const ns of config.kv_namespaces ?? []) { 50 expect(ns.id).toBe(`id-${kvTitle('jevstrudel', ns.binding)}`); 51 } 52 // a second deploy finds them all 53 const again = fakeWrangler(account); 54 provision(production(), again.wrangler, () => {}); 55 expect(again.calls.some((c) => c.includes('create'))).toBe(false); 56 }); 57 58 it('leaves a binding that already names its resource alone', () => { 59 const config = production(); 60 config.d1_databases[0].database_id = 'pinned'; 61 const { wrangler, calls } = fakeWrangler({ d1: [], kv: [] }); 62 provision({ ...config, kv_namespaces: [], r2_buckets: [] }, wrangler, () => {}); 63 expect(config.d1_databases[0].database_id).toBe('pinned'); 64 expect(calls).toEqual([]); 65 }); 66 67 it('makes an R2 bucket the account lacks, by the name the binding gives, once', () => { 68 const account = { d1: [{ name: 'jevstrudel', uuid: 'u1' }], kv: [{ title: 'jevstrudel-cache', id: 'k1' }], r2: [] }; 69 const { wrangler, calls } = fakeWrangler(account); 70 const config = provision(production(), wrangler, () => {}); 71 expect(config.r2_buckets).toEqual([{ binding: 'COVERS', bucket_name: 'jevstrudel-covers' }]); 72 expect(calls.filter((c) => c.startsWith('r2 bucket create'))).toEqual([ 73 'r2 bucket create jevstrudel-covers --update-config=false', 74 ]); 75 const again = fakeWrangler(account); 76 provision(production(), again.wrangler, () => {}); 77 expect(again.calls.some((c) => c.includes('create'))).toBe(false); 78 }); 79 80 it("reads wrangler's bucket list, which has no JSON", () => { 81 const listed = 'Listing buckets...\nname: a-b\ncreation_date: x\n\nname: c\ncreation_date: y\n'; 82 expect(bucketNames(listed)).toEqual(['a-b', 'c']); 83 expect(bucketNames('Listing buckets...\n')).toEqual([]); 84 }); 85 86 it("titles a KV namespace as wrangler's own provisioning does", () => { 87 expect(kvTitle('jevstrudel', 'CACHE')).toBe('jevstrudel-cache'); 88 expect(kvTitle('jevstrudel', 'SESSION_KEYS')).toBe('jevstrudel-session-keys'); 89 }); 90 91 it('reads a list past a banner, and refuses output with none', () => { 92 expect(parseList('⛅️ wrangler 4\n[WARNING] proxy\n[\n {"a": 1}\n]', 'x')).toEqual([{ a: 1 }]); 93 expect(parseList('[]', 'x')).toEqual([]); 94 expect(() => parseList('[WARNING] no list', 'x')).toThrow(/no JSON list/); 95 }); 96}); 97 98describe('migrate: remote migrations, for every database the Worker binds', () => { 99 it('applies each database’s migrations to production', () => { 100 const { wrangler, calls } = fakeWrangler({ d1: [], kv: [] }); 101 migrate(production(), wrangler); 102 expect(calls).toEqual(['d1 migrations apply jevstrudel --remote --env=']); 103 }); 104});