Files
crumbs-original/src/lib/server/api-keys.test.ts
T
Frederic CabassutandClaude Opus 4.6 a82ba8f61d feat: add API key CRUD and Bearer token auth for MCP
- API key generation with SHA-256 hashing and crumbs_ prefix
- User-scoped key validation, listing, and deletion
- Bearer token auth in hooks.server.ts for /api/mcp paths
- Populates event.locals.user from API key for downstream scoping
- 13 unit tests covering CRUD, validation, and user isolation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 02:03:04 +01:00

126 lines
4.0 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { createTestDb } from './db/test-helpers.js';
import { generateApiKey, validateApiKey, listApiKeys, deleteApiKey } from './api-keys.js';
import { users } from './db/schema.js';
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import type * as schema from './db/schema.js';
describe('API Keys', () => {
let db: BetterSQLite3Database<typeof schema>;
let userId: number;
beforeEach(() => {
({ db } = createTestDb());
// Create a test user
db.insert(users)
.values({ email: 'test@example.com', displayName: 'Test', createdAt: new Date() })
.run();
userId = 1;
});
describe('generateApiKey', () => {
it('returns key with crumbs_ prefix', () => {
const result = generateApiKey('Test Key', userId, db);
expect(result.key).toMatch(/^crumbs_[a-f0-9]{64}$/);
expect(result.name).toBe('Test Key');
expect(result.id).toBeTruthy();
expect(result.keyPrefix).toBe(result.key.slice(0, 12));
});
it('generates unique keys each time', () => {
const key1 = generateApiKey('Key 1', userId, db);
const key2 = generateApiKey('Key 2', userId, db);
expect(key1.key).not.toBe(key2.key);
expect(key1.id).not.toBe(key2.id);
});
});
describe('validateApiKey', () => {
it('returns valid with userId for a valid key', () => {
const { key } = generateApiKey('Test Key', userId, db);
const result = validateApiKey(key, db);
expect(result.valid).toBe(true);
expect(result.userId).toBe(userId);
});
it('returns invalid for an invalid key', () => {
const result = validateApiKey('crumbs_invalid', db);
expect(result.valid).toBe(false);
expect(result.userId).toBeNull();
});
it('returns invalid for empty string', () => {
expect(validateApiKey('', db).valid).toBe(false);
});
it('returns invalid for key without prefix', () => {
expect(validateApiKey('no_prefix_here', db).valid).toBe(false);
});
it('updates lastUsedAt on validation', () => {
const { key, id } = generateApiKey('Test Key', userId, db);
const before = listApiKeys(userId, db).find((k) => k.id === id);
expect(before?.lastUsedAt).toBeNull();
validateApiKey(key, db);
const after = listApiKeys(userId, db).find((k) => k.id === id);
expect(after?.lastUsedAt).toBeInstanceOf(Date);
});
});
describe('listApiKeys', () => {
it('returns empty array when no keys exist', () => {
expect(listApiKeys(userId, db)).toEqual([]);
});
it('returns keys without hash', () => {
generateApiKey('Key 1', userId, db);
generateApiKey('Key 2', userId, db);
const keys = listApiKeys(userId, db);
expect(keys).toHaveLength(2);
expect(keys[0]).toHaveProperty('name');
expect(keys[0]).toHaveProperty('keyPrefix');
expect(keys[0]).toHaveProperty('createdAt');
expect(keys[0]).not.toHaveProperty('keyHash');
});
it('only returns keys for the specified user', () => {
db.insert(users)
.values({ email: 'other@example.com', displayName: 'Other', createdAt: new Date() })
.run();
const otherUserId = 2;
generateApiKey('My Key', userId, db);
generateApiKey('Other Key', otherUserId, db);
expect(listApiKeys(userId, db)).toHaveLength(1);
expect(listApiKeys(otherUserId, db)).toHaveLength(1);
});
});
describe('deleteApiKey', () => {
it('deletes an existing key and returns true', () => {
const { id, key } = generateApiKey('Test Key', userId, db);
expect(deleteApiKey(id, userId, db)).toBe(true);
expect(validateApiKey(key, db).valid).toBe(false);
expect(listApiKeys(userId, db)).toHaveLength(0);
});
it('returns false for non-existent key', () => {
expect(deleteApiKey('nonexistent', userId, db)).toBe(false);
});
it('cannot delete another user\'s key', () => {
db.insert(users)
.values({ email: 'other@example.com', displayName: 'Other', createdAt: new Date() })
.run();
const otherUserId = 2;
const { id } = generateApiKey('Other Key', otherUserId, db);
expect(deleteApiKey(id, userId, db)).toBe(false);
});
});
});