Files
flyer-crawler.projectium.com/src/config/swagger.test.ts
Torben Sorensen 2075ed199b Complete ADR-008 Phase 1: API Versioning Strategy
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>
2026-01-26 21:23:25 -08:00

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