swagger.e2e.spec.tsβ’9.96 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { ConfigService } from '@nestjs/config'
import { createMockConfigService, setupTestEnvironment } from './test-config.helper'
describe('Swagger/OpenAPI (e2e)', () => {
let app: INestApplication
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let swaggerDocument: any
beforeAll(async () => {
try {
// Set up environment variables BEFORE importing AppModule
// This ensures McpAuthModule.forRoot() and TimezoneModule.forRoot() have correct env vars
setupTestEnvironment()
// Dynamic import to ensure setupTestEnvironment() runs first
// (top-level imports are hoisted and run before module-level code)
const { AppModule } = await import('../src/app/app.module')
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(ConfigService)
.useValue(createMockConfigService())
.compile()
app = moduleFixture.createNestApplication()
// Setup Swagger the same way as in main.test-helper.ts (development mode)
const config = new DocumentBuilder()
.setTitle('Timezone MCP Server API - Development')
.setDescription(
'MCP (Model Context Protocol) server for timezone operations. ' +
'\n\n**Development Mode**: This REST API is only available when NODE_ENV=development. ' +
'It provides unauthenticated endpoints for testing and development. ' +
'\n\n**Production Mode**: Only the MCP endpoint at /mcp is available, protected by OAuth authentication. ' +
'\n\nProvides endpoints to get available regions, cities, and current time in any timezone with ISO 8601 formatted timestamps.',
)
.setVersion('1.0')
.addServer('', 'Current server')
.addTag('health', 'Health check endpoints')
.addTag('timezones', 'Timezone information endpoints (development only)')
.build()
swaggerDocument = SwaggerModule.createDocument(app, config)
await app.init()
} catch (error) {
console.error('Failed to initialize app in tests:', error)
throw error
}
})
afterAll(async () => {
if (app) {
await app.close()
}
})
it('should generate a valid OpenAPI document', () => {
expect(swaggerDocument).toBeDefined()
expect(swaggerDocument.openapi).toBe('3.0.0')
})
it('should have correct API info', () => {
expect(swaggerDocument.info).toBeDefined()
expect(swaggerDocument.info.title).toBe('Timezone MCP Server API - Development')
expect(swaggerDocument.info.version).toBe('1.0')
expect(swaggerDocument.info.description).toContain('MCP (Model Context Protocol)')
expect(swaggerDocument.info.description).toContain('Development Mode')
})
it('should have correct servers configuration', () => {
expect(swaggerDocument.servers).toBeDefined()
expect(Array.isArray(swaggerDocument.servers)).toBe(true)
expect(swaggerDocument.servers.length).toBeGreaterThan(0)
// Empty string means relative URL (current server)
expect(swaggerDocument.servers[0].url).toBe('')
})
it('should have correct tags', () => {
expect(swaggerDocument.tags).toBeDefined()
expect(Array.isArray(swaggerDocument.tags)).toBe(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tagNames = swaggerDocument.tags.map((tag: any) => tag.name)
expect(tagNames).toContain('health')
expect(tagNames).toContain('timezones')
})
describe('Paths', () => {
it('should have health check endpoint documented', () => {
expect(swaggerDocument.paths).toBeDefined()
expect(swaggerDocument.paths['/health']).toBeDefined()
expect(swaggerDocument.paths['/health'].get).toBeDefined()
})
it('should have GET /timezones/regions endpoint documented', () => {
expect(swaggerDocument.paths['/timezones/regions']).toBeDefined()
expect(swaggerDocument.paths['/timezones/regions'].get).toBeDefined()
})
it('should have GET /timezones/regions/{region}/cities endpoint documented', () => {
expect(swaggerDocument.paths['/timezones/regions/{region}/cities']).toBeDefined()
expect(swaggerDocument.paths['/timezones/regions/{region}/cities'].get).toBeDefined()
})
it('should have GET /timezones/{region}/{city} endpoint documented', () => {
expect(swaggerDocument.paths['/timezones/{region}/{city}']).toBeDefined()
expect(swaggerDocument.paths['/timezones/{region}/{city}'].get).toBeDefined()
})
})
describe('Schemas', () => {
it('should have HealthResponseDto schema', () => {
expect(swaggerDocument.components).toBeDefined()
expect(swaggerDocument.components.schemas).toBeDefined()
expect(swaggerDocument.components.schemas.HealthResponseDto).toBeDefined()
const healthSchema = swaggerDocument.components.schemas.HealthResponseDto
expect(healthSchema.properties).toBeDefined()
expect(healthSchema.properties.status).toBeDefined()
expect(healthSchema.properties.timestamp).toBeDefined()
})
it('should have RegionsResponseDto schema', () => {
expect(swaggerDocument.components.schemas.RegionsResponseDto).toBeDefined()
const regionsSchema = swaggerDocument.components.schemas.RegionsResponseDto
expect(regionsSchema.properties).toBeDefined()
expect(regionsSchema.properties.regions).toBeDefined()
expect(regionsSchema.properties.count).toBeDefined()
})
it('should have CitiesResponseDto schema', () => {
expect(swaggerDocument.components.schemas.CitiesResponseDto).toBeDefined()
const citiesSchema = swaggerDocument.components.schemas.CitiesResponseDto
expect(citiesSchema.properties).toBeDefined()
expect(citiesSchema.properties.region).toBeDefined()
expect(citiesSchema.properties.cities).toBeDefined()
expect(citiesSchema.properties.count).toBeDefined()
})
it('should have TimezoneResponseDto schema', () => {
expect(swaggerDocument.components.schemas.TimezoneResponseDto).toBeDefined()
const timezoneSchema = swaggerDocument.components.schemas.TimezoneResponseDto
expect(timezoneSchema.properties).toBeDefined()
expect(timezoneSchema.properties.timezone).toBeDefined()
expect(timezoneSchema.properties.datetime_local).toBeDefined()
expect(timezoneSchema.properties.datetime_utc).toBeDefined()
expect(timezoneSchema.properties.timezone_offset).toBeDefined()
expect(timezoneSchema.properties.timestamp).toBeDefined()
})
})
describe('Health Endpoint Documentation', () => {
it('should have correct operation details', () => {
const operation = swaggerDocument.paths['/health'].get
expect(operation.summary).toBe('Health check endpoint')
expect(operation.description).toBe('Returns the health status of the service')
expect(operation.tags).toContain('health')
})
it('should have 200 response documented', () => {
const operation = swaggerDocument.paths['/health'].get
expect(operation.responses).toBeDefined()
expect(operation.responses['200']).toBeDefined()
expect(operation.responses['200'].description).toBe('Service is healthy')
})
})
describe('Timezone Endpoints Documentation', () => {
it('should have GET /timezones/regions with correct details', () => {
const operation = swaggerDocument.paths['/timezones/regions'].get
expect(operation.summary).toBe('Get all timezone regions')
expect(operation.tags).toContain('timezones')
expect(operation.responses['200']).toBeDefined()
})
it('should have GET /timezones/regions/{region}/cities with parameters', () => {
const operation = swaggerDocument.paths['/timezones/regions/{region}/cities'].get
expect(operation.summary).toBe('Get cities in a region')
expect(operation.tags).toContain('timezones')
expect(operation.parameters).toBeDefined()
expect(operation.parameters.length).toBeGreaterThan(0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const regionParam = operation.parameters.find((p: any) => p.name === 'region')
expect(regionParam).toBeDefined()
expect(regionParam.in).toBe('path')
expect(regionParam.required).toBe(true)
})
it('should document 400 error responses', () => {
const operation = swaggerDocument.paths['/timezones/regions/{region}/cities'].get
expect(operation.responses['400']).toBeDefined()
expect(operation.responses['400'].description).toBe('Invalid region name provided')
})
it('should have GET /timezones/{region}/{city} with two path parameters', () => {
const operation = swaggerDocument.paths['/timezones/{region}/{city}'].get
expect(operation.summary).toBe('Get current time in timezone')
expect(operation.parameters).toBeDefined()
expect(operation.parameters.length).toBe(2)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const regionParam = operation.parameters.find((p: any) => p.name === 'region')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cityParam = operation.parameters.find((p: any) => p.name === 'city')
expect(regionParam).toBeDefined()
expect(cityParam).toBeDefined()
// Check that the parameters have examples (they might be in schema.example instead of example)
const regionExample = regionParam.example || regionParam.schema?.example
const cityExample = cityParam.example || cityParam.schema?.example
expect(regionExample).toBe('America')
expect(cityExample).toBe('New_York')
})
})
})