mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 16:18:58 +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.
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { InMemoryGmailClient } from '../src/dev-client.js';
|
|
|
|
describe('InMemoryGmailClient', () => {
|
|
describe('searchInbox', () => {
|
|
it('matches a case-insensitive substring against subject/snippet', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const results = await client.searchInbox({
|
|
userSub: 'lauren@seahavenind.com',
|
|
query: 'INVOICE',
|
|
maxResults: 10,
|
|
});
|
|
expect(results.map((m) => m.id)).toEqual(['msg-l-1']);
|
|
});
|
|
|
|
it('matches against the snippet body too', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const results = await client.searchInbox({
|
|
userSub: 'lauren@seahavenind.com',
|
|
query: 'walkthrough',
|
|
maxResults: 10,
|
|
});
|
|
expect(results.map((m) => m.id)).toEqual(['msg-l-2']);
|
|
});
|
|
|
|
it('caps results at maxResults', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const results = await client.searchInbox({
|
|
userSub: 'lauren@seahavenind.com',
|
|
query: '',
|
|
maxResults: 1,
|
|
});
|
|
expect(results).toHaveLength(1);
|
|
});
|
|
|
|
it("is partitioned by userSub — lauren cannot see adam's mail", async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const results = await client.searchInbox({
|
|
userSub: 'lauren@seahavenind.com',
|
|
query: 'check #2087',
|
|
maxResults: 10,
|
|
});
|
|
expect(results).toEqual([]);
|
|
|
|
const adamResults = await client.searchInbox({
|
|
userSub: 'adam@seahavenind.com',
|
|
query: 'check #2087',
|
|
maxResults: 10,
|
|
});
|
|
expect(adamResults.map((m) => m.id)).toEqual(['msg-a-1']);
|
|
});
|
|
|
|
it('returns an empty list for an unknown sub', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const results = await client.searchInbox({
|
|
userSub: 'nobody@example.com',
|
|
query: 'invoice',
|
|
maxResults: 10,
|
|
});
|
|
expect(results).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getThreadDetail', () => {
|
|
it('returns an owned thread', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
const thread = await client.getThreadDetail({
|
|
userSub: 'lauren@seahavenind.com',
|
|
threadId: 'thr-l-1',
|
|
});
|
|
expect(thread.threadId).toBe('thr-l-1');
|
|
expect(thread.subject).toBe('Invoice #4821 from Coastal Supply');
|
|
expect(thread.messages).toHaveLength(1);
|
|
});
|
|
|
|
it('throws for an unknown threadId', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
await expect(
|
|
client.getThreadDetail({ userSub: 'lauren@seahavenind.com', threadId: 'thr-missing' }),
|
|
).rejects.toThrow(/not found/);
|
|
});
|
|
|
|
it('throws when the thread is not owned by the caller', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
await expect(
|
|
client.getThreadDetail({ userSub: 'lauren@seahavenind.com', threadId: 'thr-a-1' }),
|
|
).rejects.toThrow(/not found/);
|
|
});
|
|
|
|
it('throws for an unknown user', async () => {
|
|
const client = new InMemoryGmailClient();
|
|
await expect(
|
|
client.getThreadDetail({ userSub: 'nobody@example.com', threadId: 'thr-l-1' }),
|
|
).rejects.toThrow(/not found/);
|
|
});
|
|
});
|
|
});
|