From 6fb61792e7fcfa2b9d148a3194fb5ab661b27cb2 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Fri, 21 Aug 2026 13:19:17 +0200 Subject: [PATCH] test(turn-in): cover the unknown-character rejection path TurnInService locks and validates the character row before touching inventory, silver, or reputation, but no test exercised that branch. Both sibling services in this slice (renown, reputation) have the equivalent test; this closes the gap. `characterNotFound` is shared from travel.errors and so is not a TurnInDomainError -- assert on the wire contract (code + status) the way reputation.service.spec.ts does for the same shared error. Co-Authored-By: Claude Opus 5 --- apps/api/src/turn-in/turn-in.service.spec.ts | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/apps/api/src/turn-in/turn-in.service.spec.ts b/apps/api/src/turn-in/turn-in.service.spec.ts index 698f7d0..8bf9405 100644 --- a/apps/api/src/turn-in/turn-in.service.spec.ts +++ b/apps/api/src/turn-in/turn-in.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpException } from '@nestjs/common'; import { DataSource, EntityManager, EntityTarget } from 'typeorm'; import { Character } from '../characters/entities/character.entity'; import { CharacterItem } from '../items/entities/character-item.entity'; @@ -168,6 +169,30 @@ async function expectTurnInDomainError(promise: Promise, code: string): expect(error.code).toBe(code); } +/** + * `characterNotFound` is shared from `travel.errors`, so it is not a + * TurnInDomainError. Assert on the wire contract -- code plus status -- + * the way `reputation.service.spec.ts` does for the same shared error. + */ +async function expectHttpErrorWithCode( + promise: Promise, + code: string, + status: number, +): Promise { + let error: unknown; + try { + await promise; + } catch (cause) { + error = cause; + } + expect(error).toBeInstanceOf(HttpException); + if (!(error instanceof HttpException)) { + throw new Error('Expected HttpException'); + } + expect(error.getStatus()).toBe(status); + expect(error.getResponse()).toMatchObject({ code }); +} + describe('TurnInService', () => { describe('turnIn', () => { it('consumes the exact quantity, grants silver and reputation atomically', async () => { @@ -232,6 +257,19 @@ describe('TurnInService', () => { expect(state.characters[0].silver).toBe(10); }); + it('rejects an unknown character, mutating nothing', async () => { + const { service, state } = createService(createState()); + + await expectHttpErrorWithCode( + service.turnIn('unknown-character', 'ash-pelt-border-guard', 1), + 'CHARACTER_NOT_FOUND', + 404, + ); + expect(state.characterItems[0].quantity).toBe(5); + expect(state.characters[0].silver).toBe(10); + expect(state.characterReputation).toHaveLength(0); + }); + it('rejects a disabled turn-in', async () => { const state = createState({ turnIns: [turnInDefinition({ enabled: false })] }); const { service } = createService(state);