# Split pds.test.js Into Module-Specific Test Files > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Split the monolithic `test/pds.test.js` into 7 separate test files in `packages/core/test/`, each corresponding to its source module. **Architecture:** Extract describe blocks from the single test file into module-specific test files. Each new file imports only from its corresponding source module. No source file changes. **Tech Stack:** Vitest, ES modules --- ## File Mapping Reference | New Test File | Describe Blocks | |---------------|-----------------| | `packages/core/test/crypto.test.js` | Base32 Encoding, P-256 Signing, JWT Base64URL, JWK Thumbprint | | `packages/core/test/repo.test.js` | CBOR Encoding, CBOR Decoding, CID Generation, TID Generation, CAR File Builder, MIME Type Sniffing, Blob Ref Detection | | `packages/core/test/auth.test.js` | JWT Creation, JWT Verification | | `packages/core/test/scope.test.js` | Scope Parsing, ScopePermissions, parseScopesForDisplay | | `packages/core/test/oauth.test.js` | Client Metadata | | `packages/core/test/pds.test.js` | Proxy Utilities | | `packages/core/test/mst.test.js` | MST Key Depth | --- ### Task 1: Create test directory and crypto.test.js **Files:** - Create: `packages/core/test/crypto.test.js` **Step 1: Create test directory** Run: `mkdir -p packages/core/test` **Step 2: Create crypto.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { base32Decode, base32Encode, base64UrlDecode, base64UrlEncode, bytesToHex, computeJwkThumbprint, generateKeyPair, hexToBytes, importPrivateKey, sign, } from '@pdsjs/core/crypto'; describe('Base32 Encoding', () => { test('encodes bytes to base32lower', () => { const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); const encoded = base32Encode(bytes); expect(typeof encoded).toBe('string'); expect(encoded).toMatch(/^[a-z2-7]+$/); }); test('base32 encode/decode roundtrip', () => { const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); const encoded = base32Encode(original); const decoded = base32Decode(encoded); expect(decoded).toEqual(original); }); }); describe('P-256 Signing', () => { test('generates key pair with correct sizes', async () => { const kp = await generateKeyPair(); expect(kp.privateKey.length).toBe(32); expect(kp.publicKey.length).toBe(33); // compressed expect(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03).toBe(true); }); test('can sign data with generated key', async () => { const kp = await generateKeyPair(); const key = await importPrivateKey(kp.privateKey); const data = new TextEncoder().encode('test message'); const sig = await sign(key, data); expect(sig.length).toBe(64); // r (32) + s (32) }); test('different messages produce different signatures', async () => { const kp = await generateKeyPair(); const key = await importPrivateKey(kp.privateKey); const sig1 = await sign(key, new TextEncoder().encode('message 1')); const sig2 = await sign(key, new TextEncoder().encode('message 2')); expect(sig1).not.toEqual(sig2); }); test('bytesToHex and hexToBytes roundtrip', () => { const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]); const hex = bytesToHex(original); const back = hexToBytes(hex); expect(hex).toBe('000ff0ffabcd'); expect(back).toEqual(original); }); test('importPrivateKey rejects invalid key lengths', async () => { // Too short await expect(() => importPrivateKey(new Uint8Array(31))).rejects.toThrow( /expected 32 bytes, got 31/, ); // Too long await expect(() => importPrivateKey(new Uint8Array(33))).rejects.toThrow( /expected 32 bytes, got 33/, ); // Empty await expect(() => importPrivateKey(new Uint8Array(0))).rejects.toThrow( /expected 32 bytes, got 0/, ); }); test('importPrivateKey rejects non-Uint8Array input', async () => { // Arrays have .length but aren't Uint8Array // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey([1, 2, 3])).rejects.toThrow( /Invalid private key/, ); // Strings don't work either // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey('not bytes')).rejects.toThrow( /Invalid private key/, ); // null/undefined // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey(null)).rejects.toThrow( /Invalid private key/, ); }); }); describe('JWT Base64URL', () => { test('base64UrlEncode encodes bytes correctly', () => { const input = new TextEncoder().encode('hello world'); const encoded = base64UrlEncode(input); expect(encoded).toBe('aGVsbG8gd29ybGQ'); expect(encoded.includes('+')).toBe(false); expect(encoded.includes('/')).toBe(false); expect(encoded.includes('=')).toBe(false); }); test('base64UrlDecode decodes string correctly', () => { const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); const str = new TextDecoder().decode(decoded); expect(str).toBe('hello world'); }); test('base64url roundtrip', () => { const original = new Uint8Array([0, 1, 2, 255, 254, 253]); const encoded = base64UrlEncode(original); const decoded = base64UrlDecode(encoded); expect(decoded).toEqual(original); }); }); describe('JWK Thumbprint', () => { test('computes deterministic thumbprint for EC key', async () => { // Test vector: known JWK and its expected thumbprint const jwk = { kty: 'EC', crv: 'P-256', x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY', y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ', }; const jkt1 = await computeJwkThumbprint(jwk); const jkt2 = await computeJwkThumbprint(jwk); // Thumbprint must be deterministic expect(jkt1).toBe(jkt2); // Must be base64url-encoded SHA-256 (43 chars) expect(jkt1.length).toBe(43); // Must only contain base64url characters expect(jkt1).toMatch(/^[A-Za-z0-9_-]+$/); }); test('produces different thumbprints for different keys', async () => { const jwk1 = { kty: 'EC', crv: 'P-256', x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY', y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ', }; const jwk2 = { kty: 'EC', crv: 'P-256', x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', }; const jkt1 = await computeJwkThumbprint(jwk1); const jkt2 = await computeJwkThumbprint(jwk2); expect(jkt1).not.toBe(jkt2); }); }); ``` **Step 3: Run tests to verify** Run: `pnpm test packages/core/test/crypto.test.js` Expected: All tests PASS **Step 4: Commit** ```bash git add packages/core/test/crypto.test.js git commit -m "test: extract crypto tests from pds.test.js" ``` --- ### Task 2: Create repo.test.js **Files:** - Create: `packages/core/test/repo.test.js` **Step 1: Create repo.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { buildCarFile, cborDecode, cborEncode, cidToString, createBlobCid, createCid, createTid, findBlobRefs, sniffMimeType, varint, } from '@pdsjs/core/repo'; import { base32Decode, base32Encode } from '@pdsjs/core/crypto'; describe('CBOR Encoding', () => { test('encodes simple map', () => { const encoded = cborEncode({ hello: 'world', num: 42 }); // Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a const expected = new Uint8Array([ 0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a, ]); expect(encoded).toEqual(expected); }); test('encodes null', () => { const encoded = cborEncode(null); expect(encoded).toEqual(new Uint8Array([0xf6])); }); test('encodes booleans', () => { expect(cborEncode(true)).toEqual(new Uint8Array([0xf5])); expect(cborEncode(false)).toEqual(new Uint8Array([0xf4])); }); test('encodes small integers', () => { expect(cborEncode(0)).toEqual(new Uint8Array([0x00])); expect(cborEncode(1)).toEqual(new Uint8Array([0x01])); expect(cborEncode(23)).toEqual(new Uint8Array([0x17])); }); test('encodes integers >= 24', () => { expect(cborEncode(24)).toEqual(new Uint8Array([0x18, 0x18])); expect(cborEncode(255)).toEqual(new Uint8Array([0x18, 0xff])); }); test('encodes negative integers', () => { expect(cborEncode(-1)).toEqual(new Uint8Array([0x20])); expect(cborEncode(-10)).toEqual(new Uint8Array([0x29])); }); test('encodes strings', () => { const encoded = cborEncode('hello'); // 0x65 = text string of length 5 expect(encoded).toEqual( new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), ); }); test('encodes byte strings', () => { const bytes = new Uint8Array([1, 2, 3]); const encoded = cborEncode(bytes); // 0x43 = byte string of length 3 expect(encoded).toEqual(new Uint8Array([0x43, 1, 2, 3])); }); test('encodes arrays', () => { const encoded = cborEncode([1, 2, 3]); // 0x83 = array of length 3 expect(encoded).toEqual(new Uint8Array([0x83, 0x01, 0x02, 0x03])); }); test('sorts map keys deterministically', () => { const encoded1 = cborEncode({ z: 1, a: 2 }); const encoded2 = cborEncode({ a: 2, z: 1 }); expect(encoded1).toEqual(encoded2); // First key should be 'a' (0x61) expect(encoded1[1]).toBe(0x61); }); test('encodes large integers >= 2^31 without overflow', () => { // 2^31 would overflow with bitshift operators (treated as signed 32-bit) const twoTo31 = 2147483648; const encoded = cborEncode(twoTo31); const decoded = cborDecode(encoded); expect(decoded).toBe(twoTo31); // 2^32 - 1 (max unsigned 32-bit) const maxU32 = 4294967295; const encoded2 = cborEncode(maxU32); const decoded2 = cborDecode(encoded2); expect(decoded2).toBe(maxU32); }); test('encodes 2^31 with correct byte format', () => { // 2147483648 = 0x80000000 // CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows) const encoded = cborEncode(2147483648); expect(encoded[0]).toBe(0x1a); // type 0 | info 26 expect(encoded[1]).toBe(0x80); expect(encoded[2]).toBe(0x00); expect(encoded[3]).toBe(0x00); expect(encoded[4]).toBe(0x00); }); }); describe('CBOR Decoding', () => { test('decodes what encode produces (roundtrip)', () => { const original = { hello: 'world', num: 42 }; const encoded = cborEncode(original); const decoded = cborDecode(encoded); expect(decoded).toEqual(original); }); test('decodes null', () => { const encoded = cborEncode(null); const decoded = cborDecode(encoded); expect(decoded).toBe(null); }); test('decodes booleans', () => { expect(cborDecode(cborEncode(true))).toBe(true); expect(cborDecode(cborEncode(false))).toBe(false); }); test('decodes integers', () => { expect(cborDecode(cborEncode(0))).toBe(0); expect(cborDecode(cborEncode(42))).toBe(42); expect(cborDecode(cborEncode(255))).toBe(255); expect(cborDecode(cborEncode(-1))).toBe(-1); expect(cborDecode(cborEncode(-10))).toBe(-10); }); test('decodes strings', () => { expect(cborDecode(cborEncode('hello'))).toBe('hello'); expect(cborDecode(cborEncode(''))).toBe(''); }); test('decodes arrays', () => { expect(cborDecode(cborEncode([1, 2, 3]))).toEqual([1, 2, 3]); expect(cborDecode(cborEncode([]))).toEqual([]); }); test('decodes nested structures', () => { const original = { arr: [1, { nested: true }], str: 'test' }; const decoded = cborDecode(cborEncode(original)); expect(decoded).toEqual(original); }); }); describe('CID Generation', () => { test('createCid uses dag-cbor codec', async () => { const data = cborEncode({ test: 'data' }); const cid = await createCid(data); expect(cid.length).toBe(36); // 2 prefix + 2 multihash header + 32 hash expect(cid[0]).toBe(0x01); // CIDv1 expect(cid[1]).toBe(0x71); // dag-cbor expect(cid[2]).toBe(0x12); // sha-256 expect(cid[3]).toBe(0x20); // 32 bytes }); test('createBlobCid uses raw codec', async () => { const data = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes const cid = await createBlobCid(data); expect(cid.length).toBe(36); expect(cid[0]).toBe(0x01); // CIDv1 expect(cid[1]).toBe(0x55); // raw codec expect(cid[2]).toBe(0x12); // sha-256 expect(cid[3]).toBe(0x20); // 32 bytes }); test('same bytes produce different CIDs with different codecs', async () => { const data = new Uint8Array([1, 2, 3, 4]); const dagCborCid = cidToString(await createCid(data)); const rawCid = cidToString(await createBlobCid(data)); expect(dagCborCid).not.toBe(rawCid); }); test('cidToString returns base32lower with b prefix', async () => { const data = cborEncode({ test: 'data' }); const cid = await createCid(data); const cidStr = cidToString(cid); expect(cidStr[0]).toBe('b'); expect(cidStr).toMatch(/^b[a-z2-7]+$/); }); test('same input produces same CID', async () => { const data1 = cborEncode({ test: 'data' }); const data2 = cborEncode({ test: 'data' }); const cid1 = cidToString(await createCid(data1)); const cid2 = cidToString(await createCid(data2)); expect(cid1).toBe(cid2); }); test('different input produces different CID', async () => { const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))); const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))); expect(cid1).not.toBe(cid2); }); }); describe('TID Generation', () => { test('creates 13-character TIDs', () => { const tid = createTid(); expect(tid.length).toBe(13); }); test('uses valid base32-sort characters', () => { const tid = createTid(); expect(tid).toMatch(/^[234567abcdefghijklmnopqrstuvwxyz]+$/); }); test('generates monotonically increasing TIDs', () => { const tid1 = createTid(); const tid2 = createTid(); const tid3 = createTid(); expect(tid1 < tid2).toBe(true); expect(tid2 < tid3).toBe(true); }); test('generates unique TIDs', () => { const tids = new Set(); for (let i = 0; i < 100; i++) { tids.add(createTid()); } expect(tids.size).toBe(100); }); }); describe('CAR File Builder', () => { test('varint encodes small numbers', () => { expect(varint(0)).toEqual(new Uint8Array([0])); expect(varint(1)).toEqual(new Uint8Array([1])); expect(varint(127)).toEqual(new Uint8Array([127])); }); test('varint encodes multi-byte numbers', () => { // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01] expect(varint(128)).toEqual(new Uint8Array([0x80, 0x01])); // 300 = 0x12c -> [0xac, 0x02] expect(varint(300)).toEqual(new Uint8Array([0xac, 0x02])); }); test('buildCarFile produces valid structure', async () => { const data = cborEncode({ test: 'data' }); const cid = await createCid(data); const cidStr = cidToString(cid); const car = buildCarFile(cidStr, [{ cid: cidStr, data }]); expect(car instanceof Uint8Array).toBe(true); expect(car.length > 0).toBe(true); // First byte should be varint of header length expect(car[0] > 0).toBe(true); }); }); describe('MIME Type Sniffing', () => { test('detects JPEG', () => { const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); expect(sniffMimeType(bytes)).toBe('image/jpeg'); }); test('detects PNG', () => { const bytes = new Uint8Array([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ]); expect(sniffMimeType(bytes)).toBe('image/png'); }); test('detects GIF', () => { const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); expect(sniffMimeType(bytes)).toBe('image/gif'); }); test('detects WebP', () => { const bytes = new Uint8Array([ 0x52, 0x49, 0x46, 0x46, // RIFF 0x00, 0x00, 0x00, 0x00, // size (ignored) 0x57, 0x45, 0x42, 0x50, // WEBP ]); expect(sniffMimeType(bytes)).toBe('image/webp'); }); test('detects MP4', () => { const bytes = new Uint8Array([ 0x00, 0x00, 0x00, 0x18, // size 0x66, 0x74, 0x79, 0x70, // ftyp 0x69, 0x73, 0x6f, 0x6d, // isom brand ]); expect(sniffMimeType(bytes)).toBe('video/mp4'); }); test('detects AVIF', () => { const bytes = new Uint8Array([ 0x00, 0x00, 0x00, 0x1c, // size 0x66, 0x74, 0x79, 0x70, // ftyp 0x61, 0x76, 0x69, 0x66, // avif brand ]); expect(sniffMimeType(bytes)).toBe('image/avif'); }); test('detects HEIC', () => { const bytes = new Uint8Array([ 0x00, 0x00, 0x00, 0x18, // size 0x66, 0x74, 0x79, 0x70, // ftyp 0x68, 0x65, 0x69, 0x63, // heic brand ]); expect(sniffMimeType(bytes)).toBe('image/heic'); }); test('returns null for unknown', () => { const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]); expect(sniffMimeType(bytes)).toBe(null); }); }); describe('Blob Ref Detection', () => { test('finds blob ref in simple object', () => { const record = { $type: 'app.bsky.feed.post', text: 'Hello', embed: { $type: 'app.bsky.embed.images', images: [ { image: { $type: 'blob', ref: { $link: 'bafkreiabc123' }, mimeType: 'image/jpeg', size: 1234, }, alt: 'test image', }, ], }, }; const refs = findBlobRefs(record); expect(refs).toEqual(['bafkreiabc123']); }); test('finds multiple blob refs', () => { const record = { images: [ { image: { $type: 'blob', ref: { $link: 'cid1' }, mimeType: 'image/png', size: 100, }, }, { image: { $type: 'blob', ref: { $link: 'cid2' }, mimeType: 'image/png', size: 200, }, }, ], }; const refs = findBlobRefs(record); expect(refs).toEqual(['cid1', 'cid2']); }); test('returns empty array when no blobs', () => { const record = { text: 'Hello world', count: 42 }; const refs = findBlobRefs(record); expect(refs).toEqual([]); }); test('handles null and primitives', () => { expect(findBlobRefs(null)).toEqual([]); expect(findBlobRefs('string')).toEqual([]); expect(findBlobRefs(42)).toEqual([]); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/repo.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/repo.test.js git commit -m "test: extract repo tests from pds.test.js" ``` --- ### Task 3: Create auth.test.js **Files:** - Create: `packages/core/test/auth.test.js` **Step 1: Create auth.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { createAccessJwt, createRefreshJwt, verifyAccessJwt, verifyRefreshJwt, } from '@pdsjs/core/auth'; import { base64UrlDecode } from '@pdsjs/core/crypto'; describe('JWT Creation', () => { test('createAccessJwt creates valid JWT structure', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); const parts = jwt.split('.'); expect(parts.length).toBe(3); // Decode header const header = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[0])), ); expect(header.typ).toBe('at+jwt'); expect(header.alg).toBe('HS256'); // Decode payload const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); expect(payload.scope).toBe('com.atproto.access'); expect(payload.sub).toBe(did); expect(payload.aud).toBe(did); expect(payload.iat > 0).toBe(true); expect(payload.exp > payload.iat).toBe(true); }); test('createRefreshJwt creates valid JWT with jti', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); const parts = jwt.split('.'); const header = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[0])), ); expect(header.typ).toBe('refresh+jwt'); const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); expect(payload.scope).toBe('com.atproto.refresh'); expect(payload.jti).toBeTruthy(); // has unique token ID }); }); describe('JWT Verification', () => { test('verifyAccessJwt returns payload for valid token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); const payload = await verifyAccessJwt(jwt, secret); expect(payload.sub).toBe(did); expect(payload.scope).toBe('com.atproto.access'); }); test('verifyAccessJwt throws for wrong secret', async () => { const did = 'did:web:test.example'; const jwt = await createAccessJwt(did, 'correct-secret'); await expect(() => verifyAccessJwt(jwt, 'wrong-secret')).rejects.toThrow( /invalid signature/i, ); }); test('verifyAccessJwt throws for expired token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; // Create token that expired 1 second ago const jwt = await createAccessJwt(did, secret, -1); await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow( /expired/i, ); }); test('verifyAccessJwt throws for refresh token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow( /invalid token type/i, ); }); test('verifyRefreshJwt returns payload for valid token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); const payload = await verifyRefreshJwt(jwt, secret); expect(payload.sub).toBe(did); expect(payload.scope).toBe('com.atproto.refresh'); expect(payload.jti).toBeTruthy(); // has token ID }); test('verifyRefreshJwt throws for wrong secret', async () => { const did = 'did:web:test.example'; const jwt = await createRefreshJwt(did, 'correct-secret'); await expect(() => verifyRefreshJwt(jwt, 'wrong-secret')).rejects.toThrow( /invalid signature/i, ); }); test('verifyRefreshJwt throws for expired token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; // Create token that expired 1 second ago const jwt = await createRefreshJwt(did, secret, -1); await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow( /expired/i, ); }); test('verifyRefreshJwt throws for access token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow( /invalid token type/i, ); }); test('verifyAccessJwt throws for malformed JWT', async () => { const secret = 'test-secret-key'; // Not a JWT at all await expect(() => verifyAccessJwt('not-a-jwt', secret)).rejects.toThrow( /Invalid JWT format/i, ); // Only two parts await expect(() => verifyAccessJwt('two.parts', secret)).rejects.toThrow( /Invalid JWT format/i, ); // Four parts await expect(() => verifyAccessJwt('one.two.three.four', secret), ).rejects.toThrow(/Invalid JWT format/i); }); test('verifyRefreshJwt throws for malformed JWT', async () => { const secret = 'test-secret-key'; await expect(() => verifyRefreshJwt('not-a-jwt', secret)).rejects.toThrow( /Invalid JWT format/i, ); await expect(() => verifyRefreshJwt('two.parts', secret)).rejects.toThrow( /Invalid JWT format/i, ); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/auth.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/auth.test.js git commit -m "test: extract auth tests from pds.test.js" ``` --- ### Task 4: Create mst.test.js **Files:** - Create: `packages/core/test/mst.test.js` **Step 1: Create mst.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { getKeyDepth } from '@pdsjs/core/repo'; describe('MST Key Depth', () => { test('returns a non-negative integer', async () => { const depth = await getKeyDepth('app.bsky.feed.post/abc123'); expect(typeof depth).toBe('number'); expect(depth >= 0).toBe(true); }); test('is deterministic for same key', async () => { const key = 'app.bsky.feed.post/test123'; const depth1 = await getKeyDepth(key); const depth2 = await getKeyDepth(key); expect(depth1).toBe(depth2); }); test('different keys can have different depths', async () => { // Generate many keys and check we get some variation const depths = new Set(); for (let i = 0; i < 100; i++) { depths.add(await getKeyDepth(`collection/key${i}`)); } // Should have at least 1 unique depth (realistically more) expect(depths.size >= 1).toBe(true); }); test('handles empty string', async () => { const depth = await getKeyDepth(''); expect(typeof depth).toBe('number'); expect(depth >= 0).toBe(true); }); test('handles unicode strings', async () => { const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉'); expect(typeof depth).toBe('number'); expect(depth >= 0).toBe(true); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/mst.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/mst.test.js git commit -m "test: extract mst tests from pds.test.js" ``` --- ### Task 5: Create scope.test.js **Files:** - Create: `packages/core/test/scope.test.js` **Step 1: Create scope.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { matchesMime, parseBlobScope, parseRepoScope, parseScopesForDisplay, ScopePermissions, } from '@pdsjs/core/scope'; describe('Scope Parsing', () => { describe('parseRepoScope', () => { test('parses repo scope with query parameter action', () => { const result = parseRepoScope('repo:app.bsky.feed.post?action=create'); expect(result).toEqual({ collection: 'app.bsky.feed.post', actions: ['create'], }); }); test('parses repo scope with multiple query parameter actions', () => { const result = parseRepoScope( 'repo:app.bsky.feed.post?action=create&action=update', ); expect(result).toEqual({ collection: 'app.bsky.feed.post', actions: ['create', 'update'], }); }); test('parses repo scope without actions as all actions', () => { const result = parseRepoScope('repo:app.bsky.feed.post'); expect(result).toEqual({ collection: 'app.bsky.feed.post', actions: ['create', 'update', 'delete'], }); }); test('parses wildcard collection with action', () => { const result = parseRepoScope('repo:*?action=create'); expect(result).toEqual({ collection: '*', actions: ['create'], }); }); test('parses query-only format', () => { const result = parseRepoScope( 'repo?collection=app.bsky.feed.post&action=create', ); expect(result).toEqual({ collection: 'app.bsky.feed.post', actions: ['create'], }); }); test('deduplicates repeated actions', () => { const result = parseRepoScope( 'repo:app.bsky.feed.post?action=create&action=create&action=update', ); expect(result).toEqual({ collection: 'app.bsky.feed.post', actions: ['create', 'update'], }); }); test('returns null for non-repo scope', () => { expect(parseRepoScope('atproto')).toBe(null); expect(parseRepoScope('blob:image/*')).toBe(null); expect(parseRepoScope('transition:generic')).toBe(null); }); test('returns null for invalid repo scope', () => { expect(parseRepoScope('repo:')).toBe(null); expect(parseRepoScope('repo?')).toBe(null); }); }); describe('parseBlobScope', () => { test('parses wildcard MIME', () => { const result = parseBlobScope('blob:*/*'); expect(result).toEqual({ accept: ['*/*'] }); }); test('parses type wildcard', () => { const result = parseBlobScope('blob:image/*'); expect(result).toEqual({ accept: ['image/*'] }); }); test('parses specific MIME', () => { const result = parseBlobScope('blob:image/png'); expect(result).toEqual({ accept: ['image/png'] }); }); test('parses multiple MIMEs', () => { const result = parseBlobScope('blob:image/png,image/jpeg'); expect(result).toEqual({ accept: ['image/png', 'image/jpeg'] }); }); test('returns null for non-blob scope', () => { expect(parseBlobScope('atproto')).toBe(null); expect(parseBlobScope('repo:*:create')).toBe(null); }); }); describe('matchesMime', () => { test('wildcard matches everything', () => { expect(matchesMime('*/*', 'image/png')).toBe(true); expect(matchesMime('*/*', 'video/mp4')).toBe(true); }); test('type wildcard matches same type', () => { expect(matchesMime('image/*', 'image/png')).toBe(true); expect(matchesMime('image/*', 'image/jpeg')).toBe(true); expect(matchesMime('image/*', 'video/mp4')).toBe(false); }); test('exact match', () => { expect(matchesMime('image/png', 'image/png')).toBe(true); expect(matchesMime('image/png', 'image/jpeg')).toBe(false); }); test('case insensitive', () => { expect(matchesMime('image/PNG', 'image/png')).toBe(true); expect(matchesMime('IMAGE/*', 'image/png')).toBe(true); }); }); }); describe('ScopePermissions', () => { describe('static scopes', () => { test('atproto grants full access', () => { const perms = new ScopePermissions('atproto'); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('any.collection', 'delete')).toBe(true); expect(perms.allowsBlob('image/png')).toBe(true); expect(perms.allowsBlob('video/mp4')).toBe(true); }); test('transition:generic grants full repo/blob access', () => { const perms = new ScopePermissions('transition:generic'); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('any.collection', 'delete')).toBe(true); expect(perms.allowsBlob('image/png')).toBe(true); }); }); describe('repo scopes', () => { test('wildcard collection allows any collection', () => { const perms = new ScopePermissions('repo:*?action=create'); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false); }); test('specific collection restricts to that collection', () => { const perms = new ScopePermissions( 'repo:app.bsky.feed.post?action=create', ); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(false); }); test('multiple actions', () => { const perms = new ScopePermissions('repo:*?action=create&action=update'); expect(perms.allowsRepo('x', 'create')).toBe(true); expect(perms.allowsRepo('x', 'update')).toBe(true); expect(perms.allowsRepo('x', 'delete')).toBe(false); }); test('multiple scopes combine', () => { const perms = new ScopePermissions( 'repo:app.bsky.feed.post?action=create repo:app.bsky.feed.like?action=delete', ); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.like', 'delete')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false); }); test('allowsRepo with query param format scopes', () => { const perms = new ScopePermissions( 'atproto repo:app.bsky.feed.post?action=create', ); expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(true); // atproto grants full access }); }); describe('blob scopes', () => { test('wildcard allows any MIME', () => { const perms = new ScopePermissions('blob:*/*'); expect(perms.allowsBlob('image/png')).toBe(true); expect(perms.allowsBlob('video/mp4')).toBe(true); }); test('type wildcard restricts to type', () => { const perms = new ScopePermissions('blob:image/*'); expect(perms.allowsBlob('image/png')).toBe(true); expect(perms.allowsBlob('image/jpeg')).toBe(true); expect(perms.allowsBlob('video/mp4')).toBe(false); }); test('specific MIME restricts exactly', () => { const perms = new ScopePermissions('blob:image/png'); expect(perms.allowsBlob('image/png')).toBe(true); expect(perms.allowsBlob('image/jpeg')).toBe(false); }); }); describe('empty/no scope', () => { test('no scope denies everything', () => { const perms = new ScopePermissions(''); expect(perms.allowsRepo('x', 'create')).toBe(false); expect(perms.allowsBlob('image/png')).toBe(false); }); test('undefined scope denies everything', () => { const perms = new ScopePermissions(undefined); expect(perms.allowsRepo('x', 'create')).toBe(false); }); }); describe('assertRepo', () => { test('throws ScopeMissingError when denied', () => { const perms = new ScopePermissions( 'repo:app.bsky.feed.post?action=create', ); expect(() => perms.assertRepo('app.bsky.feed.like', 'create')).toThrow( /Missing required scope/, ); }); test('does not throw when allowed', () => { const perms = new ScopePermissions( 'repo:app.bsky.feed.post?action=create', ); expect(() => perms.assertRepo('app.bsky.feed.post', 'create'), ).not.toThrow(); }); }); describe('assertBlob', () => { test('throws ScopeMissingError when denied', () => { const perms = new ScopePermissions('blob:image/*'); expect(() => perms.assertBlob('video/mp4')).toThrow( /Missing required scope/, ); }); test('does not throw when allowed', () => { const perms = new ScopePermissions('blob:image/*'); expect(() => perms.assertBlob('image/png')).not.toThrow(); }); }); }); describe('parseScopesForDisplay', () => { test('parses identity-only scope', () => { const result = parseScopesForDisplay('atproto'); expect(result.hasAtproto).toBe(true); expect(result.hasTransitionGeneric).toBe(false); expect(result.repoPermissions.size).toBe(0); expect(result.blobPermissions).toEqual([]); }); test('parses granular repo scopes', () => { const result = parseScopesForDisplay( 'atproto repo:app.bsky.feed.post?action=create&action=update', ); expect(result.repoPermissions.size).toBe(1); const postPerms = result.repoPermissions.get('app.bsky.feed.post'); expect(postPerms).toEqual({ create: true, update: true, delete: false, }); }); test('merges multiple scopes for same collection', () => { const result = parseScopesForDisplay( 'atproto repo:app.bsky.feed.post?action=create repo:app.bsky.feed.post?action=delete', ); const postPerms = result.repoPermissions.get('app.bsky.feed.post'); expect(postPerms).toEqual({ create: true, update: false, delete: true, }); }); test('parses blob scopes', () => { const result = parseScopesForDisplay('atproto blob:image/*'); expect(result.blobPermissions).toEqual(['image/*']); }); test('detects transition:generic', () => { const result = parseScopesForDisplay('atproto transition:generic'); expect(result.hasTransitionGeneric).toBe(true); }); test('handles empty scope string', () => { const result = parseScopesForDisplay(''); expect(result.hasAtproto).toBe(false); expect(result.hasTransitionGeneric).toBe(false); expect(result.repoPermissions.size).toBe(0); expect(result.blobPermissions).toEqual([]); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/scope.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/scope.test.js git commit -m "test: extract scope tests from pds.test.js" ``` --- ### Task 6: Create oauth.test.js **Files:** - Create: `packages/core/test/oauth.test.js` **Step 1: Create oauth.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { getLoopbackClientMetadata, isLoopbackClient, validateClientMetadata, } from '@pdsjs/core/oauth'; describe('Client Metadata', () => { test('isLoopbackClient detects localhost', () => { expect(isLoopbackClient('http://localhost:8080')).toBe(true); expect(isLoopbackClient('http://127.0.0.1:3000')).toBe(true); expect(isLoopbackClient('https://example.com')).toBe(false); }); test('getLoopbackClientMetadata returns permissive defaults', () => { const metadata = getLoopbackClientMetadata('http://localhost:8080'); expect(metadata.client_id).toBe('http://localhost:8080'); expect(metadata.grant_types.includes('authorization_code')).toBe(true); expect(metadata.dpop_bound_access_tokens).toBe(true); }); test('validateClientMetadata rejects mismatched client_id', () => { const metadata = { client_id: 'https://other.com/metadata.json', redirect_uris: ['https://example.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; expect(() => validateClientMetadata(metadata, 'https://example.com/metadata.json'), ).toThrow(/client_id mismatch/); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/oauth.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/oauth.test.js git commit -m "test: extract oauth tests from pds.test.js" ``` --- ### Task 7: Create pds.test.js **Files:** - Create: `packages/core/test/pds.test.js` **Step 1: Create pds.test.js** ```javascript import { describe, expect, test } from 'vitest'; import { getKnownServiceUrl, parseAtprotoProxyHeader, } from '@pdsjs/core/pds'; // Internal constant - not exported from pds.js due to Cloudflare Workers limitation const BSKY_APPVIEW_URL = 'https://api.bsky.app'; describe('Proxy Utilities', () => { describe('parseAtprotoProxyHeader', () => { test('parses valid header', () => { const result = parseAtprotoProxyHeader( 'did:web:api.bsky.app#bsky_appview', ); expect(result).toEqual({ did: 'did:web:api.bsky.app', serviceId: 'bsky_appview', }); }); test('parses header with did:plc', () => { const result = parseAtprotoProxyHeader( 'did:plc:z72i7hdynmk6r22z27h6tvur#atproto_labeler', ); expect(result).toEqual({ did: 'did:plc:z72i7hdynmk6r22z27h6tvur', serviceId: 'atproto_labeler', }); }); test('returns null for null/undefined', () => { // @ts-expect-error - Testing runtime handling of null expect(parseAtprotoProxyHeader(null)).toBe(null); // @ts-expect-error - Testing runtime handling of undefined expect(parseAtprotoProxyHeader(undefined)).toBe(null); expect(parseAtprotoProxyHeader('')).toBe(null); }); test('returns null for header without fragment', () => { expect(parseAtprotoProxyHeader('did:web:api.bsky.app')).toBe(null); }); test('returns null for header with only fragment', () => { expect(parseAtprotoProxyHeader('#bsky_appview')).toBe(null); }); test('returns null for header with trailing fragment', () => { expect(parseAtprotoProxyHeader('did:web:api.bsky.app#')).toBe(null); }); }); describe('getKnownServiceUrl', () => { test('returns URL for known Bluesky AppView', () => { const result = getKnownServiceUrl('did:web:api.bsky.app', 'bsky_appview'); expect(result).toBe(BSKY_APPVIEW_URL); }); test('returns null for unknown service DID', () => { const result = getKnownServiceUrl( 'did:web:unknown.service', 'bsky_appview', ); expect(result).toBe(null); }); test('returns null for unknown service ID', () => { const result = getKnownServiceUrl( 'did:web:api.bsky.app', 'unknown_service', ); expect(result).toBe(null); }); test('returns null for both unknown', () => { const result = getKnownServiceUrl('did:web:unknown', 'unknown'); expect(result).toBe(null); }); }); }); ``` **Step 2: Run tests to verify** Run: `pnpm test packages/core/test/pds.test.js` Expected: All tests PASS **Step 3: Commit** ```bash git add packages/core/test/pds.test.js git commit -m "test: extract pds tests from pds.test.js" ``` --- ### Task 8: Run all new tests together **Step 1: Run all core tests** Run: `pnpm test packages/core/test/` Expected: All tests PASS (same count as original file) **Step 2: Verify test count matches original** The original `test/pds.test.js` had tests across all describe blocks. Verify the total test count is the same. --- ### Task 9: Delete original test file and verify **Files:** - Delete: `test/pds.test.js` **Step 1: Delete original file** Run: `rm test/pds.test.js` **Step 2: Run full test suite** Run: `pnpm test` Expected: All tests PASS (e2e.test.js + all new core tests) **Step 3: Commit** ```bash git add -A git commit -m "refactor: remove original pds.test.js after splitting into modules" ``` --- ### Task 10: Final verification **Step 1: Run full test suite one more time** Run: `pnpm test` Expected: All tests PASS **Step 2: Verify no regressions** Check that test count matches expectations and no tests were lost in the split.