import request from 'supertest'; import express from 'express'; import { createI18nRoutes } from '../../../src/routes/i18n'; import { i18nService } from '../../../src/i18n'; // Mock the i18n service jest.mock('../../../src/i18n', () => ({ i18nService: { isLocaleSupported: jest.fn(), getAvailableLocales: jest.fn(), getTranslations: jest.fn(), getDefaultLocale: jest.fn(), getLocaleDisplayName: jest.fn(), detectLocale: jest.fn() } })); const mockI18nService = i18nService as jest.Mocked; describe('I18n API Routes', () => { let app: express.Application; beforeEach(() => { app = express(); app.use('/api/i18n', createI18nRoutes()); // Reset mocks jest.clearAllMocks(); // Default mock implementations mockI18nService.getAvailableLocales.mockReturnValue(['en', 'es-MX']); mockI18nService.getDefaultLocale.mockReturnValue('en'); mockI18nService.getLocaleDisplayName.mockImplementation((locale: string) => { const names: Record = { 'en': 'English', 'es-MX': 'Español (México)' }; return names[locale] || locale; }); }); describe('GET /api/i18n/:locale', () => { const mockTranslations = { common: { appName: 'Great Lakes Ice Report', submit: 'Submit', cancel: 'Cancel' }, pages: { home: { title: 'Ice Report', subtitle: 'Community winter road conditions' } } }; it('should return translations for a supported locale', async () => { mockI18nService.isLocaleSupported.mockReturnValue(true); mockI18nService.getTranslations.mockReturnValue(mockTranslations); const response = await request(app) .get('/api/i18n/en') .expect(200); expect(response.headers['cache-control']).toBe('public, max-age=3600'); expect(response.headers['content-type']).toMatch(/application\/json/); expect(response.body).toEqual(mockTranslations); expect(mockI18nService.isLocaleSupported).toHaveBeenCalledWith('en'); expect(mockI18nService.getTranslations).toHaveBeenCalledWith('en'); }); it('should return translations for Spanish locale', async () => { const spanishTranslations = { common: { appName: 'Reporte de Hielo de los Grandes Lagos', submit: 'Enviar', cancel: 'Cancelar' } }; mockI18nService.isLocaleSupported.mockReturnValue(true); mockI18nService.getTranslations.mockReturnValue(spanishTranslations); const response = await request(app) .get('/api/i18n/es-MX') .expect(200); expect(response.body).toEqual(spanishTranslations); expect(mockI18nService.isLocaleSupported).toHaveBeenCalledWith('es-MX'); expect(mockI18nService.getTranslations).toHaveBeenCalledWith('es-MX'); }); it('should reject unsupported locale', async () => { mockI18nService.isLocaleSupported.mockReturnValue(false); const response = await request(app) .get('/api/i18n/fr') .expect(400); expect(response.body).toEqual({ error: 'Unsupported locale', supportedLocales: ['en', 'es-MX'] }); expect(mockI18nService.isLocaleSupported).toHaveBeenCalledWith('fr'); expect(mockI18nService.getAvailableLocales).toHaveBeenCalled(); expect(mockI18nService.getTranslations).not.toHaveBeenCalled(); }); it('should handle missing translations for supported locale', async () => { mockI18nService.isLocaleSupported.mockReturnValue(true); mockI18nService.getTranslations.mockReturnValue(null); const response = await request(app) .get('/api/i18n/en') .expect(404); expect(response.body).toEqual({ error: 'Translations not found for locale', locale: 'en' }); expect(mockI18nService.getTranslations).toHaveBeenCalledWith('en'); }); it('should handle special characters in locale parameter', async () => { mockI18nService.isLocaleSupported.mockReturnValue(false); await request(app) .get('/api/i18n/en-US@currency=USD') .expect(400); expect(mockI18nService.isLocaleSupported).toHaveBeenCalledWith('en-US@currency=USD'); }); it('should handle empty locale parameter', async () => { mockI18nService.isLocaleSupported.mockReturnValue(false); await request(app) .get('/api/i18n/') .expect(200); // This will hit the GET / route instead expect(mockI18nService.isLocaleSupported).not.toHaveBeenCalled(); }); }); describe('GET /api/i18n', () => { it('should return available locales with display names', async () => { const response = await request(app) .get('/api/i18n') .expect(200); expect(response.body).toEqual({ default: 'en', available: [ { code: 'en', name: 'English' }, { code: 'es-MX', name: 'Español (México)' } ] }); expect(mockI18nService.getAvailableLocales).toHaveBeenCalled(); expect(mockI18nService.getDefaultLocale).toHaveBeenCalled(); expect(mockI18nService.getLocaleDisplayName).toHaveBeenCalledWith('en'); expect(mockI18nService.getLocaleDisplayName).toHaveBeenCalledWith('es-MX'); }); it('should handle empty available locales', async () => { mockI18nService.getAvailableLocales.mockReturnValue([]); const response = await request(app) .get('/api/i18n') .expect(200); expect(response.body).toEqual({ default: 'en', available: [] }); }); it('should handle missing display names gracefully', async () => { mockI18nService.getAvailableLocales.mockReturnValue(['unknown']); mockI18nService.getLocaleDisplayName.mockReturnValue('unknown'); const response = await request(app) .get('/api/i18n') .expect(200); expect(response.body.available).toEqual([ { code: 'unknown', name: 'unknown' } ]); }); }); describe('GET /api/i18n/detect', () => { it('should detect locale from Accept-Language header', async () => { mockI18nService.detectLocale.mockReturnValue('es-MX'); const response = await request(app) .get('/api/i18n/detect') .set('Accept-Language', 'es-MX,es;q=0.9,en;q=0.8') .expect(200); expect(response.body).toEqual({ detected: 'es-MX', displayName: 'Español (México)' }); expect(mockI18nService.detectLocale).toHaveBeenCalledWith('es-MX,es;q=0.9,en;q=0.8'); expect(mockI18nService.getLocaleDisplayName).toHaveBeenCalledWith('es-MX'); }); it('should handle missing Accept-Language header', async () => { mockI18nService.detectLocale.mockReturnValue('en'); const response = await request(app) .get('/api/i18n/detect') .expect(200); expect(response.body).toEqual({ detected: 'en', displayName: 'English' }); expect(mockI18nService.detectLocale).toHaveBeenCalledWith(undefined); }); it('should handle complex Accept-Language header', async () => { mockI18nService.detectLocale.mockReturnValue('en'); const response = await request(app) .get('/api/i18n/detect') .set('Accept-Language', 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,*;q=0.5') .expect(200); expect(response.body.detected).toBe('en'); expect(mockI18nService.detectLocale).toHaveBeenCalledWith('fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,*;q=0.5'); }); it('should handle malformed Accept-Language header', async () => { mockI18nService.detectLocale.mockReturnValue('en'); const response = await request(app) .get('/api/i18n/detect') .set('Accept-Language', 'invalid;;;malformed,,,') .expect(200); expect(response.body.detected).toBe('en'); expect(mockI18nService.detectLocale).toHaveBeenCalledWith('invalid;;;malformed,,,'); }); it('should handle empty Accept-Language header', async () => { mockI18nService.detectLocale.mockReturnValue('en'); const response = await request(app) .get('/api/i18n/detect') .set('Accept-Language', '') .expect(200); expect(response.body.detected).toBe('en'); expect(mockI18nService.detectLocale).toHaveBeenCalledWith(''); }); }); describe('Route ordering and conflicts', () => { it('should prioritize specific routes over detect route', async () => { // Test that /api/i18n/detect doesn't conflict with /api/i18n/:locale mockI18nService.detectLocale.mockReturnValue('en'); const response = await request(app) .get('/api/i18n/detect') .expect(200); expect(response.body).toHaveProperty('detected'); expect(response.body).toHaveProperty('displayName'); expect(mockI18nService.detectLocale).toHaveBeenCalled(); }); it('should handle locale named "detect" correctly', async () => { // This should NOT call the detect endpoint mockI18nService.isLocaleSupported.mockReturnValue(false); await request(app) .get('/api/i18n/detect') .expect(200); // This hits the detect route, not the locale route expect(mockI18nService.isLocaleSupported).not.toHaveBeenCalled(); expect(mockI18nService.detectLocale).toHaveBeenCalled(); }); }); describe('HTTP headers and content type', () => { it('should set correct content-type for translations', async () => { mockI18nService.isLocaleSupported.mockReturnValue(true); mockI18nService.getTranslations.mockReturnValue({ test: 'data' }); const response = await request(app) .get('/api/i18n/en') .expect(200); expect(response.headers['content-type']).toMatch(/application\/json/); }); it('should set cache headers for translations', async () => { mockI18nService.isLocaleSupported.mockReturnValue(true); mockI18nService.getTranslations.mockReturnValue({ test: 'data' }); const response = await request(app) .get('/api/i18n/en') .expect(200); expect(response.headers['cache-control']).toBe('public, max-age=3600'); }); it('should not set cache headers for error responses', async () => { mockI18nService.isLocaleSupported.mockReturnValue(false); const response = await request(app) .get('/api/i18n/invalid') .expect(400); expect(response.headers['cache-control']).toBeUndefined(); }); }); });