mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 05:52:03 +00:00
Add tests for the highest-risk surface (build-plan §5, design.md §7.3): tool-hiding, server-side scope enforcement (incl. forced hidden calls), audience binding, input-schema validation, finance redaction on egress, audit emission with hashed args, prompt-injection regression (tool output is data), rate limiting, MCP conformance (in-memory transport round-trip), OpenAPI 3.1 validity, and local-auth safety. Add HTTP integration tests (supertest) for both servers and per-package dev-client tests. 405 tests pass. Wire the coverage gate into vitest.config.ts: 80% overall, with per-file thresholds on the auth + dispatch crown jewels; exclude deferred real client stubs, entrypoints, cdk apps, and aws-only config from the gate (documented). Extend eslint flat config + add .prettierignore to cover servers/. Commit the updated package-lock.json.
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { InMemoryCalendarClient } from '../src/dev-client.js';
|
|
|
|
const WIDE = { timeMin: '2026-01-01T00:00:00Z', timeMax: '2026-12-31T23:59:59Z' };
|
|
|
|
describe('InMemoryCalendarClient', () => {
|
|
describe('getEvents', () => {
|
|
it("returns lauren's seeded events partitioned by userSub", async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const events = await client.getEvents('lauren@seahavenind.com', WIDE);
|
|
const ids = events.map((e) => e.id);
|
|
expect(ids).toContain('evt-lauren-001');
|
|
expect(ids).toContain('evt-lauren-002');
|
|
expect(events).toHaveLength(2);
|
|
});
|
|
|
|
it("returns adam's own events, not lauren's", async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const events = await client.getEvents('adam@seahavenind.com', WIDE);
|
|
const ids = events.map((e) => e.id);
|
|
expect(ids).toEqual(['evt-adam-001']);
|
|
expect(ids).not.toContain('evt-lauren-001');
|
|
});
|
|
|
|
it('returns an empty list for an unknown sub', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const events = await client.getEvents('nobody@example.com', WIDE);
|
|
expect(events).toEqual([]);
|
|
});
|
|
|
|
it('filters by the time window', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const events = await client.getEvents('lauren@seahavenind.com', {
|
|
timeMin: '2026-06-26T00:00:00Z',
|
|
timeMax: '2026-06-27T00:00:00Z',
|
|
});
|
|
expect(events.map((e) => e.id)).toEqual(['evt-lauren-002']);
|
|
});
|
|
|
|
it('respects maxResults', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const events = await client.getEvents('lauren@seahavenind.com', {
|
|
...WIDE,
|
|
maxResults: 1,
|
|
});
|
|
expect(events).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('checkAvailability', () => {
|
|
it('reports busy slots and free gaps around them', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const result = await client.checkAvailability('lauren@seahavenind.com', WIDE);
|
|
expect(result.busy).toHaveLength(2);
|
|
expect(result.busy[0]).toEqual({
|
|
start: '2026-06-25T15:00:00Z',
|
|
end: '2026-06-25T15:30:00Z',
|
|
});
|
|
expect(result.free.length).toBeGreaterThan(0);
|
|
// window starts before first busy slot -> a leading free gap exists
|
|
expect(Date.parse(result.free[0].start)).toBe(Date.parse(WIDE.timeMin));
|
|
});
|
|
|
|
it('returns the full window as free when there are no events', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const result = await client.checkAvailability('nobody@example.com', WIDE);
|
|
expect(result.busy).toEqual([]);
|
|
expect(result.free).toEqual([
|
|
{
|
|
start: new Date(Date.parse(WIDE.timeMin)).toISOString(),
|
|
end: new Date(Date.parse(WIDE.timeMax)).toISOString(),
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('createEvent', () => {
|
|
it('appends the new event and returns it', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const created = await client.createEvent('lauren@seahavenind.com', {
|
|
summary: 'New planning session',
|
|
description: 'Discuss Q3 roadmap',
|
|
start: '2026-07-01T16:00:00Z',
|
|
end: '2026-07-01T17:00:00Z',
|
|
attendees: [
|
|
{ email: 'adam@seahavenind.com', displayName: 'Adam' },
|
|
{ email: 'x@example.com' },
|
|
],
|
|
});
|
|
expect(created.id).toMatch(/^evt-dev-\d+$/);
|
|
expect(created.summary).toBe('New planning session');
|
|
expect(created.description).toBe('Discuss Q3 roadmap');
|
|
expect(created.status).toBe('confirmed');
|
|
expect(created.attendees).toEqual([
|
|
{ email: 'adam@seahavenind.com', displayName: 'Adam', responseStatus: 'needsAction' },
|
|
{ email: 'x@example.com', responseStatus: 'needsAction' },
|
|
]);
|
|
|
|
const events = await client.getEvents('lauren@seahavenind.com', WIDE);
|
|
expect(events.map((e) => e.id)).toContain(created.id);
|
|
expect(events).toHaveLength(3);
|
|
});
|
|
|
|
it('creates an event for a previously-unknown sub', async () => {
|
|
const client = new InMemoryCalendarClient();
|
|
const created = await client.createEvent('fresh@seahavenind.com', {
|
|
summary: 'First event',
|
|
start: '2026-07-02T16:00:00Z',
|
|
end: '2026-07-02T17:00:00Z',
|
|
});
|
|
expect(created.summary).toBe('First event');
|
|
expect(created.description).toBeUndefined();
|
|
expect(created.attendees).toBeUndefined();
|
|
const events = await client.getEvents('fresh@seahavenind.com', WIDE);
|
|
expect(events).toHaveLength(1);
|
|
});
|
|
});
|
|
});
|