/**
* MCP Server Authentication Tests (Phase 1)
*
* Tests format-based API key validation.
* Database-backed validation tests moved to Phase 2.
*
* Security requirements:
* - API key format validation (sk_live_/sk_test_ prefix)
* - Dangerous character rejection
* - Minimum key length enforcement
*
* @package SnapBack MCP Server
*/
import { describe, expect, it } from "vitest";
import { type ValidationResult, validateApiKey } from "../validation.js";
describe("MCP Server Authentication (Phase 1)", () => {
describe("API Key Format Validation", () => {
it("should accept valid sk_live_ prefixed keys", () => {
// GIVEN: A valid live API key
const validKey = "sk_live_abcdef1234567890abcdef1234567890";
// WHEN: We validate the key format
const result = validateApiKey(validKey);
// THEN: Validation should succeed
expect(result.valid).toBe(true);
});
it("should accept valid sk_test_ prefixed keys", () => {
// GIVEN: A valid test API key
const testKey = "sk_test_abcdef1234567890abcdef1234567890";
// WHEN: We validate the key format
const result = validateApiKey(testKey);
// THEN: Validation should succeed
expect(result.valid).toBe(true);
});
it("should reject keys without valid prefix", () => {
// GIVEN: An invalid API key without proper prefix
const invalidKey = "invalid_key_1234567890abcdef123456";
// WHEN: We validate the key format
const result = validateApiKey(invalidKey);
// THEN: Validation should fail
expect(result.valid).toBe(false);
expect(result.error).toContain("Invalid API key format");
});
it("should reject empty API key", () => {
// GIVEN: An empty API key
const emptyKey = "";
// WHEN: We validate the key format
const result = validateApiKey(emptyKey);
// THEN: Validation should fail
expect(result.valid).toBe(false);
expect(result.error).toContain("Missing API key");
});
it("should reject keys with dangerous characters", () => {
// GIVEN: An API key with shell injection characters
const dangerousKey = "sk_live_test$(whoami)1234567890";
// WHEN: We validate the key format
const result = validateApiKey(dangerousKey);
// THEN: Validation should fail (security)
expect(result.valid).toBe(false);
});
it("should reject keys that are too short", () => {
// GIVEN: An API key with valid prefix but too short
const shortKey = "sk_live_tooshort";
// WHEN: We validate the key format
const result = validateApiKey(shortKey);
// THEN: Validation should fail (must be 32+ chars after prefix)
expect(result.valid).toBe(false);
expect(result.error).toContain("32");
});
});
describe("MCP Scope Permission Checking", () => {
it("should define mcp:tools as a valid scope", () => {
// GIVEN: The MCP scope type
const validScopes = ["api:read", "api:write", "mcp:tools", "cli:snapshots"];
// THEN: mcp:tools should be included
expect(validScopes).toContain("mcp:tools");
});
});
describe("Security Best Practices", () => {
it("should never return full API key in validation response", () => {
// GIVEN: A validation result
const validResult: ValidationResult & { userId?: string } = {
valid: true,
userId: "user_123",
};
// THEN: Full key should not be in response
expect("key" in validResult).toBe(false);
expect("apiKey" in validResult).toBe(false);
});
it("should accept keys with 64+ characters after prefix (Better Auth format)", () => {
// GIVEN: A long key matching Better Auth's 64-char generation
const longKey = "sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2";
// WHEN: We validate the key format
const result = validateApiKey(longKey);
// THEN: Validation should pass
expect(result.valid).toBe(true);
});
});
describe("Better Auth Integration Requirements", () => {
it("should be compatible with Better Auth API key plugin format", () => {
// GIVEN: Better Auth apiKey plugin configuration
// Permissions structure: { "mcp": ["tools"] }
// THEN: Our validation should align with Better Auth's permission format
const betterAuthPermissionFormat = {
mcp: ["tools"],
"snapback:snapshot": ["read", "write"],
};
expect(betterAuthPermissionFormat.mcp).toContain("tools");
});
});
});
// Phase 2 tests for database-backed validation are defined below
// These will be enabled when @snapback/auth is integrated
describe.skip("Database Validation Integration (Phase 2)", () => {
it("should have validateApiKeyWithDatabase function that checks permissions", () => {
// Phase 2: Requires @snapback/auth integration
});
it("should return userId on successful validation", () => {
// Phase 2: Requires database access
});
it("should check key is not revoked", () => {
// Phase 2: Requires database access
});
it("should check key is not expired", () => {
// Phase 2: Requires database access
});
it("should check key has mcp:tools permission", () => {
// Phase 2: Requires database access
});
it("should reject requests when auth service is unavailable", () => {
// Phase 2: Security-first rejection when DB unavailable
});
});