Skip to main content
Glama
token-validation.contract.test.ts11.1 kB
/** * Contract Test: Token Validation State Machine * * Tests FR-001, FR-002, FR-003, FR-008, FR-009 from spec.md * These tests MUST FAIL before implementation (TDD approach) * * @see /specs/006-more-mcp-compliance/contracts/token-validation.contract.ts */ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; import { TokenValidator } from '../../src/services/token-validator.interface.js'; import { TokenErrorCategory, TOKEN_ERROR_MESSAGES, } from '../../src/types/token-validation.types.js'; import { createInMemoryApiService } from '../helpers/inMemoryTodoistApiService.js'; describe('Token Validation State Machine Contract', () => { let originalToken: string | undefined; beforeEach(async () => { // Save original token originalToken = process.env.TODOIST_API_TOKEN; // Reset config cache const { resetConfig } = await import('../../src/config/index.js'); resetConfig(); // Reset singleton state for isolated tests const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); (TokenValidatorSingleton as any).resetForTesting(); // Setup mock API service for all tests const mockApiService = createInMemoryApiService(); (TokenValidatorSingleton as any).setMockApiService(mockApiService); }); afterEach(() => { // Restore original token if (originalToken) { process.env.TODOIST_API_TOKEN = originalToken; } else { delete process.env.TODOIST_API_TOKEN; } }); describe('FR-001: Server starts without token', () => { test('allows initialization without TODOIST_API_TOKEN', async () => { delete process.env.TODOIST_API_TOKEN; // This will fail until TokenValidator is implemented const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; const state = validator.getValidationState(); expect(state.status).toBe('not_validated'); expect(state.validatedAt).toBeNull(); expect(state.error).toBeNull(); }); test('isTokenConfigured returns false when token missing', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; expect(validator.isTokenConfigured()).toBe(false); }); test('isTokenConfigured returns true when token present', async () => { process.env.TODOIST_API_TOKEN = 'test_token_123456'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; expect(validator.isTokenConfigured()).toBe(true); }); }); describe('FR-002: MCP protocol handshake without token', () => { test('getValidationState never triggers validation', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Should not throw even without token const state = validator.getValidationState(); expect(state.status).toBe('not_validated'); }); }); describe('FR-003: Validation triggered on first tool call', () => { test('validateOnce throws TOKEN_MISSING when token absent', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; await expect(validator.validateOnce()).rejects.toThrow(); const state = validator.getValidationState(); expect(state.status).toBe('invalid'); expect(state.error?.category).toBe(TokenErrorCategory.TOKEN_MISSING); }); test('validateOnce succeeds with valid token', async () => { // This test will fail until TodoistApiService integration is complete process.env.TODOIST_API_TOKEN = 'valid_test_token'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Mock successful API response (implementation detail) await expect(validator.validateOnce()).resolves.not.toThrow(); const state = validator.getValidationState(); expect(state.status).toBe('valid'); expect(state.validatedAt).toBeInstanceOf(Date); expect(state.error).toBeNull(); }); }); describe('FR-008: Error message format "[Category]. [Next step]"', () => { test('TOKEN_MISSING error has actionable message', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; try { await validator.validateOnce(); expect(true).toBe(false); // Should not reach here } catch (error: any) { const state = validator.getValidationState(); expect(state.error?.message).toBe( TOKEN_ERROR_MESSAGES[TokenErrorCategory.TOKEN_MISSING] ); expect(state.error?.message).toMatch(/^Token missing\./); expect(state.error?.message).toContain('Set TODOIST_API_TOKEN'); } }); test('All error categories follow format', () => { Object.values(TokenErrorCategory).forEach(category => { const message = TOKEN_ERROR_MESSAGES[category]; expect(message).toMatch(/^[A-Za-z ]+\. [A-Z]/); // "[Category]. [Next step]" }); }); }); describe('FR-009: Validation caching for session (Clarification Q1)', () => { test('validateOnce is idempotent - calls once only', async () => { process.env.TODOIST_API_TOKEN = 'valid_test_token'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // First call validates await validator.validateOnce(); const firstState = validator.getValidationState(); const firstTimestamp = firstState.validatedAt; // Small delay to ensure different timestamps if re-validating await new Promise(resolve => setTimeout(resolve, 10)); // Second call should use cache await validator.validateOnce(); const secondState = validator.getValidationState(); expect(secondState.validatedAt).toEqual(firstTimestamp); // Same timestamp = cached }); test('validateOnce caches validation failures', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // First call fails await expect(validator.validateOnce()).rejects.toThrow(); const firstError = validator.getValidationState().error; // Second call should throw same cached error await expect(validator.validateOnce()).rejects.toThrow(); const secondError = validator.getValidationState().error; expect(secondError).toEqual(firstError); // Same error object = cached }); test('cache hit performance <1ms', async () => { process.env.TODOIST_API_TOKEN = 'valid_test_token'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // First call (will be slow due to API call) await validator.validateOnce(); // Subsequent call should be <1ms const start = performance.now(); await validator.validateOnce(); const duration = performance.now() - start; expect(duration).toBeLessThan(1); // <1ms for cache hit }); }); describe('State transition rules', () => { test('not_validated → valid → (no transitions)', async () => { process.env.TODOIST_API_TOKEN = 'valid_test_token'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Initial state expect(validator.getValidationState().status).toBe('not_validated'); // Transition to valid await validator.validateOnce(); expect(validator.getValidationState().status).toBe('valid'); // Should stay valid (no re-validation) await validator.validateOnce(); expect(validator.getValidationState().status).toBe('valid'); }); test('not_validated → invalid → (no transitions)', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Initial state expect(validator.getValidationState().status).toBe('not_validated'); // Transition to invalid await expect(validator.validateOnce()).rejects.toThrow(); expect(validator.getValidationState().status).toBe('invalid'); // Should stay invalid (no re-validation) await expect(validator.validateOnce()).rejects.toThrow(); expect(validator.getValidationState().status).toBe('invalid'); }); }); describe('Error metadata completeness', () => { test('includes timestamp when error occurs', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; await expect(validator.validateOnce()).rejects.toThrow(); const state = validator.getValidationState(); expect(state.error?.timestamp).toBeInstanceOf(Date); }); test('validatedAt only set when status=valid', async () => { delete process.env.TODOIST_API_TOKEN; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Invalid state await expect(validator.validateOnce()).rejects.toThrow(); expect(validator.getValidationState().validatedAt).toBeNull(); }); test('error only set when status=invalid', async () => { process.env.TODOIST_API_TOKEN = 'valid_test_token'; const { TokenValidatorSingleton } = await import( '../../src/services/token-validator.js' ); const validator: TokenValidator = TokenValidatorSingleton; // Valid state await validator.validateOnce(); expect(validator.getValidationState().error).toBeNull(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shayonpal/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server