mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-09-30 20:33:18 +00:00
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
|
||
|
|
import { runSync, type SyncDeps } from '../src/sync.js';
|
||
|
|
import type { DirectoryReader, CognitoGroupTarget, SyncStateWriter } from '../src/clients.js';
|
||
|
|
|
||
|
|
function fakes(
|
||
|
|
directoryMembers: Record<string, string[]>,
|
||
|
|
cognitoMembers: Record<string, string[]>,
|
||
|
|
) {
|
||
|
|
const added: string[] = [];
|
||
|
|
const removed: string[] = [];
|
||
|
|
const writes: number[] = [];
|
||
|
|
|
||
|
|
const directory: DirectoryReader = {
|
||
|
|
listGroupMembers: vi.fn(async (email: string) => directoryMembers[email] ?? []),
|
||
|
|
};
|
||
|
|
const cognito: CognitoGroupTarget = {
|
||
|
|
ensureGroup: vi.fn(async () => {}),
|
||
|
|
listMembers: vi.fn(async (g: string) => cognitoMembers[g] ?? []),
|
||
|
|
addMember: vi.fn(async (g: string, e: string) => {
|
||
|
|
added.push(`${g}:${e}`);
|
||
|
|
}),
|
||
|
|
removeMember: vi.fn(async (g: string, e: string) => {
|
||
|
|
removed.push(`${g}:${e}`);
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
const syncState: SyncStateWriter = {
|
||
|
|
writeLastSuccessfulSync: vi.fn(async (ms: number) => {
|
||
|
|
writes.push(ms);
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
const deps: SyncDeps = {
|
||
|
|
directory,
|
||
|
|
cognito,
|
||
|
|
syncState,
|
||
|
|
domain: 'seahavenind.com',
|
||
|
|
now: () => 1000,
|
||
|
|
};
|
||
|
|
return { deps, added, removed, writes, directory, cognito };
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('runSync', () => {
|
||
|
|
it('reconciles each group and writes the freshness marker once, on full success', async () => {
|
||
|
|
const { deps, added, removed, writes } = fakes(
|
||
|
|
{ 'sh-mcp-finance@seahavenind.com': ['adam@seahavenind.com', 'new@seahavenind.com'] },
|
||
|
|
{ 'sh-mcp-finance': ['adam@seahavenind.com', 'gone@seahavenind.com'] },
|
||
|
|
);
|
||
|
|
await runSync(deps);
|
||
|
|
expect(added).toContain('sh-mcp-finance:new@seahavenind.com');
|
||
|
|
expect(removed).toContain('sh-mcp-finance:gone@seahavenind.com');
|
||
|
|
expect(writes).toEqual([1000]); // marker written exactly once
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ensures every managed group exists', async () => {
|
||
|
|
const { deps, cognito } = fakes({}, {});
|
||
|
|
await runSync(deps);
|
||
|
|
expect(cognito.ensureGroup).toHaveBeenCalledTimes(4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('FAIL CLOSED: a Directory error aborts and the marker is NOT written', async () => {
|
||
|
|
const { deps, writes, directory } = fakes({}, {});
|
||
|
|
(directory.listGroupMembers as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
|
||
|
|
new Error('directory 503'),
|
||
|
|
);
|
||
|
|
await expect(runSync(deps)).rejects.toThrow('directory 503');
|
||
|
|
expect(writes).toEqual([]); // stale marker preserved → pre-token fails closed
|
||
|
|
});
|
||
|
|
|
||
|
|
it('FAIL CLOSED: a Cognito reconcile error aborts before the marker write', async () => {
|
||
|
|
const { deps, writes, cognito } = fakes(
|
||
|
|
{ 'sh-mcp-ops@seahavenind.com': ['x@seahavenind.com'] },
|
||
|
|
{},
|
||
|
|
);
|
||
|
|
(cognito.addMember as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
|
||
|
|
new Error('cognito throttled'),
|
||
|
|
);
|
||
|
|
await expect(runSync(deps)).rejects.toThrow('cognito throttled');
|
||
|
|
expect(writes).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|