feat(combat): add HTTP controllers for starting, reading, and acting on combats
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
72
apps/api/src/combat/combat.controller.spec.ts
Normal file
72
apps/api/src/combat/combat.controller.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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<App>;
|
||||||
|
const getCombat = jest.fn();
|
||||||
|
const performAction = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
getCombat.mockReset();
|
||||||
|
performAction.mockReset();
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
controllers: [CombatController],
|
||||||
|
providers: [{ provide: CombatService, useValue: { getCombat, performAction } }],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = module.createNestApplication<App>();
|
||||||
|
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: [] };
|
||||||
|
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 POST /api/combats/:combatId/actions with only the action field', async () => {
|
||||||
|
const combat = { id: 'combat-1', status: 'ACTIVE', round: 2, player: {}, monster: {}, events: [] };
|
||||||
|
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: 'HEAVY_STRIKE' })
|
||||||
|
.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();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
apps/api/src/combat/combat.controller.ts
Normal file
19
apps/api/src/combat/combat.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||||
|
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||||
|
import { CombatActionDto } from './dto/combat-action.dto';
|
||||||
|
import { CombatService } from './combat.service';
|
||||||
|
|
||||||
|
@Controller('combats')
|
||||||
|
export class CombatController {
|
||||||
|
constructor(private readonly combatService: CombatService) {}
|
||||||
|
|
||||||
|
@Get(':combatId')
|
||||||
|
getCombat(@Param('combatId') combatId: string) {
|
||||||
|
return this.combatService.getCombat(DEMO_CHARACTER_ID, combatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':combatId/actions')
|
||||||
|
performAction(@Param('combatId') combatId: string, @Body() dto: CombatActionDto) {
|
||||||
|
return this.combatService.performAction(DEMO_CHARACTER_ID, combatId, dto.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/api/src/combat/dto/combat-action.dto.ts
Normal file
7
apps/api/src/combat/dto/combat-action.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { IsEnum } from 'class-validator';
|
||||||
|
import { CombatAction } from '../combat-action.enum';
|
||||||
|
|
||||||
|
export class CombatActionDto {
|
||||||
|
@IsEnum(CombatAction)
|
||||||
|
action!: CombatAction;
|
||||||
|
}
|
||||||
41
apps/api/src/combat/hunt-encounter-attack.controller.spec.ts
Normal file
41
apps/api/src/combat/hunt-encounter-attack.controller.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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 { CombatService } from './combat.service';
|
||||||
|
import { HuntEncounterAttackController } from './hunt-encounter-attack.controller';
|
||||||
|
|
||||||
|
describe('HuntEncounterAttackController', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
const startCombat = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
startCombat.mockReset();
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
controllers: [HuntEncounterAttackController],
|
||||||
|
providers: [{ provide: CombatService, useValue: { startCombat } }],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = module.createNestApplication<App>();
|
||||||
|
configureApplication(app);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates to combatService.startCombat with the demo character id and the encounter id', async () => {
|
||||||
|
const combat = { id: 'combat-1', status: 'ACTIVE', round: 1, player: {}, monster: {}, events: [] };
|
||||||
|
startCombat.mockResolvedValue(combat);
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post('/api/hunt-encounters/encounter-1/attack')
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(startCombat).toHaveBeenCalledWith(DEMO_CHARACTER_ID, 'encounter-1');
|
||||||
|
expect(response.body).toEqual(combat);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
apps/api/src/combat/hunt-encounter-attack.controller.ts
Normal file
13
apps/api/src/combat/hunt-encounter-attack.controller.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Param, Post } from '@nestjs/common';
|
||||||
|
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||||
|
import { CombatService } from './combat.service';
|
||||||
|
|
||||||
|
@Controller('hunt-encounters')
|
||||||
|
export class HuntEncounterAttackController {
|
||||||
|
constructor(private readonly combatService: CombatService) {}
|
||||||
|
|
||||||
|
@Post(':encounterId/attack')
|
||||||
|
attack(@Param('encounterId') encounterId: string) {
|
||||||
|
return this.combatService.startCombat(DEMO_CHARACTER_ID, encounterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user