diff --git a/src/controllers/api/2014/magicItemController.ts b/src/controllers/api/2014/magicItemController.ts index dcb68d88..1144bc82 100644 --- a/src/controllers/api/2014/magicItemController.ts +++ b/src/controllers/api/2014/magicItemController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express' import MagicItem from '@/models/2014/magicItem' import { NameQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' +import { applyTranslation, applyTranslationToList } from '@/util/translation' interface IndexQuery { name?: { $regex: RegExp } @@ -18,6 +19,7 @@ export const index = async (req: Request, res: Response, next: NextFunction) => .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name } = validatedQuery.data + const lang = req.lang ?? 'en' const searchQueries: IndexQuery = {} if (name !== undefined) { @@ -25,16 +27,23 @@ export const index = async (req: Request, res: Response, next: NextFunction) => } const redisKey = req.originalUrl - const data = await redisClient.get(redisKey) + const cached = await redisClient.get(redisKey) - if (data !== null && data !== undefined && data !== '') { - res.status(200).json(JSON.parse(data)) + if (cached !== null && cached !== undefined && cached !== '') { + return res.status(200).json(JSON.parse(cached)) } else { const data = await MagicItem.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) - const jsonData = ResourceList(data) + const plain = data.map((d: any) => d.toObject?.() ?? d) + const { docs: translated, wasTranslated } = await applyTranslationToList( + plain, + '2014-magic-items', + lang + ) + const jsonData = ResourceList(translated) redisClient.set(redisKey, JSON.stringify(jsonData)) + res.setHeader('Content-Language', wasTranslated ? lang : 'en') return res.status(200).json(jsonData) } } catch (err) { @@ -52,10 +61,15 @@ export const show = async (req: Request, res: Response, next: NextFunction) => { .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data + const lang = req.lang ?? 'en' const data = await MagicItem.findOne({ index: index }) if (data === null || data === undefined) return next() - return res.status(200).json(data) + + const plain = (data.toObject?.() ?? data) as unknown as Record + const translated = await applyTranslation(plain, '2014-magic-items', lang) + res.setHeader('Content-Language', translated !== plain ? lang : 'en') + return res.status(200).json(translated) } catch (err) { next(err) } diff --git a/src/controllers/api/2014/monsterController.ts b/src/controllers/api/2014/monsterController.ts index a0b3734e..1db8ea14 100644 --- a/src/controllers/api/2014/monsterController.ts +++ b/src/controllers/api/2014/monsterController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express' import Monster from '@/models/2014/monster' import { MonsterIndexQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' +import { applyTranslation, applyTranslationToList } from '@/util/translation' interface IndexQuery { name?: { $regex: RegExp } @@ -19,6 +20,7 @@ export const index = async (req: Request, res: Response, next: NextFunction) => .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, challenge_rating } = validatedQuery.data + const lang = req.lang ?? 'en' const searchQueries: IndexQuery = {} if (name !== undefined) { @@ -29,16 +31,23 @@ export const index = async (req: Request, res: Response, next: NextFunction) => } const redisKey = req.originalUrl - const data = await redisClient.get(redisKey) + const cached = await redisClient.get(redisKey) - if (data !== null) { - res.status(200).json(JSON.parse(data)) + if (cached !== null) { + return res.status(200).json(JSON.parse(cached)) } else { const data = await Monster.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) - const jsonData = ResourceList(data) + const plain = data.map((d: any) => d.toObject?.() ?? d) + const { docs: translated, wasTranslated } = await applyTranslationToList( + plain, + '2014-monsters', + lang + ) + const jsonData = ResourceList(translated) redisClient.set(redisKey, JSON.stringify(jsonData)) + res.setHeader('Content-Language', wasTranslated ? lang : 'en') return res.status(200).json(jsonData) } } catch (err) { @@ -56,10 +65,15 @@ export const show = async (req: Request, res: Response, next: NextFunction) => { .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data + const lang = req.lang ?? 'en' const data = await Monster.findOne({ index: index }) if (!data) return next() - return res.status(200).json(data) + + const plain = (data.toObject?.() ?? data) as unknown as Record + const translated = await applyTranslation(plain, '2014-monsters', lang) + res.setHeader('Content-Language', translated !== plain ? lang : 'en') + return res.status(200).json(translated) } catch (err) { next(err) } diff --git a/src/controllers/api/2014/ruleController.ts b/src/controllers/api/2014/ruleController.ts index 92fd6ce9..568a547f 100644 --- a/src/controllers/api/2014/ruleController.ts +++ b/src/controllers/api/2014/ruleController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express' import Rule from '@/models/2014/rule' import { NameDescQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' +import { applyTranslation, applyTranslationToList } from '@/util/translation' interface IndexQuery { name?: { $regex: RegExp } @@ -19,6 +20,7 @@ export const index = async (req: Request, res: Response, next: NextFunction) => .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, desc } = validatedQuery.data + const lang = req.lang ?? 'en' const searchQueries: IndexQuery = {} if (name !== undefined) { @@ -29,16 +31,23 @@ export const index = async (req: Request, res: Response, next: NextFunction) => } const redisKey = req.originalUrl - const data = await redisClient.get(redisKey) + const cached = await redisClient.get(redisKey) - if (data !== null) { - res.status(200).json(JSON.parse(data)) + if (cached !== null) { + return res.status(200).json(JSON.parse(cached)) } else { const data = await Rule.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) - const jsonData = ResourceList(data) + const plain = data.map((d: any) => d.toObject?.() ?? d) + const { docs: translated, wasTranslated } = await applyTranslationToList( + plain, + '2014-rules', + lang + ) + const jsonData = ResourceList(translated) redisClient.set(redisKey, JSON.stringify(jsonData)) + res.setHeader('Content-Language', wasTranslated ? lang : 'en') return res.status(200).json(jsonData) } } catch (err) { @@ -56,10 +65,15 @@ export const show = async (req: Request, res: Response, next: NextFunction) => { .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data + const lang = req.lang ?? 'en' const data = await Rule.findOne({ index: index }) if (!data) return next() - return res.status(200).json(data) + + const plain = (data.toObject?.() ?? data) as unknown as Record + const translated = await applyTranslation(plain, '2014-rules', lang) + res.setHeader('Content-Language', translated !== plain ? lang : 'en') + return res.status(200).json(translated) } catch (err) { next(err) } diff --git a/src/controllers/api/2014/ruleSectionController.ts b/src/controllers/api/2014/ruleSectionController.ts index de4a9e8b..ad99002d 100644 --- a/src/controllers/api/2014/ruleSectionController.ts +++ b/src/controllers/api/2014/ruleSectionController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express' import RuleSection from '@/models/2014/ruleSection' import { NameDescQuerySchema, ShowParamsSchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' +import { applyTranslation, applyTranslationToList } from '@/util/translation' interface IndexQuery { name?: { $regex: RegExp } @@ -19,6 +20,7 @@ export const index = async (req: Request, res: Response, next: NextFunction) => .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, desc } = validatedQuery.data + const lang = req.lang ?? 'en' const searchQueries: IndexQuery = {} if (name !== undefined) { @@ -29,16 +31,23 @@ export const index = async (req: Request, res: Response, next: NextFunction) => } const redisKey = req.originalUrl - const data = await redisClient.get(redisKey) + const cached = await redisClient.get(redisKey) - if (data !== null) { - res.status(200).json(JSON.parse(data)) + if (cached !== null) { + return res.status(200).json(JSON.parse(cached)) } else { const data = await RuleSection.find(searchQueries) .select({ index: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) - const jsonData = ResourceList(data) + const plain = data.map((d: any) => d.toObject?.() ?? d) + const { docs: translated, wasTranslated } = await applyTranslationToList( + plain, + '2014-rule-sections', + lang + ) + const jsonData = ResourceList(translated) redisClient.set(redisKey, JSON.stringify(jsonData)) + res.setHeader('Content-Language', wasTranslated ? lang : 'en') return res.status(200).json(jsonData) } } catch (err) { @@ -56,10 +65,15 @@ export const show = async (req: Request, res: Response, next: NextFunction) => { .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data + const lang = req.lang ?? 'en' const data = await RuleSection.findOne({ index: index }) if (!data) return next() - return res.status(200).json(data) + + const plain = (data.toObject?.() ?? data) as unknown as Record + const translated = await applyTranslation(plain, '2014-rule-sections', lang) + res.setHeader('Content-Language', translated !== plain ? lang : 'en') + return res.status(200).json(translated) } catch (err) { next(err) } diff --git a/src/controllers/api/2014/spellController.ts b/src/controllers/api/2014/spellController.ts index 2d4f7227..cef3b69a 100644 --- a/src/controllers/api/2014/spellController.ts +++ b/src/controllers/api/2014/spellController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express' import Spell from '@/models/2014/spell' import { ShowParamsSchema, SpellIndexQuerySchema } from '@/schemas/schemas' import { escapeRegExp, redisClient, ResourceList } from '@/util' +import { applyTranslation, applyTranslationToList } from '@/util/translation' interface IndexQuery { name?: { $regex: RegExp } @@ -19,6 +20,7 @@ export const index = async (req: Request, res: Response, next: NextFunction) => .json({ error: 'Invalid query parameters', details: validatedQuery.error.issues }) } const { name, level, school } = validatedQuery.data + const lang = req.lang ?? 'en' const searchQueries: IndexQuery = {} if (name !== undefined) { @@ -35,16 +37,23 @@ export const index = async (req: Request, res: Response, next: NextFunction) => } const redisKey = req.originalUrl - const data = await redisClient.get(redisKey) + const cached = await redisClient.get(redisKey) - if (data !== null) { - res.status(200).json(JSON.parse(data)) + if (cached !== null) { + return res.status(200).json(JSON.parse(cached)) } else { const data = await Spell.find(searchQueries) .select({ index: 1, level: 1, name: 1, url: 1, _id: 0 }) .sort({ index: 'asc' }) - const jsonData = ResourceList(data) + const plain = data.map((d: any) => d.toObject?.() ?? d) + const { docs: translated, wasTranslated } = await applyTranslationToList( + plain, + '2014-spells', + lang + ) + const jsonData = ResourceList(translated) redisClient.set(redisKey, JSON.stringify(jsonData)) + res.setHeader('Content-Language', wasTranslated ? lang : 'en') return res.status(200).json(jsonData) } } catch (err) { @@ -61,10 +70,15 @@ export const show = async (req: Request, res: Response, next: NextFunction) => { .json({ error: 'Invalid path parameters', details: validatedParams.error.issues }) } const { index } = validatedParams.data + const lang = req.lang ?? 'en' const data = await Spell.findOne({ index: index }) if (!data) return next() - return res.status(200).json(data) + + const plain = (data.toObject?.() ?? data) as unknown as Record + const translated = await applyTranslation(plain, '2014-spells', lang) + res.setHeader('Content-Language', translated !== plain ? lang : 'en') + return res.status(200).json(translated) } catch (err) { next(err) } diff --git a/src/tests/controllers/api/2014/magicItemController.test.ts b/src/tests/controllers/api/2014/magicItemController.test.ts index 3af1f26b..8a9bbde8 100644 --- a/src/tests/controllers/api/2014/magicItemController.test.ts +++ b/src/tests/controllers/api/2014/magicItemController.test.ts @@ -3,8 +3,9 @@ import { describe, expect, it, vi } from 'vitest' import * as MagicItemController from '@/controllers/api/2014/magicItemController' import MagicItemModel from '@/models/2014/magicItem' +import Translation2014Model from '@/models/2014/translation' import { magicItemFactory } from '@/tests/factories/2014/magicItem.factory' -import { mockNext as defaultMockNext } from '@/tests/support' // Assuming mockNext is here based on spell test +import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, @@ -21,6 +22,7 @@ const dbUri = generateUniqueDbUri('magicitem') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MagicItemModel) +setupModelCleanup(Translation2014Model) describe('MagicItemController', () => { describe('index', () => { @@ -107,7 +109,47 @@ describe('MagicItemController', () => { expect(mockNext).not.toHaveBeenCalled() }) - // TODO: Add tests for query parameters (e.g., name) if applicable + it('returns translated names and Content-Language header when translations exist', async () => { + const itemData = magicItemFactory.build({ index: 'bag-of-holding', name: 'Bag of Holding' }) + await MagicItemModel.insertMany([itemData]) + await Translation2014Model.insertMany([ + { + source_index: 'bag-of-holding', + source_collection: 'magic-items', + lang: 'fr-FR', + fields: { name: 'Sac sans fond' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ query: {} }) + request.lang = 'fr-FR' + const response = createResponse() + + await MagicItemController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.results[0].name).toBe('Sac sans fond') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns Content-Language: en when no translations exist for lang', async () => { + const itemsData = magicItemFactory.buildList(2) + await MagicItemModel.insertMany(itemsData) + + const request = createRequest({ query: {} }) + request.lang = 'de-DE' + const response = createResponse() + + await MagicItemController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) describe('show', () => { @@ -154,6 +196,51 @@ describe('MagicItemController', () => { expect(mockNext).toHaveBeenCalledWith() // Called with no arguments for default 404 handling }) + it('returns translated fields and Content-Language header when a translation exists', async () => { + const itemData = magicItemFactory.build({ index: 'bag-of-holding', name: 'Bag of Holding' }) + await MagicItemModel.insertMany([itemData]) + await Translation2014Model.insertMany([ + { + source_index: 'bag-of-holding', + source_collection: 'magic-items', + lang: 'fr-FR', + fields: { name: 'Sac sans fond', desc: ['Description en français'] }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ params: { index: 'bag-of-holding' } }) + request.lang = 'fr-FR' + const response = createResponse() + + await MagicItemController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Sac sans fond') + expect(responseData.desc).toEqual(['Description en français']) + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns original content and Content-Language: en when no translation exists for lang', async () => { + const itemData = magicItemFactory.build({ index: 'bag-of-holding', name: 'Bag of Holding' }) + await MagicItemModel.insertMany([itemData]) + + const request = createRequest({ params: { index: 'bag-of-holding' } }) + request.lang = 'de-DE' + const response = createResponse() + + await MagicItemController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Bag of Holding') + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) + it('handles database errors during findOne', async () => { // Arrange const request = createRequest({ params: { index: 'any-index' } }) diff --git a/src/tests/controllers/api/2014/monsterController.test.ts b/src/tests/controllers/api/2014/monsterController.test.ts index ebfefb21..3f24cf1b 100644 --- a/src/tests/controllers/api/2014/monsterController.test.ts +++ b/src/tests/controllers/api/2014/monsterController.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import * as MonsterController from '@/controllers/api/2014/monsterController' import MonsterModel from '@/models/2014/monster' +import Translation2014Model from '@/models/2014/translation' import { monsterFactory } from '@/tests/factories/2014/monster.factory' import { mockNext as defaultMockNext } from '@/tests/support' // DB Helper Imports @@ -13,9 +14,6 @@ import { teardownIsolatedDatabase } from '@/tests/support/db' -// Remove redis mock - Integration tests will hit the real DB -// vi.mock('@/util', ...) - const mockNext = vi.fn(defaultMockNext) // Setup DB isolation @@ -23,6 +21,7 @@ const dbUri = generateUniqueDbUri('monster') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(MonsterModel) +setupModelCleanup(Translation2014Model) // Removed createMockQuery helper @@ -136,7 +135,92 @@ describe('MonsterController', () => { expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) - // No need for explicit DB error mocking - // describe('when something goes wrong', ...) + it('returns translated fields and Content-Language header when a translation exists', async () => { + const monsterData = monsterFactory.build({ index: 'goblin', name: 'Goblin' }) + await MonsterModel.insertMany([monsterData]) + await Translation2014Model.insertMany([ + { + source_index: 'goblin', + source_collection: 'monsters', + lang: 'fr-FR', + fields: { name: 'Gobelin' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ params: { index: 'goblin' } }) + request.lang = 'fr-FR' + const response = createResponse() + + await MonsterController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Gobelin') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns original content and Content-Language: en when no translation exists for lang', async () => { + const monsterData = monsterFactory.build({ index: 'goblin', name: 'Goblin' }) + await MonsterModel.insertMany([monsterData]) + + const request = createRequest({ params: { index: 'goblin' } }) + request.lang = 'de-DE' + const response = createResponse() + + await MonsterController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Goblin') + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) + }) + + describe('index (translation)', () => { + it('returns translated names and Content-Language header when translations exist', async () => { + const monsterData = monsterFactory.build({ index: 'goblin', name: 'Goblin' }) + await MonsterModel.insertMany([monsterData]) + await Translation2014Model.insertMany([ + { + source_index: 'goblin', + source_collection: 'monsters', + lang: 'fr-FR', + fields: { name: 'Gobelin' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ query: {}, originalUrl: '/api/monsters' }) + request.lang = 'fr-FR' + const response = createResponse() + + await MonsterController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.results[0].name).toBe('Gobelin') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns Content-Language: en when no translations exist for lang', async () => { + const monstersData = monsterFactory.buildList(2) + await MonsterModel.insertMany(monstersData) + + const request = createRequest({ query: {}, originalUrl: '/api/monsters' }) + request.lang = 'de-DE' + const response = createResponse() + + await MonsterController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) }) diff --git a/src/tests/controllers/api/2014/ruleSectionController.test.ts b/src/tests/controllers/api/2014/ruleSectionController.test.ts index 9cf1d594..a98b5b9b 100644 --- a/src/tests/controllers/api/2014/ruleSectionController.test.ts +++ b/src/tests/controllers/api/2014/ruleSectionController.test.ts @@ -2,9 +2,10 @@ import { createRequest, createResponse } from 'node-mocks-http' import { describe, expect, it, vi } from 'vitest' import * as RuleSectionController from '@/controllers/api/2014/ruleSectionController' -import RuleSectionModel from '@/models/2014/ruleSection' // Use Model suffix -import { ruleSectionFactory } from '@/tests/factories/2014/ruleSection.factory' // Updated path -import { mockNext as defaultMockNext } from '@/tests/support' // Assuming support helper location +import RuleSectionModel from '@/models/2014/ruleSection' +import Translation2014Model from '@/models/2014/translation' +import { ruleSectionFactory } from '@/tests/factories/2014/ruleSection.factory' +import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, setupIsolatedDatabase, @@ -21,6 +22,7 @@ const dbUri = generateUniqueDbUri('rulesection') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(RuleSectionModel) +setupModelCleanup(Translation2014Model) describe('RuleSectionController', () => { describe('index', () => { @@ -106,5 +108,93 @@ describe('RuleSectionController', () => { expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() // Default 404 handling }) + + it('returns translated fields and Content-Language header when a translation exists', async () => { + const sectionData = ruleSectionFactory.build({ index: 'adventuring', name: 'Adventuring' }) + await RuleSectionModel.insertMany([sectionData]) + await Translation2014Model.insertMany([ + { + source_index: 'adventuring', + source_collection: 'rule-sections', + lang: 'fr-FR', + fields: { name: 'Aventure' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ params: { index: 'adventuring' } }) + request.lang = 'fr-FR' + const response = createResponse() + + await RuleSectionController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Aventure') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns original content and Content-Language: en when no translation exists for lang', async () => { + const sectionData = ruleSectionFactory.build({ index: 'adventuring', name: 'Adventuring' }) + await RuleSectionModel.insertMany([sectionData]) + + const request = createRequest({ params: { index: 'adventuring' } }) + request.lang = 'de-DE' + const response = createResponse() + + await RuleSectionController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Adventuring') + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) + }) + + describe('index (translation)', () => { + it('returns translated names and Content-Language header when translations exist', async () => { + const sectionData = ruleSectionFactory.build({ index: 'adventuring', name: 'Adventuring' }) + await RuleSectionModel.insertMany([sectionData]) + await Translation2014Model.insertMany([ + { + source_index: 'adventuring', + source_collection: 'rule-sections', + lang: 'fr-FR', + fields: { name: 'Aventure' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ query: {} }) + request.lang = 'fr-FR' + const response = createResponse() + + await RuleSectionController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.results[0].name).toBe('Aventure') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns Content-Language: en when no translations exist for lang', async () => { + const sectionsData = ruleSectionFactory.buildList(2) + await RuleSectionModel.insertMany(sectionsData) + + const request = createRequest({ query: {} }) + request.lang = 'de-DE' + const response = createResponse() + + await RuleSectionController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) }) diff --git a/src/tests/controllers/api/2014/rulesController.test.ts b/src/tests/controllers/api/2014/rulesController.test.ts index 12c24f97..73173f63 100644 --- a/src/tests/controllers/api/2014/rulesController.test.ts +++ b/src/tests/controllers/api/2014/rulesController.test.ts @@ -3,8 +3,9 @@ import { describe, expect, it, vi } from 'vitest' // Import specific functions from the correct controller file import * as RuleController from '@/controllers/api/2014/ruleController' -import RuleModel from '@/models/2014/rule' // Use Model suffix -import { ruleFactory } from '@/tests/factories/2014/rule.factory' // Updated path +import RuleModel from '@/models/2014/rule' +import Translation2014Model from '@/models/2014/translation' +import { ruleFactory } from '@/tests/factories/2014/rule.factory' import { mockNext as defaultMockNext } from '@/tests/support' import { generateUniqueDbUri, @@ -22,6 +23,7 @@ const dbUri = generateUniqueDbUri('rule') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(RuleModel) +setupModelCleanup(Translation2014Model) describe('RuleController', () => { // Updated describe block name @@ -106,5 +108,93 @@ describe('RuleController', () => { expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) + + it('returns translated fields and Content-Language header when a translation exists', async () => { + const ruleData = ruleFactory.build({ index: 'combat', name: 'Combat' }) + await RuleModel.insertMany([ruleData]) + await Translation2014Model.insertMany([ + { + source_index: 'combat', + source_collection: 'rules', + lang: 'fr-FR', + fields: { name: 'Combat (fr)' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ params: { index: 'combat' } }) + request.lang = 'fr-FR' + const response = createResponse() + + await RuleController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Combat (fr)') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns original content and Content-Language: en when no translation exists for lang', async () => { + const ruleData = ruleFactory.build({ index: 'combat', name: 'Combat' }) + await RuleModel.insertMany([ruleData]) + + const request = createRequest({ params: { index: 'combat' } }) + request.lang = 'de-DE' + const response = createResponse() + + await RuleController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Combat') + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) + }) + + describe('index (translation)', () => { + it('returns translated names and Content-Language header when translations exist', async () => { + const ruleData = ruleFactory.build({ index: 'combat', name: 'Combat' }) + await RuleModel.insertMany([ruleData]) + await Translation2014Model.insertMany([ + { + source_index: 'combat', + source_collection: 'rules', + lang: 'fr-FR', + fields: { name: 'Combat (fr)' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ query: {} }) + request.lang = 'fr-FR' + const response = createResponse() + + await RuleController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.results[0].name).toBe('Combat (fr)') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns Content-Language: en when no translations exist for lang', async () => { + const rulesData = ruleFactory.buildList(2) + await RuleModel.insertMany(rulesData) + + const request = createRequest({ query: {} }) + request.lang = 'de-DE' + const response = createResponse() + + await RuleController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) }) diff --git a/src/tests/controllers/api/2014/spellController.test.ts b/src/tests/controllers/api/2014/spellController.test.ts index eb529fc5..b9c88017 100644 --- a/src/tests/controllers/api/2014/spellController.test.ts +++ b/src/tests/controllers/api/2014/spellController.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import * as SpellController from '@/controllers/api/2014/spellController' import SpellModel from '@/models/2014/spell' +import Translation2014Model from '@/models/2014/translation' import { spellFactory } from '@/tests/factories/2014/spell.factory' import { mockNext as defaultMockNext } from '@/tests/support' // Import the DB helper functions @@ -22,6 +23,7 @@ const dbUri = generateUniqueDbUri('spell') setupIsolatedDatabase(dbUri) teardownIsolatedDatabase() setupModelCleanup(SpellModel) +setupModelCleanup(Translation2014Model) describe('SpellController', () => { describe('index', () => { @@ -91,6 +93,48 @@ describe('SpellController', () => { expect(responseData.results).toEqual([]) expect(mockNext).not.toHaveBeenCalled() }) + + it('returns translated names and Content-Language header when translations exist', async () => { + const spellData = spellFactory.build({ index: 'fireball', name: 'Fireball' }) + await SpellModel.insertMany([spellData]) + await Translation2014Model.insertMany([ + { + source_index: 'fireball', + source_collection: 'spells', + lang: 'fr-FR', + fields: { name: 'Boule de Feu' }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ query: {} }) + request.lang = 'fr-FR' + const response = createResponse() + + await SpellController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.results[0].name).toBe('Boule de Feu') + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns Content-Language: en when no translations exist for lang', async () => { + const spellsData = spellFactory.buildList(2) + await SpellModel.insertMany(spellsData) + + const request = createRequest({ query: {} }) + request.lang = 'de-DE' + const response = createResponse() + + await SpellController.index(request, response, mockNext) + + expect(response.statusCode).toBe(200) + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) describe('show', () => { @@ -129,5 +173,50 @@ describe('SpellController', () => { expect(mockNext).toHaveBeenCalledOnce() expect(mockNext).toHaveBeenCalledWith() }) + + it('returns translated fields and Content-Language header when a translation exists', async () => { + const spellData = spellFactory.build({ index: 'fireball', name: 'Fireball' }) + await SpellModel.insertMany([spellData]) + await Translation2014Model.insertMany([ + { + source_index: 'fireball', + source_collection: 'spells', + lang: 'fr-FR', + fields: { name: 'Boule de Feu', desc: ['Description en français'] }, + completeness: 1.0, + updated_at: new Date().toISOString() + } + ]) + + const request = createRequest({ params: { index: 'fireball' } }) + request.lang = 'fr-FR' + const response = createResponse() + + await SpellController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Boule de Feu') + expect(responseData.desc).toEqual(['Description en français']) + expect(response.getHeader('Content-Language')).toBe('fr-FR') + expect(mockNext).not.toHaveBeenCalled() + }) + + it('returns original content and Content-Language: en when no translation exists for lang', async () => { + const spellData = spellFactory.build({ index: 'fireball', name: 'Fireball' }) + await SpellModel.insertMany([spellData]) + + const request = createRequest({ params: { index: 'fireball' } }) + request.lang = 'de-DE' + const response = createResponse() + + await SpellController.show(request, response, mockNext) + + expect(response.statusCode).toBe(200) + const responseData = JSON.parse(response._getData()) + expect(responseData.name).toBe('Fireball') + expect(response.getHeader('Content-Language')).toBe('en') + expect(mockNext).not.toHaveBeenCalled() + }) }) })