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 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-21 13:19:17 +02:00
parent 4e3af9fe39
commit 6fb61792e7

View File

@@ -1,3 +1,4 @@
import { HttpException } from '@nestjs/common';
import { DataSource, EntityManager, EntityTarget } from 'typeorm'; import { DataSource, EntityManager, EntityTarget } from 'typeorm';
import { Character } from '../characters/entities/character.entity'; import { Character } from '../characters/entities/character.entity';
import { CharacterItem } from '../items/entities/character-item.entity'; import { CharacterItem } from '../items/entities/character-item.entity';
@@ -168,6 +169,30 @@ async function expectTurnInDomainError(promise: Promise<unknown>, code: string):
expect(error.code).toBe(code); 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<unknown>,
code: string,
status: number,
): Promise<void> {
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('TurnInService', () => {
describe('turnIn', () => { describe('turnIn', () => {
it('consumes the exact quantity, grants silver and reputation atomically', async () => { it('consumes the exact quantity, grants silver and reputation atomically', async () => {
@@ -232,6 +257,19 @@ describe('TurnInService', () => {
expect(state.characters[0].silver).toBe(10); 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 () => { it('rejects a disabled turn-in', async () => {
const state = createState({ turnIns: [turnInDefinition({ enabled: false })] }); const state = createState({ turnIns: [turnInDefinition({ enabled: false })] });
const { service } = createService(state); const { service } = createService(state);