sh-mcp/packages/gmail/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

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