import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { App } from 'supertest/types'; import { configureApplication } from '../app.config'; import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants'; import { CombatController } from './combat.controller'; import { CombatService } from './combat.service'; describe('CombatController', () => { let app: INestApplication; const getCombat = jest.fn(); const getActiveCombat = jest.fn(); const performAction = jest.fn(); beforeEach(async () => { getCombat.mockReset(); getActiveCombat.mockReset(); performAction.mockReset(); const module = await Test.createTestingModule({ controllers: [CombatController], providers: [ { provide: CombatService, useValue: { getCombat, getActiveCombat, performAction }, }, ], }).compile(); app = module.createNestApplication(); configureApplication(app); await app.init(); }); afterEach(async () => { await app.close(); }); it('delegates GET /api/combats/:combatId to combatService.getCombat', async () => { const combat = { id: 'combat-1', status: 'ACTIVE', round: 1, player: {}, monster: {}, events: [], rewards: null, }; getCombat.mockResolvedValue(combat); const response = await request(app.getHttpServer()) .get('/api/combats/combat-1') .expect(200); expect(getCombat).toHaveBeenCalledWith(DEMO_CHARACTER_ID, 'combat-1'); expect(response.body).toEqual(combat); }); it('delegates GET /api/combats/active to combatService.getActiveCombat', async () => { const combat = { id: 'combat-1', status: 'ACTIVE', round: 3, player: {}, monster: {}, events: [], rewards: null, }; getActiveCombat.mockResolvedValue(combat); const response = await request(app.getHttpServer()) .get('/api/combats/active') .expect(200); expect(getActiveCombat).toHaveBeenCalledWith(DEMO_CHARACTER_ID); expect(getCombat).not.toHaveBeenCalled(); expect(response.body).toEqual(combat); }); it('returns an empty body from GET /api/combats/active when no combat is running', async () => { getActiveCombat.mockResolvedValue(null); const response = await request(app.getHttpServer()) .get('/api/combats/active') .expect(200); expect(getActiveCombat).toHaveBeenCalledWith(DEMO_CHARACTER_ID); expect(response.body).toEqual({}); expect(getCombat).not.toHaveBeenCalled(); }); it('delegates POST /api/combats/:combatId/actions with only the action field', async () => { const combat = { id: 'combat-1', status: 'ACTIVE', round: 2, player: {}, monster: {}, events: [], rewards: null, }; performAction.mockResolvedValue(combat); const response = await request(app.getHttpServer()) .post('/api/combats/combat-1/actions') .send({ action: 'ATTACK' }) .expect(201); expect(performAction).toHaveBeenCalledWith( DEMO_CHARACTER_ID, 'combat-1', 'ATTACK', ); expect(response.body).toEqual(combat); }); it('rejects an unknown action value', async () => { await request(app.getHttpServer()) .post('/api/combats/combat-1/actions') .send({ action: 'FLEE' }) .expect(400); expect(performAction).not.toHaveBeenCalled(); }); it('rejects server-owned combat fields the client must never send', async () => { await request(app.getHttpServer()) .post('/api/combats/combat-1/actions') .send({ action: 'ATTACK', damage: 999, playerHp: 1, monsterHp: 1, round: 99, }) .expect(400); expect(performAction).not.toHaveBeenCalled(); }); });