151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { DataRoute } from '../../source/routes/resources.ts';
|
|
import { UnauthorizedError } from '../../source/errors/index.ts';
|
|
import { type BaseBroadcaster } from '../../source/services/broadcaster.ts';
|
|
import { ApplicationRouteStream } from '../../source/services/route-stream.ts';
|
|
import type { Database } from '../../source/services/storage/database.ts';
|
|
import { TestConnection } from '../helpers/test-connection.ts';
|
|
import { HTTP_STATUS_CODE_NOT_ACCEPTED } from '../../source/constants.ts';
|
|
import { createMockAuth } from '../helpers/misc.ts';
|
|
import { AuthSecp256k1 } from '../../source/auth/auth.ts';
|
|
|
|
const createBroadcasterStub = (): BaseBroadcaster => {
|
|
return {
|
|
subscribe: vi.fn(),
|
|
unsubscribe: vi.fn().mockResolvedValue(undefined),
|
|
publish: vi.fn(),
|
|
sendEvent: vi.fn(),
|
|
} as unknown as BaseBroadcaster;
|
|
};
|
|
|
|
describe('DataRoute subscriptions', (): void => {
|
|
it('subscribes to future resource changes until removal', async (): Promise<void> => {
|
|
let resolveRemoved: () => void = () => undefined;
|
|
const removed = new Promise<void>((resolve) => {
|
|
resolveRemoved = resolve;
|
|
});
|
|
const storage = {
|
|
db: {
|
|
transaction: vi.fn(),
|
|
},
|
|
} as unknown as Database;
|
|
const broadcaster = createBroadcasterStub();
|
|
vi.mocked(broadcaster.subscribe).mockReturnValue(removed);
|
|
const connection = new TestConnection(true, false);
|
|
const stream = new ApplicationRouteStream(connection, {
|
|
resourceId: [ 'a', 'b' ],
|
|
}, 'subscribe-1');
|
|
const route = new DataRoute({
|
|
auth: await AuthSecp256k1.create({ database: storage }, { timestampWindowMs: 0 }),
|
|
database: storage,
|
|
broadcaster: broadcaster,
|
|
}, {
|
|
timestampWindowMs: 0,
|
|
});
|
|
|
|
const execution = route.subscribeData(stream);
|
|
|
|
expect(broadcaster.subscribe).toHaveBeenCalledWith(stream, [ 'resource:a', 'resource:b' ]);
|
|
expect(storage.db.transaction).not.toHaveBeenCalled();
|
|
expect(connection.messages).toEqual([]);
|
|
await expect(Promise.race([ execution.then(() => 'settled'), Promise.resolve('pending') ])).resolves.toBe('pending');
|
|
|
|
resolveRemoved();
|
|
await execution;
|
|
});
|
|
|
|
it('unsubscribes a bidirectional connection and acknowledges the request', async (): Promise<void> => {
|
|
const storage = {
|
|
db: {
|
|
transaction: vi.fn(),
|
|
},
|
|
} as unknown as Database;
|
|
const broadcaster = createBroadcasterStub();
|
|
const connection = new TestConnection(true, true);
|
|
const stream = new ApplicationRouteStream(connection, { resourceId: [ 'a' ] }, '/unsubscribe', 'unsubscribe-1');
|
|
const route = new DataRoute({
|
|
auth: await AuthSecp256k1.create({ database: storage }, { timestampWindowMs: 0 }),
|
|
database: storage,
|
|
broadcaster: broadcaster,
|
|
}, {
|
|
timestampWindowMs: 0,
|
|
});
|
|
|
|
await route.unsubscribeData(stream);
|
|
|
|
expect(broadcaster.unsubscribe).toHaveBeenCalledWith(stream, [ 'resource:a' ]);
|
|
expect(connection.messages).toEqual([
|
|
{
|
|
id: 'unsubscribe-1',
|
|
type: 'response',
|
|
statusCode: 200,
|
|
body: {},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('rejects selective unsubscribe on a one-way connection', async (): Promise<void> => {
|
|
const storage = {
|
|
db: {
|
|
transaction: vi.fn(),
|
|
},
|
|
} as unknown as Database;
|
|
const broadcaster = createBroadcasterStub();
|
|
const connection = new TestConnection(true, false);
|
|
const stream = new ApplicationRouteStream(connection, {
|
|
resourceId: [ 'a' ],
|
|
}, '/unsubscribe', 'unsubscribe-1');
|
|
const route = new DataRoute({
|
|
auth: await AuthSecp256k1.create({ database: storage }, { timestampWindowMs: 0 }),
|
|
database: storage,
|
|
broadcaster: broadcaster,
|
|
}, {
|
|
timestampWindowMs: 0,
|
|
});
|
|
|
|
await expect(route.unsubscribeData(stream)).rejects.toMatchObject({ statusCode: HTTP_STATUS_CODE_NOT_ACCEPTED });
|
|
expect(broadcaster.unsubscribe).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('DataRoute resource write auth', (): void => {
|
|
it('rejects an invalid resource signature before writing the batch', async (): Promise<void> => {
|
|
const storage = {
|
|
db: {
|
|
transaction: vi.fn(),
|
|
},
|
|
} as unknown as Database;
|
|
|
|
const broadcaster = createBroadcasterStub();
|
|
const route = new DataRoute({
|
|
auth: await AuthSecp256k1.create({ database: storage }, { timestampWindowMs: 0 }),
|
|
database: storage,
|
|
broadcaster: broadcaster,
|
|
}, {
|
|
timestampWindowMs: 0,
|
|
});
|
|
|
|
await expect(route.writeData({
|
|
connection: new TestConnection(true, true),
|
|
streaming: true,
|
|
bidirectional: true,
|
|
send: vi.fn(),
|
|
body: {
|
|
resources: [
|
|
{
|
|
id: 'resource-a',
|
|
publicKey: 'not-a-public-key',
|
|
timestamp: Date.now(),
|
|
signature: 'not-a-signature',
|
|
value: new Uint8Array([ 1, 2, 3 ]),
|
|
},
|
|
],
|
|
},
|
|
} as unknown as ApplicationRouteStream)).rejects.toBeInstanceOf(UnauthorizedError);
|
|
|
|
expect(storage.db.transaction).not.toHaveBeenCalled();
|
|
expect(broadcaster.publish).not.toHaveBeenCalled();
|
|
});
|
|
});
|