diff --git a/cornucopia.owasp.org/src/lib/services/deckService.test.ts b/cornucopia.owasp.org/src/lib/services/deckService.test.ts index 034f10b15..106f16682 100644 --- a/cornucopia.owasp.org/src/lib/services/deckService.test.ts +++ b/cornucopia.owasp.org/src/lib/services/deckService.test.ts @@ -624,6 +624,60 @@ suits: consoleLogSpy.mockRestore(); }); + + it('should warn and return empty map if card file is missing', () => { + vi.mocked(FileSystemHelper.hasFile).mockReturnValue(false); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const deckService = new DeckService(); + const result = deckService.getCardDataForEditionVersionLang('webapp', '2.2', 'en'); + + expect(result.size).toBe(0); + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); +}); + +it('should handle generic markdown read error gracefully', () => { + vi.mocked(FileSystemHelper.hasFile).mockReturnValue(true); + vi.mocked(FileSystemHelper.hasDir).mockReturnValue(true); + + const mockYamlContent = ` +suits: + - id: suit1 + name: Test Suit + cards: + - id: CARD-1 + value: A + desc: Card 1 +`; + + // First call → YAML + // Second call → throw error (simulate markdown failure) + vi.mocked(fs.readFileSync) + .mockReturnValueOnce(mockYamlContent) + .mockImplementationOnce(() => { + throw new Error('Markdown read failed'); + }); + + const mockMapping = { + suits: { + '0': { name: 'Test Suit' } + } + }; + vi.mocked(MappingService.prototype.getCardMapping).mockReturnValue(mockMapping as any); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const deckService = new DeckService(); + const result = deckService.getCardDataForEditionVersionLang('webapp', '2.2', 'en'); + + expect(result.size).toBe(0); + expect(consoleErrorSpy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); +}); }, 10000); describe('clear', () => { diff --git a/cornucopia.owasp.org/src/lib/services/deckService.ts b/cornucopia.owasp.org/src/lib/services/deckService.ts index 1fcddb32f..23d9fbdb4 100644 --- a/cornucopia.owasp.org/src/lib/services/deckService.ts +++ b/cornucopia.owasp.org/src/lib/services/deckService.ts @@ -90,6 +90,7 @@ export class DeckService { let cardFile = `${__dirname}${DeckService.path}${edition}-cards-${version}-${lang}.yaml`; if (!FileSystemHelper.hasFile(cardFile)) { + console.warn(`Card file not found: ${cardFile}`); return cards; } @@ -126,15 +127,19 @@ export class DeckService { try { file = fs.readFileSync(path, 'utf8'); } catch (e) { - console.error(`Error reading file at path: ${path}`, e); + console.error( + `Error reading markdown file for card ${cardObject?.id || "unknown"} at ${path}`,e + ); continue; } let parsed = fm(file); cardObject.concept = parsed.body; + const explanationPath = `./${base}${cardFolderPath}/explanation.md`; try { - cardObject.summary = fm(fs.readFileSync(`./${base}${cardFolderPath}/explanation.md`, 'utf8')).body; + cardObject.summary = fm(fs.readFileSync(explanationPath, 'utf8')).body; } catch (e) { - console.error(`Error reading file at path: ./${base}${cardFolderPath}/explanation.md`, e); + console.error( + `Missing explanation.md for card ${cardObject?.id || "unknown"} at ${explanationPath}`,e); continue; } diff --git a/cornucopia.owasp.org/src/routes/api/mapping/[edition]/[version]/server.test.ts b/cornucopia.owasp.org/src/routes/api/mapping/[edition]/[version]/server.test.ts index 942b1a7d3..3599f5350 100644 --- a/cornucopia.owasp.org/src/routes/api/mapping/[edition]/[version]/server.test.ts +++ b/cornucopia.owasp.org/src/routes/api/mapping/[edition]/[version]/server.test.ts @@ -72,6 +72,36 @@ describe('GET /api/mapping/[edition]/[version]', () => { expect(body.suits).toBeUndefined(); }); + it('skips invalid suits and duplicate or invalid cards while transforming suits', async () => { + vi.spyOn(DeckService, 'hasEdition').mockReturnValue(true); + vi.spyOn(DeckService, 'hasVersion').mockReturnValue(true); + vi.spyOn(MappingService.prototype, 'getCardMapping').mockReturnValue({ + suits: [ + null, + {}, + { + cards: [ + null, + { id: '' }, + { id: 'VE2', value: '2' }, + { id: 'VE2', value: 'duplicate' } + ] + } + ] + } as any); + + const response = await GET({ + params: { edition: 'webapp', version: '3.0' } + } as any); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.meta).toBeUndefined(); + expect(body.cards).toEqual({ + VE2: { id: 'VE2', value: '2' } + }); + }); + it('throws 404 when edition is invalid', () => { vi.spyOn(DeckService, 'hasEdition').mockReturnValue(false); vi.spyOn(DeckService, 'getLatestEditions').mockReturnValue(['webapp', 'mobileapp']); diff --git a/cornucopia.owasp.org/vite.config.ts b/cornucopia.owasp.org/vite.config.ts index 632c5f44d..f48b82199 100644 --- a/cornucopia.owasp.org/vite.config.ts +++ b/cornucopia.owasp.org/vite.config.ts @@ -12,8 +12,8 @@ let vitePluginRestartOptions = {restart: ['./data/**']} // This copies the content from the filesystem data folder to the static file location under '/data/' available at runtime. // Also copies to server output for prerendering let viteStaticCopyTargets = [ - {src: './data/**/*', dest: './data/'}, - {src: './data/**/*', dest: '../server/data/'} + {src: './data/**/*', dest: './'}, + {src: './data/**/*', dest: '../server/'} ] let viteStaticCopyOptions = { targets: viteStaticCopyTargets}