101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||
|
|
import { createShade, type Shade } from '@shade/sdk';
|
||
|
|
import { createPrekeyServer, MemoryPrekeyStore, PrekeyServerEvents } from '@shade/server';
|
||
|
|
import { SubtleCryptoProvider } from '@shade/crypto-web';
|
||
|
|
import type { FileEntry } from '../../src/index.js';
|
||
|
|
|
||
|
|
const crypto = new SubtleCryptoProvider();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* End-to-end test of `Shade.files` — the high-level SDK entrypoint.
|
||
|
|
* Verifies that `shade.files.serve(...)` and `shade.files.client(peer)`
|
||
|
|
* compose correctly and share a single channel + bridges per Shade.
|
||
|
|
*/
|
||
|
|
describe('Shade.files namespace — end-to-end via SDK getter', () => {
|
||
|
|
let alice: Shade;
|
||
|
|
let bob: Shade;
|
||
|
|
let prekeyServer: { stop(): void };
|
||
|
|
let aliceServer: { stop(): void };
|
||
|
|
let bobServer: { stop(): void };
|
||
|
|
let stopBobFiles: (() => Promise<void>) | null = null;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
const prekey = createPrekeyServer({
|
||
|
|
crypto,
|
||
|
|
store: new MemoryPrekeyStore(),
|
||
|
|
disableRateLimit: true,
|
||
|
|
events: new PrekeyServerEvents(),
|
||
|
|
});
|
||
|
|
prekeyServer = Bun.serve({ port: 0, fetch: prekey.fetch });
|
||
|
|
const prekeyUrl = `http://localhost:${(prekeyServer as unknown as { port: number }).port}`;
|
||
|
|
|
||
|
|
alice = await createShade({ prekeyServer: prekeyUrl, address: 'alice' });
|
||
|
|
bob = await createShade({ prekeyServer: prekeyUrl, address: 'bob' });
|
||
|
|
|
||
|
|
let aliceUrl = '';
|
||
|
|
let bobUrl = '';
|
||
|
|
alice.configureTransfers({
|
||
|
|
resolveBaseUrl: async (peer) => {
|
||
|
|
if (peer === 'bob') return bobUrl;
|
||
|
|
throw new Error(`unknown peer: ${peer}`);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
bob.configureTransfers({
|
||
|
|
resolveBaseUrl: async (peer) => {
|
||
|
|
if (peer === 'alice') return aliceUrl;
|
||
|
|
throw new Error(`unknown peer: ${peer}`);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const aliceApp = await alice.transferRoute();
|
||
|
|
const bobApp = await bob.transferRoute();
|
||
|
|
aliceServer = Bun.serve({ port: 0, fetch: aliceApp.fetch });
|
||
|
|
bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||
|
|
aliceUrl = `http://localhost:${(aliceServer as unknown as { port: number }).port}`;
|
||
|
|
bobUrl = `http://localhost:${(bobServer as unknown as { port: number }).port}`;
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
if (stopBobFiles !== null) await stopBobFiles();
|
||
|
|
await alice.files.destroy();
|
||
|
|
await bob.files.destroy();
|
||
|
|
await alice.shutdown();
|
||
|
|
await bob.shutdown();
|
||
|
|
aliceServer.stop();
|
||
|
|
bobServer.stop();
|
||
|
|
prekeyServer.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('shade.files getter is memoized', () => {
|
||
|
|
const a = bob.files;
|
||
|
|
const b = bob.files;
|
||
|
|
expect(a).toBe(b);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('serve() + client() round-trip stat through the SDK', async () => {
|
||
|
|
stopBobFiles = await bob.files.serve({
|
||
|
|
stat: async (ctx) => {
|
||
|
|
const e: FileEntry = {
|
||
|
|
name: ctx.path.split('/').filter(Boolean).pop() ?? '',
|
||
|
|
kind: 'file',
|
||
|
|
size: 42,
|
||
|
|
mtime: 1234,
|
||
|
|
metadata: {},
|
||
|
|
};
|
||
|
|
return e;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const fs = await alice.files.client('bob');
|
||
|
|
const result = await fs.stat('/answer.txt');
|
||
|
|
expect(result.name).toBe('answer.txt');
|
||
|
|
expect(result.size).toBe(42);
|
||
|
|
expect(result.mtime).toBe(1234);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('second serve() throws (one handler per Shade)', async () => {
|
||
|
|
await expect(
|
||
|
|
bob.files.serve({ stat: async () => ({ name: 'x', kind: 'file', size: 0, mtime: 0, metadata: {} }) }),
|
||
|
|
).rejects.toThrow(/handler is already registered/);
|
||
|
|
});
|
||
|
|
});
|