import { NotFoundException } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Character } from '../characters/entities/character.entity'; import { BURNED_ROAD_ID, SOUTH_GATE_ID, } from '../database/seeds/vertical-slice.constants'; import { LocationMonster } from '../monsters/entities/location-monster.entity'; import { TravelService } from '../travel/travel.service'; import { LocationConnection } from './entities/location-connection.entity'; import { LocationDefinition } from './entities/location-definition.entity'; import { WorldService } from './world.service'; const CHARACTER_ID = '10000000-0000-4000-8000-000000000001'; function currentLocation(): LocationDefinition { return { id: SOUTH_GATE_ID, key: 'south-gate', name: 'S\u00fcdtor von Graufurt', description: 'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.', regionKey: 'ashen-fields', minRecommendedLevel: 1, maxRecommendedLevel: 1, dangerLevel: 0, isSafe: true, huntingEnabled: false, artworkPath: '/assets/locations/south-gate.webp', createdAt: new Date('2026-08-18T09:00:00.000Z'), updatedAt: new Date('2026-08-18T09:00:00.000Z'), characters: [], outgoingConnections: [], incomingConnections: [], }; } function burnedRoad(): LocationDefinition { return { id: BURNED_ROAD_ID, key: 'burned-road', name: 'Verbrannte Stra\u00dfe', description: 'Eine verbrannte Handelsroute durch die Aschenfelder.', regionKey: 'ashen-fields', minRecommendedLevel: 1, maxRecommendedLevel: 2, dangerLevel: 1, isSafe: false, huntingEnabled: true, artworkPath: '/assets/locations/burned-road.webp', createdAt: new Date('2026-08-18T09:00:00.000Z'), updatedAt: new Date('2026-08-18T09:00:00.000Z'), characters: [], outgoingConnections: [], incomingConnections: [], }; } describe('WorldService', () => { it('returns the authoritative current location and only enabled public connections', async () => { const callOrder: string[] = []; const location = currentLocation(); const completeTravelIfDue = jest.fn().mockImplementation(() => { callOrder.push('completeTravelIfDue'); return Promise.resolve({ status: 'IDLE' }); }); const travelService = { completeTravelIfDue, } as unknown as TravelService; const findCharacter = jest.fn().mockImplementation(() => { callOrder.push('findCharacter'); return Promise.resolve({ id: CHARACTER_ID, currentLocationId: SOUTH_GATE_ID, currentLocation: location, }); }); const characters = { findOne: findCharacter, } as unknown as Repository; const findConnections = jest.fn().mockResolvedValue([ { id: '30000000-0000-4000-8000-000000000001', fromLocationId: SOUTH_GATE_ID, toLocationId: BURNED_ROAD_ID, travelDurationSeconds: 10, ambushChance: '0.0500', enabled: true, fromLocation: location, toLocation: burnedRoad(), }, { id: '30000000-0000-4000-8000-000000000002', fromLocationId: SOUTH_GATE_ID, toLocationId: '20000000-0000-4000-8000-000000000003', travelDurationSeconds: 30, ambushChance: '0.9000', enabled: false, fromLocation: location, toLocation: burnedRoad(), }, ]); const connections = { find: findConnections, } as unknown as Repository; const findLocationMonsters = jest.fn(); const locationMonsters = { find: findLocationMonsters, } as unknown as Repository; const service = new WorldService( travelService, characters, connections, locationMonsters, ); const result = await service.getCurrentLocation(CHARACTER_ID); expect(callOrder).toEqual(['completeTravelIfDue', 'findCharacter']); expect(result).toEqual({ id: SOUTH_GATE_ID, key: 'south-gate', name: 'S\u00fcdtor von Graufurt', description: 'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.', regionKey: 'ashen-fields', minRecommendedLevel: 1, maxRecommendedLevel: 1, dangerLevel: 0, isSafe: true, huntingEnabled: false, artworkPath: '/assets/locations/south-gate.webp', connections: [ { targetLocation: { id: BURNED_ROAD_ID, key: 'burned-road', name: 'Verbrannte Stra\u00dfe', }, travelDurationSeconds: 10, danger: 'LOW', }, ], possibleMonsters: [], }); expect(findCharacter).toHaveBeenCalledWith({ where: { id: CHARACTER_ID }, relations: { currentLocation: true }, }); expect(findConnections).toHaveBeenCalledWith({ where: { fromLocationId: SOUTH_GATE_ID, enabled: true }, relations: { toLocation: true }, }); expect(findLocationMonsters).not.toHaveBeenCalled(); }); it('returns the enabled monster pool by name, ordered by weight descending, when hunting is enabled', async () => { const location = burnedRoad(); const travelService = { completeTravelIfDue: jest.fn().mockResolvedValue({ status: 'IDLE' }), } as unknown as TravelService; const characters = { findOne: jest.fn().mockResolvedValue({ id: CHARACTER_ID, currentLocationId: BURNED_ROAD_ID, currentLocation: location, }), } as unknown as Repository; const connections = { find: jest.fn().mockResolvedValue([]), } as unknown as Repository; const findLocationMonsters = jest .fn() .mockResolvedValue([ { monster: { name: 'Aschenratte' } }, { monster: { name: 'Stra\u00dfenr\u00e4uber' } }, ]); const locationMonsters = { find: findLocationMonsters, } as unknown as Repository; const service = new WorldService( travelService, characters, connections, locationMonsters, ); const result = await service.getCurrentLocation(CHARACTER_ID); expect(result.possibleMonsters).toEqual([ 'Aschenratte', 'Stra\u00dfenr\u00e4uber', ]); expect(findLocationMonsters).toHaveBeenCalledWith({ where: { locationId: BURNED_ROAD_ID, enabled: true }, relations: { monster: true }, order: { weight: 'DESC' }, }); }); it('reports a missing character after travel completion', async () => { const completeTravelIfDue = jest.fn().mockResolvedValue({ status: 'IDLE', }); const travelService = { completeTravelIfDue, } as unknown as TravelService; const findCharacter = jest.fn().mockResolvedValue(null); const characters = { findOne: findCharacter, } as unknown as Repository; const findConnections = jest.fn(); const connections = { find: findConnections, } as unknown as Repository; const findLocationMonsters = jest.fn(); const locationMonsters = { find: findLocationMonsters, } as unknown as Repository; const service = new WorldService( travelService, characters, connections, locationMonsters, ); await expect( service.getCurrentLocation(CHARACTER_ID), ).rejects.toBeInstanceOf(NotFoundException); expect(findConnections).not.toHaveBeenCalled(); expect(findLocationMonsters).not.toHaveBeenCalled(); }); });