256 lines
8.0 KiB
TypeScript
256 lines
8.0 KiB
TypeScript
import 'reflect-metadata';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { Db } from '@mosaicstack/db';
|
|
import type { FederationListResponse } from '@mosaicstack/types';
|
|
import {
|
|
FederationClientError,
|
|
type FederationClientService,
|
|
} from '../federation-client.service.js';
|
|
import { type QuerySourceError, QuerySourceService } from '../query-source.service.js';
|
|
|
|
interface TestRow {
|
|
id: string;
|
|
title: string;
|
|
}
|
|
|
|
interface PeerRow {
|
|
id: string;
|
|
commonName: string;
|
|
endpointUrl: string | null;
|
|
clientKeyPem: string | null;
|
|
state: 'active' | 'pending' | 'suspended' | 'revoked';
|
|
}
|
|
|
|
const LOCAL_ROWS: TestRow[] = [
|
|
{ id: 'local-1', title: 'Local One' },
|
|
{ id: 'local-2', title: 'Local Two' },
|
|
];
|
|
|
|
const PEER_A: PeerRow = {
|
|
id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
|
commonName: 'peer-a',
|
|
endpointUrl: 'https://peer-a.example.com',
|
|
clientKeyPem: 'sealed-key-a',
|
|
state: 'active',
|
|
};
|
|
|
|
const PEER_B: PeerRow = {
|
|
id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
|
commonName: 'peer-b',
|
|
endpointUrl: 'https://peer-b.example.com',
|
|
clientKeyPem: 'sealed-key-b',
|
|
state: 'active',
|
|
};
|
|
|
|
const PEER_LOCALHOST: PeerRow = {
|
|
id: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
|
commonName: 'peer-localhost',
|
|
endpointUrl: 'https://localhost:3001',
|
|
clientKeyPem: 'sealed-key-c',
|
|
state: 'active',
|
|
};
|
|
|
|
function makeDb(activePeers: PeerRow[]): Db {
|
|
const orderBy = vi.fn().mockResolvedValue(activePeers);
|
|
const where = vi.fn().mockReturnValue({ orderBy });
|
|
const from = vi.fn().mockReturnValue({ where });
|
|
const select = vi.fn().mockReturnValue({ from });
|
|
|
|
return {
|
|
select,
|
|
insert: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn(),
|
|
transaction: vi.fn(),
|
|
} as unknown as Db;
|
|
}
|
|
|
|
function makeFederationClient(
|
|
list: (
|
|
peerId: string,
|
|
resource: string,
|
|
request: Record<string, unknown>,
|
|
) => Promise<FederationListResponse<TestRow>>,
|
|
): FederationClientService {
|
|
return {
|
|
list: list as unknown as FederationClientService['list'],
|
|
} as FederationClientService;
|
|
}
|
|
|
|
function makeLocalResponse(rows: TestRow[] = LOCAL_ROWS): Promise<FederationListResponse<TestRow>> {
|
|
return Promise.resolve({ items: rows });
|
|
}
|
|
|
|
describe('QuerySourceService', () => {
|
|
it('routes source="local" to the local executor and tags rows as local', async () => {
|
|
const list = vi.fn(async (): Promise<FederationListResponse<TestRow>> => ({ items: [] }));
|
|
const service = new QuerySourceService(makeDb([PEER_A]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'local',
|
|
resource: 'tasks',
|
|
request: { cursor: 'ignored-for-local-test' },
|
|
local: () => makeLocalResponse(),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
{ id: 'local-1', title: 'Local One', _source: 'local' },
|
|
{ id: 'local-2', title: 'Local Two', _source: 'local' },
|
|
],
|
|
});
|
|
expect(list).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('routes source="federated:<host>" to the matching active peer and tags rows with peer commonName', async () => {
|
|
const list = vi.fn(
|
|
async (): Promise<FederationListResponse<TestRow>> => ({
|
|
items: [{ id: 'remote-1', title: 'Remote One' }],
|
|
}),
|
|
);
|
|
const service = new QuerySourceService(makeDb([PEER_A, PEER_B]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'federated:peer-b.example.com',
|
|
resource: 'tasks',
|
|
request: { status: 'open' },
|
|
local: () => makeLocalResponse(),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [{ id: 'remote-1', title: 'Remote One', _source: 'peer-b' }],
|
|
});
|
|
expect(list).toHaveBeenCalledWith(PEER_B.id, 'tasks', { status: 'open' });
|
|
});
|
|
|
|
it('matches federated hosts by endpoint host including non-default port', async () => {
|
|
const list = vi.fn(
|
|
async (): Promise<FederationListResponse<TestRow>> => ({
|
|
items: [{ id: 'remote-port', title: 'Remote Port' }],
|
|
}),
|
|
);
|
|
const service = new QuerySourceService(makeDb([PEER_LOCALHOST]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'federated:localhost:3001',
|
|
resource: 'tasks',
|
|
request: {},
|
|
local: () => makeLocalResponse(),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [{ id: 'remote-port', title: 'Remote Port', _source: 'peer-localhost' }],
|
|
});
|
|
expect(list).toHaveBeenCalledWith(PEER_LOCALHOST.id, 'tasks', {});
|
|
});
|
|
|
|
it('fans out source="all" to local plus every active outbound peer in parallel and merges tagged rows', async () => {
|
|
const callOrder: string[] = [];
|
|
const list = vi.fn(async (peerId: string): Promise<FederationListResponse<TestRow>> => {
|
|
callOrder.push(`remote-start:${peerId}`);
|
|
await Promise.resolve();
|
|
return {
|
|
items: [{ id: `remote-${peerId.slice(0, 1)}`, title: `Remote ${peerId.slice(0, 1)}` }],
|
|
};
|
|
});
|
|
const service = new QuerySourceService(makeDb([PEER_A, PEER_B]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'all',
|
|
resource: 'tasks',
|
|
request: { limit: 25 },
|
|
local: async () => {
|
|
callOrder.push('local-start');
|
|
await Promise.resolve();
|
|
return { items: [{ id: 'local-1', title: 'Local One' }] };
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
{ id: 'local-1', title: 'Local One', _source: 'local' },
|
|
{ id: 'remote-a', title: 'Remote a', _source: 'peer-a' },
|
|
{ id: 'remote-b', title: 'Remote b', _source: 'peer-b' },
|
|
],
|
|
});
|
|
expect(list).toHaveBeenCalledTimes(2);
|
|
expect(callOrder).toEqual([
|
|
'local-start',
|
|
`remote-start:${PEER_A.id}`,
|
|
`remote-start:${PEER_B.id}`,
|
|
]);
|
|
});
|
|
|
|
it('marks source="all" as partial and truncated when any subquery returns a cursor', async () => {
|
|
const list = vi.fn(
|
|
async (): Promise<FederationListResponse<TestRow>> => ({
|
|
items: [{ id: 'remote-a', title: 'Remote A' }],
|
|
nextCursor: 'remote-next',
|
|
}),
|
|
);
|
|
const service = new QuerySourceService(makeDb([PEER_A]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'all',
|
|
resource: 'tasks',
|
|
request: {},
|
|
local: () => makeLocalResponse([{ id: 'local-1', title: 'Local One' }]),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
{ id: 'local-1', title: 'Local One', _source: 'local' },
|
|
{ id: 'remote-a', title: 'Remote A', _source: 'peer-a' },
|
|
],
|
|
_partial: true,
|
|
_truncated: true,
|
|
});
|
|
});
|
|
|
|
it('returns _partial=true for source="all" when one peer fails without dropping successful sources', async () => {
|
|
const list = vi.fn(async (peerId: string): Promise<FederationListResponse<TestRow>> => {
|
|
if (peerId === PEER_B.id) {
|
|
throw new FederationClientError({
|
|
code: 'NETWORK',
|
|
message: 'peer unavailable',
|
|
peerId,
|
|
});
|
|
}
|
|
return { items: [{ id: 'remote-a', title: 'Remote A' }] };
|
|
});
|
|
const service = new QuerySourceService(makeDb([PEER_A, PEER_B]), makeFederationClient(list));
|
|
|
|
const result = await service.list<TestRow>({
|
|
source: 'all',
|
|
resource: 'tasks',
|
|
request: {},
|
|
local: () => makeLocalResponse([{ id: 'local-1', title: 'Local One' }]),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
{ id: 'local-1', title: 'Local One', _source: 'local' },
|
|
{ id: 'remote-a', title: 'Remote A', _source: 'peer-a' },
|
|
],
|
|
_partial: true,
|
|
});
|
|
});
|
|
|
|
it('throws QuerySourceError when a federated host does not match an active outbound peer', async () => {
|
|
const list = vi.fn(async (): Promise<FederationListResponse<TestRow>> => ({ items: [] }));
|
|
const service = new QuerySourceService(makeDb([PEER_A]), makeFederationClient(list));
|
|
|
|
await expect(
|
|
service.list<TestRow>({
|
|
source: 'federated:missing.example.com',
|
|
resource: 'tasks',
|
|
request: {},
|
|
local: () => makeLocalResponse(),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
name: 'QuerySourceError',
|
|
code: 'PEER_NOT_FOUND',
|
|
} satisfies Partial<QuerySourceError>);
|
|
});
|
|
});
|