sh-mcp/packages/calendar/test/dev-client.test.ts
Adam Moussa a60a5a5794 Add security-weighted test suite + coverage gate; wire tooling
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.
2026-06-26 12:48:26 -04:00

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);
});
});
});