Implement URI-based API versioning with /api/v1 prefix across all routes. This establishes a foundation for future API evolution and breaking changes. Changes: - server.ts: All routes mounted under /api/v1/ (15 route handlers) - apiClient.ts: Base URL updated to /api/v1 - swagger.ts: OpenAPI server URL changed to /api/v1 - Redirect middleware: Added backwards compatibility for /api/* → /api/v1/* - Tests: Updated 72 test files with versioned path assertions - ADR documentation: Marked Phase 1 as complete (Accepted status) Test fixes: - apiClient.test.ts: 27 tests updated for /api/v1 paths - user.routes.ts: 36 log messages updated to reflect versioned paths - swagger.test.ts: 1 test updated for new server URL - All integration/E2E tests updated for versioned endpoints All Phase 1 acceptance criteria met: ✓ Routes use /api/v1/ prefix ✓ Frontend requests /api/v1/ ✓ OpenAPI docs reflect /api/v1/ ✓ Backwards compatibility via redirect middleware ✓ Tests pass with versioned paths Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
266 lines
9.0 KiB
TypeScript
266 lines
9.0 KiB
TypeScript
// src/config/swagger.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { swaggerSpec } from './swagger';
|
|
|
|
// Type definition for OpenAPI 3.0 spec structure used in tests
|
|
interface OpenAPISpec {
|
|
openapi: string;
|
|
info: {
|
|
title: string;
|
|
version: string;
|
|
description?: string;
|
|
contact?: { name: string };
|
|
license?: { name: string };
|
|
};
|
|
servers: Array<{ url: string; description?: string }>;
|
|
components: {
|
|
securitySchemes?: {
|
|
bearerAuth?: {
|
|
type: string;
|
|
scheme: string;
|
|
bearerFormat?: string;
|
|
description?: string;
|
|
};
|
|
};
|
|
schemas?: Record<string, unknown>;
|
|
};
|
|
tags: Array<{ name: string; description?: string }>;
|
|
paths?: Record<string, unknown>;
|
|
}
|
|
|
|
// Cast to typed spec for property access
|
|
const spec = swaggerSpec as OpenAPISpec;
|
|
|
|
/**
|
|
* Tests for src/config/swagger.ts - OpenAPI/Swagger configuration.
|
|
*
|
|
* These tests verify the swagger specification structure and content
|
|
* without testing the swagger-jsdoc library itself.
|
|
*/
|
|
describe('swagger configuration', () => {
|
|
describe('swaggerSpec export', () => {
|
|
it('should export a swagger specification object', () => {
|
|
expect(swaggerSpec).toBeDefined();
|
|
expect(typeof swaggerSpec).toBe('object');
|
|
});
|
|
|
|
it('should have openapi version 3.0.0', () => {
|
|
expect(spec.openapi).toBe('3.0.0');
|
|
});
|
|
});
|
|
|
|
describe('info section', () => {
|
|
it('should have info object with required fields', () => {
|
|
expect(spec.info).toBeDefined();
|
|
expect(spec.info.title).toBe('Flyer Crawler API');
|
|
expect(spec.info.version).toBe('1.0.0');
|
|
});
|
|
|
|
it('should have description', () => {
|
|
expect(spec.info.description).toBeDefined();
|
|
expect(spec.info.description).toContain('Flyer Crawler');
|
|
});
|
|
|
|
it('should have contact information', () => {
|
|
expect(spec.info.contact).toBeDefined();
|
|
expect(spec.info.contact?.name).toBe('API Support');
|
|
});
|
|
|
|
it('should have license information', () => {
|
|
expect(spec.info.license).toBeDefined();
|
|
expect(spec.info.license?.name).toBe('Private');
|
|
});
|
|
});
|
|
|
|
describe('servers section', () => {
|
|
it('should have servers array', () => {
|
|
expect(spec.servers).toBeDefined();
|
|
expect(Array.isArray(spec.servers)).toBe(true);
|
|
expect(spec.servers.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should have /api/v1 as the server URL (ADR-008)', () => {
|
|
const apiServer = spec.servers.find((s) => s.url === '/api/v1');
|
|
expect(apiServer).toBeDefined();
|
|
expect(apiServer?.description).toBe('API server (v1)');
|
|
});
|
|
});
|
|
|
|
describe('components section', () => {
|
|
it('should have components object', () => {
|
|
expect(spec.components).toBeDefined();
|
|
});
|
|
|
|
describe('securitySchemes', () => {
|
|
it('should have bearerAuth security scheme', () => {
|
|
expect(spec.components.securitySchemes).toBeDefined();
|
|
expect(spec.components.securitySchemes?.bearerAuth).toBeDefined();
|
|
});
|
|
|
|
it('should configure bearerAuth as HTTP bearer with JWT format', () => {
|
|
const bearerAuth = spec.components.securitySchemes?.bearerAuth;
|
|
expect(bearerAuth?.type).toBe('http');
|
|
expect(bearerAuth?.scheme).toBe('bearer');
|
|
expect(bearerAuth?.bearerFormat).toBe('JWT');
|
|
});
|
|
|
|
it('should have description for bearerAuth', () => {
|
|
const bearerAuth = spec.components.securitySchemes?.bearerAuth;
|
|
expect(bearerAuth?.description).toContain('JWT token');
|
|
});
|
|
});
|
|
|
|
describe('schemas', () => {
|
|
const schemas = () => spec.components.schemas as Record<string, any>;
|
|
|
|
it('should have schemas object', () => {
|
|
expect(spec.components.schemas).toBeDefined();
|
|
});
|
|
|
|
it('should have SuccessResponse schema (ADR-028)', () => {
|
|
const schema = schemas().SuccessResponse;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.success).toBeDefined();
|
|
expect(schema.properties.data).toBeDefined();
|
|
expect(schema.required).toContain('success');
|
|
expect(schema.required).toContain('data');
|
|
});
|
|
|
|
it('should have ErrorResponse schema (ADR-028)', () => {
|
|
const schema = schemas().ErrorResponse;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.success).toBeDefined();
|
|
expect(schema.properties.error).toBeDefined();
|
|
expect(schema.required).toContain('success');
|
|
expect(schema.required).toContain('error');
|
|
});
|
|
|
|
it('should have ErrorResponse error object with code and message', () => {
|
|
const errorSchema = schemas().ErrorResponse.properties.error;
|
|
expect(errorSchema.properties.code).toBeDefined();
|
|
expect(errorSchema.properties.message).toBeDefined();
|
|
expect(errorSchema.required).toContain('code');
|
|
expect(errorSchema.required).toContain('message');
|
|
});
|
|
|
|
it('should have ServiceHealth schema', () => {
|
|
const schema = schemas().ServiceHealth;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.status).toBeDefined();
|
|
expect(schema.properties.status.enum).toContain('healthy');
|
|
expect(schema.properties.status.enum).toContain('degraded');
|
|
expect(schema.properties.status.enum).toContain('unhealthy');
|
|
});
|
|
|
|
it('should have Achievement schema', () => {
|
|
const schema = schemas().Achievement;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.achievement_id).toBeDefined();
|
|
expect(schema.properties.name).toBeDefined();
|
|
expect(schema.properties.description).toBeDefined();
|
|
expect(schema.properties.icon).toBeDefined();
|
|
expect(schema.properties.points_value).toBeDefined();
|
|
});
|
|
|
|
it('should have UserAchievement schema extending Achievement', () => {
|
|
const schema = schemas().UserAchievement;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.allOf).toBeDefined();
|
|
expect(schema.allOf[0].$ref).toBe('#/components/schemas/Achievement');
|
|
});
|
|
|
|
it('should have LeaderboardUser schema', () => {
|
|
const schema = schemas().LeaderboardUser;
|
|
expect(schema).toBeDefined();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.user_id).toBeDefined();
|
|
expect(schema.properties.full_name).toBeDefined();
|
|
expect(schema.properties.points).toBeDefined();
|
|
expect(schema.properties.rank).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('tags section', () => {
|
|
it('should have tags array', () => {
|
|
expect(spec.tags).toBeDefined();
|
|
expect(Array.isArray(spec.tags)).toBe(true);
|
|
});
|
|
|
|
it('should have Health tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Health');
|
|
expect(tag).toBeDefined();
|
|
expect(tag?.description).toContain('health');
|
|
});
|
|
|
|
it('should have Auth tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Auth');
|
|
expect(tag).toBeDefined();
|
|
expect(tag?.description).toContain('Authentication');
|
|
});
|
|
|
|
it('should have Users tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Users');
|
|
expect(tag).toBeDefined();
|
|
expect(tag?.description).toContain('User');
|
|
});
|
|
|
|
it('should have Achievements tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Achievements');
|
|
expect(tag).toBeDefined();
|
|
expect(tag?.description).toContain('Gamification');
|
|
});
|
|
|
|
it('should have Flyers tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Flyers');
|
|
expect(tag).toBeDefined();
|
|
});
|
|
|
|
it('should have Recipes tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Recipes');
|
|
expect(tag).toBeDefined();
|
|
});
|
|
|
|
it('should have Budgets tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Budgets');
|
|
expect(tag).toBeDefined();
|
|
});
|
|
|
|
it('should have Admin tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'Admin');
|
|
expect(tag).toBeDefined();
|
|
expect(tag?.description).toContain('admin');
|
|
});
|
|
|
|
it('should have System tag', () => {
|
|
const tag = spec.tags.find((t) => t.name === 'System');
|
|
expect(tag).toBeDefined();
|
|
});
|
|
|
|
it('should have 9 tags total', () => {
|
|
expect(spec.tags.length).toBe(9);
|
|
});
|
|
});
|
|
|
|
describe('specification validity', () => {
|
|
it('should have paths object (may be empty if no JSDoc annotations parsed)', () => {
|
|
// swagger-jsdoc creates paths from JSDoc annotations in route files
|
|
// In test environment, this may be empty if routes aren't scanned
|
|
expect(swaggerSpec).toHaveProperty('paths');
|
|
});
|
|
|
|
it('should be a valid JSON-serializable object', () => {
|
|
expect(() => JSON.stringify(swaggerSpec)).not.toThrow();
|
|
});
|
|
|
|
it('should produce valid JSON output', () => {
|
|
const json = JSON.stringify(swaggerSpec);
|
|
expect(() => JSON.parse(json)).not.toThrow();
|
|
});
|
|
});
|
|
});
|