import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { BrowserProductClient, requireValue } from '../src/transport.ts'; const fixtures = JSON.parse( await readFile(new URL('./fixtures/contract.json', import.meta.url), 'utf8') ); test('transport derives routes, credentials, and encoded queries from the contract', async () => { const calls = []; const client = new BrowserProductClient( async (input, init) => { calls.push({ input: String(input), init }); return jsonResponse(fixtures.browse_response); }, 'https://nuthatch.test/' ); const response = requireValue( await client.browse({ root: 'shared', prefix: '문서/a b', q: 'needle', continuation: 'next/token' }) ); assert.deepEqual(response, fixtures.browse_response); assert.equal(calls.length, 1); assert.equal(calls[0].init.credentials, 'same-origin'); assert.equal(calls[0].init.method, 'GET'); const url = new URL(calls[0].input); assert.equal(url.pathname, '/api/browse'); assert.equal(url.searchParams.get('root'), 'shared'); assert.equal(url.searchParams.get('prefix'), '문서/a b'); assert.equal(url.searchParams.get('q'), 'needle'); assert.equal(url.searchParams.get('continuation'), 'next/token'); }); test('binary upload preserves the Blob and required generated headers', async () => { const calls = []; const client = new BrowserProductClient(async (input, init) => { calls.push({ input: String(input), init }); return jsonResponse(fixtures.upload_response); }); const body = new Blob(['payload']); const response = requireValue( await client.upload({ root: 'personal', path: 'folder/문서.txt', operationId: 'operation-1', declaredSize: body.size, contentSha256: 'a'.repeat(64), csrfToken: 'b'.repeat(43), body }) ); assert.deepEqual(response, fixtures.upload_response); assert.equal(calls.length, 1); assert.equal(calls[0].init.body, body); assert.equal(calls[0].init.headers.get('Content-Type'), 'application/octet-stream'); assert.equal(calls[0].init.headers.get('X-Declared-Size'), '7'); assert.equal(calls[0].init.headers.get('X-Content-SHA256'), 'a'.repeat(64)); assert.equal(calls[0].init.headers.get('X-CSRF-Token'), 'b'.repeat(43)); const url = new URL(calls[0].input, 'https://nuthatch.test'); assert.equal(url.pathname, '/api/upload'); assert.equal(url.searchParams.get('operation_id'), 'operation-1'); assert.equal(url.searchParams.get('path'), 'folder/문서.txt'); }); test('download exposes its stream and declared response headers', async () => { const client = new BrowserProductClient(async () => new Response(new Uint8Array([1, 2, 3]), { headers: { 'Content-Disposition': 'attachment; filename="file.bin"', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'none'", 'Content-Length': '3' } }) ); const download = requireValue( await client.download({ root: 'shared', path: 'file.bin' }) ); assert.deepEqual( new Uint8Array(await new Response(download.body).arrayBuffer()), new Uint8Array([1, 2, 3]) ); assert.equal(download.contentDisposition, 'attachment; filename="file.bin"'); assert.equal(download.xContentTypeOptions, 'nosniff'); assert.equal(download.contentSecurityPolicy, "default-src 'none'"); assert.equal(download.contentLength, 3); }); test('shared fixtures decode through every JSON transport method', async () => { const responses = [ fixtures.session_response, fixtures.delete_response, fixtures.delete_plan_response, fixtures.delete_batch_response ]; const client = new BrowserProductClient(async () => jsonResponse(responses.shift()) ); assert.deepEqual(requireValue(await client.session({})), fixtures.session_response); assert.deepEqual( requireValue(await client.delete({ body: fixtures.delete_request })), fixtures.delete_response ); assert.deepEqual( requireValue( await client.deletePlan({ root: 'shared', path: 'folder/문서.txt', continuation: 'next-page' }) ), fixtures.delete_plan_response ); assert.deepEqual( requireValue( await client.deleteBatch({ body: fixtures.delete_batch_request }) ), fixtures.delete_batch_response ); }); test('declared, undeclared, and network failures remain distinct without retries', async () => { const declaredClient = new BrowserProductClient(async () => jsonResponse( { error: 'csrf_validation_failed', message: 'CSRF validation failed', details: null }, 403 ) ); const declared = await declaredClient.delete({ body: fixtures.delete_request }); assert.equal(declared.ok, false); assert.equal(declared.failure.kind, 'http'); assert.equal(declared.failure.error.error, 'csrf_validation_failed'); const undeclaredClient = new BrowserProductClient(async () => jsonResponse( { error: 'unknown_error', message: 'unknown', details: null }, 500 ) ); const undeclared = await undeclaredClient.session({}); assert.equal(undeclared.ok, false); assert.equal(undeclared.failure.kind, 'unexpected-http'); let attempts = 0; const networkClient = new BrowserProductClient(async () => { attempts += 1; throw new Error('offline'); }); const network = await networkClient.browse({}); assert.equal(network.ok, false); assert.equal(network.failure.kind, 'network'); assert.equal(attempts, 1); }); test('checked generated artifact contains no transport policy', async () => { const generated = await readFile( new URL('../src/generated/contract.ts', import.meta.url), 'utf8' ); assert.doesNotMatch(generated, /\bfetch\s*\(/); assert.doesNotMatch(generated, /\bcredentials\b/); assert.doesNotMatch(generated, /\bretry\b/i); }); function jsonResponse(value, status = 200) { return new Response(JSON.stringify(value), { status, headers: { 'Content-Type': 'application/json' } }); }