Skip to content

Commit

Permalink
feat(game-history): get game history endpoint (#360)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi committed Aug 1, 2023
1 parent 06e431f commit be65031
Show file tree
Hide file tree
Showing 8 changed files with 24,928 additions and 24,638 deletions.
12 changes: 12 additions & 0 deletions src/modules/game/controllers/game.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { GetGameRandomCompositionPlayerResponseDto } from "../dto/get-game-rando
import { GetGameRandomCompositionDto } from "../dto/get-game-random-composition/get-game-random-composition.dto";
import { MakeGamePlayDto } from "../dto/make-game-play/make-game-play.dto";
import { GAME_STATUSES } from "../enums/game.enum";
import { GameHistoryRecordService } from "../providers/services/game-history/game-history-record.service";
import { GameRandomCompositionService } from "../providers/services/game-random-composition.service";
import { GameService } from "../providers/services/game.service";
import { GameHistoryRecord } from "../schemas/game-history-record/game-history-record.schema";
import { Game } from "../schemas/game.schema";
import { ApiGameIdParam } from "./decorators/api-game-id-param.decorator";
import { ApiGameNotFoundResponse } from "./decorators/api-game-not-found-response.decorator";
Expand All @@ -19,6 +21,7 @@ export class GameController {
public constructor(
private readonly gameService: GameService,
private readonly gameRandomCompositionService: GameRandomCompositionService,
private readonly gameHistoryRecordService: GameHistoryRecordService,
) {}

@Get()
Expand Down Expand Up @@ -65,4 +68,13 @@ export class GameController {
private async makeGamePlay(@Param("id", GetGameByIdPipe) game: Game, @Body() makeGamePlayDto: MakeGamePlayDto): Promise<Game> {
return this.gameService.makeGamePlay(game, makeGamePlayDto);
}

@Get(":id/history")
@ApiOperation({ summary: "Get a game full history by id" })
@ApiGameIdParam()
@ApiResponse({ status: HttpStatus.OK, type: [GameHistoryRecord] })
@ApiGameNotFoundResponse()
private async getGameHistory(@Param("id", GetGameByIdPipe) game: Game): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordService.getGameHistory(game._id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import type { GameHistoryRecordToInsert } from "../../types/game-history-record.
export class GameHistoryRecordRepository {
public constructor(@InjectModel(GameHistoryRecord.name) private readonly gameHistoryRecordModel: Model<GameHistoryRecordDocument>) {}

public async getGameHistory(gameId: Types.ObjectId): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordModel.find({ gameId });
}

public async create(gameHistoryRecord: GameHistoryRecordToInsert): Promise<GameHistoryRecord> {
return this.gameHistoryRecordModel.create(gameHistoryRecord);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import type { Types } from "mongoose";
import { toJSON } from "../../../../../../tests/helpers/object/object.helper";
import { API_RESOURCES } from "../../../../../shared/api/enums/api.enum";
import { RESOURCE_NOT_FOUND_REASONS } from "../../../../../shared/exception/enums/resource-not-found-error.enum";
import { createNoCurrentGamePlayUnexpectedException } from "../../../../../shared/exception/helpers/unexpected-exception.factory";
Expand Down Expand Up @@ -85,7 +86,11 @@ export class GameHistoryRecordService {
if (gameHistoryRecordToInsert.play.votes) {
gameHistoryRecordToInsert.play.voting = this.generateCurrentGameHistoryRecordPlayVotingToInsert(baseGame as GameWithCurrentPlay, newGame, gameHistoryRecordToInsert);
}
return plainToInstance(GameHistoryRecordToInsert, gameHistoryRecordToInsert, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
return plainToInstance(GameHistoryRecordToInsert, gameHistoryRecordToInsert, plainToInstanceDefaultOptions);
}

public async getGameHistory(gameId: Types.ObjectId): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordRepository.getGameHistory(gameId);
}

private generateCurrentGameHistoryRecordDeadPlayersToInsert(baseGame: Game, newGame: Game): Player[] | undefined {
Expand Down Expand Up @@ -118,7 +123,7 @@ export class GameHistoryRecordService {
chosenCard: play.chosenCard,
chosenSide: play.chosenSide,
};
return plainToInstance(GameHistoryRecordPlay, gameHistoryRecordPlayToInsert, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
return plainToInstance(GameHistoryRecordPlay, toJSON(gameHistoryRecordPlayToInsert), plainToInstanceDefaultOptions);
}

private generateCurrentGameHistoryRecordPlayVotingResultToInsert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES } from "../../../../../../src/modul
import { GAME_PHASES, GAME_STATUSES } from "../../../../../../src/modules/game/enums/game.enum";
import { PLAYER_GROUPS } from "../../../../../../src/modules/game/enums/player.enum";
import { GameModule } from "../../../../../../src/modules/game/game.module";
import { GameHistoryRecord } from "../../../../../../src/modules/game/schemas/game-history-record/game-history-record.schema";
import type { GameOptions } from "../../../../../../src/modules/game/schemas/game-options/game-options.schema";
import { Game } from "../../../../../../src/modules/game/schemas/game.schema";
import type { Player } from "../../../../../../src/modules/game/schemas/player/player.schema";
Expand All @@ -27,6 +28,7 @@ import { createFakeGameOptionsDto } from "../../../../../factories/game/dto/crea
import { bulkCreateFakeCreateGamePlayerDto } from "../../../../../factories/game/dto/create-game/create-game-player/create-game-player.dto.factory";
import { createFakeCreateGameDto, createFakeCreateGameWithPlayersDto } from "../../../../../factories/game/dto/create-game/create-game.dto.factory";
import { createFakeMakeGamePlayDto } from "../../../../../factories/game/dto/make-game-play/make-game-play.dto.factory";
import { createFakeGameHistoryRecord } from "../../../../../factories/game/schemas/game-history-record/game-history-record.schema.factory";
import { createFakeGamePlayAllVote, createFakeGamePlaySeerLooks, createFakeGamePlayWerewolvesEat } from "../../../../../factories/game/schemas/game-play/game-play.schema.factory";
import { createFakeGame, createFakeGameWithCurrentPlay } from "../../../../../factories/game/schemas/game.schema.factory";
import { createFakeSeenBySeerPlayerAttribute } from "../../../../../factories/game/schemas/player/player-attribute/player-attribute.schema.factory";
Expand All @@ -39,7 +41,10 @@ import { initNestApp } from "../../../../helpers/nest-app.helper";

describe("Game Controller", () => {
let app: NestFastifyApplication;
let models: { game: Model<Game> };
let models: {
game: Model<Game>;
gameHistoryRecord: Model<GameHistoryRecord>;
};

beforeAll(async() => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -49,7 +54,10 @@ describe("Game Controller", () => {
],
}).compile();
app = module.createNestApplication<NestFastifyApplication>(new FastifyAdapter(fastifyServerDefaultOptions));
models = { game: module.get<Model<Game>>(getModelToken(Game.name)) };
models = {
game: module.get<Model<Game>>(getModelToken(Game.name)),
gameHistoryRecord: module.get<Model<GameHistoryRecord>>(getModelToken(GameHistoryRecord.name)),
};

await initNestApp(app);
});
Expand Down Expand Up @@ -733,4 +741,87 @@ describe("Game Controller", () => {
});
});
});

describe("GET /games/:id/history", () => {
afterEach(async() => {
await models.gameHistoryRecord.deleteMany();
});

it("should get a bad request error when id is not mongoId.", async() => {
const response = await app.inject({
method: "GET",
url: "/games/123/history",
});

expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
expect(response.json<BadRequestException>().message).toBe("Validation failed (Mongo ObjectId is expected)");
});

it("should get a not found error when id doesn't exist in base.", async() => {
const unknownId = faker.database.mongodbObjectId();
const response = await app.inject({
method: "GET",
url: `/games/${unknownId}/history`,
});

expect(response.statusCode).toBe(HttpStatus.NOT_FOUND);
expect(response.json<NotFoundException>().message).toBe(`Game with id "${unknownId}" not found`);
});

it("should return no game history records when game doesn't have any.", async() => {
const game = createFakeGameWithCurrentPlay();
const secondGame = createFakeGameWithCurrentPlay();
const gameHistoryRecords = [
createFakeGameHistoryRecord({ gameId: game._id }),
createFakeGameHistoryRecord({ gameId: game._id }),
createFakeGameHistoryRecord({ gameId: game._id }),
];
await models.game.insertMany([game, secondGame]);
await models.gameHistoryRecord.insertMany(gameHistoryRecords);

const response = await app.inject({
method: "GET",
url: `/games/${secondGame._id.toString()}/history`,
});

expect(response.statusCode).toBe(HttpStatus.OK);
expect(response.json<GameHistoryRecord[]>()).toStrictEqual([]);
});

it("should return 3 game history records when game have 3 records.", async() => {
const game = createFakeGameWithCurrentPlay();
const secondGame = createFakeGameWithCurrentPlay();
const gameHistoryRecords = [
createFakeGameHistoryRecord({ gameId: game._id }),
createFakeGameHistoryRecord({ gameId: game._id }),
createFakeGameHistoryRecord({ gameId: game._id }),
];
await models.game.insertMany([game, secondGame]);
await models.gameHistoryRecord.insertMany(gameHistoryRecords);

const response = await app.inject({
method: "GET",
url: `/games/${game._id.toString()}/history`,
});

expect(response.statusCode).toBe(HttpStatus.OK);
expect(response.json<GameHistoryRecord[]>()).toStrictEqual<GameHistoryRecord[]>([
{
...toJSON(gameHistoryRecords[0]),
createdAt: expect.any(String) as Date,
updatedAt: expect.any(String) as Date,
},
{
...toJSON(gameHistoryRecords[1]),
createdAt: expect.any(String) as Date,
updatedAt: expect.any(String) as Date,
},
{
...toJSON(gameHistoryRecords[2]),
createdAt: expect.any(String) as Date,
updatedAt: expect.any(String) as Date,
},
] as GameHistoryRecord[]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ describe("Game History Record Repository", () => {
return models.gameHistoryRecord.insertMany(gameHistoryRecords);
}

describe("getGameHistory", () => {
it("should get nothing when there are no record.", async() => {
const gameId = createFakeObjectId();

await expect(repositories.gameHistoryRecord.getGameHistory(gameId)).resolves.toStrictEqual<GameHistoryRecord[]>([]);
});

it("should get records when called with gameId with records.", async() => {
const gameId = createFakeObjectId();
const gameHistoryRecords = [
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWerewolvesEatPlay() }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay() }),
];
await populate(gameHistoryRecords);
const records = await repositories.gameHistoryRecord.getGameHistory(gameId);

expect(toJSON(records)).toStrictEqual<GameHistoryRecord[]>(toJSON(gameHistoryRecords) as GameHistoryRecord[]);
});
});

describe("create", () => {
it.each<{ toInsert: GameHistoryRecordToInsert; errorMessage: string; test: string }>([
{
Expand Down Expand Up @@ -113,7 +133,7 @@ describe("Game History Record Repository", () => {
describe("getLastGameHistoryGuardProtectsRecord", () => {
it("should return no record when there is no guard play in the history.", async() => {
const gameId = createFakeObjectId();
await models.gameHistoryRecord.insertMany([
await populate([
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWerewolvesEatPlay() }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay() }),
]);
Expand All @@ -124,7 +144,7 @@ describe("Game History Record Repository", () => {
it("should return no record when there gameId is not the good one.", async() => {
const gameId = createFakeObjectId();
const otherGameId = createFakeObjectId();
await models.gameHistoryRecord.insertMany([
await populate([
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordGuardProtectPlay() }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay() }),
]);
Expand All @@ -150,7 +170,7 @@ describe("Game History Record Repository", () => {
describe("getLastGameHistoryTieInVotesRecord", () => {
it("should return no record when there is no vote play in the history.", async() => {
const gameId = createFakeObjectId();
await models.gameHistoryRecord.insertMany([
await populate([
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWerewolvesEatPlay() }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay() }),
]);
Expand All @@ -161,7 +181,7 @@ describe("Game History Record Repository", () => {
it("should return no record when there is no tie in vote play in the history.", async() => {
const gameId = createFakeObjectId();
const gameHistoryRecordPlayTieVoting = createFakeGameHistoryRecordPlayVoting({ result: GAME_HISTORY_RECORD_VOTING_RESULTS.DEATH });
await models.gameHistoryRecord.insertMany([
await populate([
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordAllVotePlay({ voting: gameHistoryRecordPlayTieVoting }) }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordAllVotePlay() }),
]);
Expand All @@ -173,7 +193,7 @@ describe("Game History Record Repository", () => {
const gameId = createFakeObjectId();
const otherGameId = createFakeObjectId();
const gameHistoryRecordPlayTieVoting = createFakeGameHistoryRecordPlayVoting({ result: GAME_HISTORY_RECORD_VOTING_RESULTS.TIE });
await models.gameHistoryRecord.insertMany([
await populate([
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordGuardProtectPlay() }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordAllVotePlay({ voting: gameHistoryRecordPlayTieVoting }) }),
]);
Expand Down Expand Up @@ -415,27 +435,36 @@ describe("Game History Record Repository", () => {
const gameId = createFakeObjectId();
const otherGameId = createFakeObjectId();
const gameHistoryRecords = [
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordAllVotePlay({ didJudgeRequestAnotherVote: false }) }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordAllVotePlay({ didJudgeRequestAnotherVote: false }), createdAt: new Date("2023-01-01") }),
createFakeGameHistoryRecord({
gameId, play: createFakeGameHistoryRecordGuardProtectPlay({
gameId,
play: createFakeGameHistoryRecordGuardProtectPlay({
targets: [
createFakeGameHistoryRecordPlayTarget({ player: createFakeSeerAlivePlayer() }),
createFakeGameHistoryRecordPlayTarget({ player: createFakeAncientAlivePlayer() }),
],
}),
}),
createFakeGameHistoryRecord({ gameId: otherGameId, play: createFakeGameHistoryRecordGuardProtectPlay({ targets: [createFakeGameHistoryRecordPlayTarget({ player: createFakeAncientAlivePlayer() })] }) }),
createFakeGameHistoryRecord({ gameId, play: createFakeGameHistoryRecordGuardProtectPlay({ targets: [createFakeGameHistoryRecordPlayTarget({ player: createFakeSeerAlivePlayer() })] }) }),
createFakeGameHistoryRecord({
gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay({
gameId: otherGameId,
play: createFakeGameHistoryRecordGuardProtectPlay({ targets: [createFakeGameHistoryRecordPlayTarget({ player: createFakeAncientAlivePlayer() })] }),
}),
createFakeGameHistoryRecord({
gameId,
play: createFakeGameHistoryRecordGuardProtectPlay({ targets: [createFakeGameHistoryRecordPlayTarget({ player: createFakeSeerAlivePlayer() })] }),
}),
createFakeGameHistoryRecord({
gameId,
play: createFakeGameHistoryRecordWitchUsePotionsPlay({
targets: [
createFakeGameHistoryRecordPlayTarget({ player: createFakeSeerAlivePlayer(), drankPotion: WITCH_POTIONS.LIFE }),
createFakeGameHistoryRecordPlayTarget({ player: createFakeAncientAlivePlayer(), drankPotion: WITCH_POTIONS.DEATH }),
],
}),
}),
createFakeGameHistoryRecord({
gameId, play: createFakeGameHistoryRecordWitchUsePotionsPlay({
gameId,
play: createFakeGameHistoryRecordWitchUsePotionsPlay({
targets: [
createFakeGameHistoryRecordPlayTarget({ player: createFakeSeerAlivePlayer(), drankPotion: WITCH_POTIONS.DEATH }),
createFakeGameHistoryRecordPlayTarget({ player: createFakeAncientAlivePlayer(), drankPotion: WITCH_POTIONS.LIFE }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ function createFakeGameHistoryRecordSheriffSettleVotesPlay(gameHistoryRecordPlay
function createFakeGameHistoryRecordPlaySource(gameHistoryRecordPlaySource: Partial<GameHistoryRecordPlaySource> = {}, override: object = {}): GameHistoryRecordPlaySource {
return plainToInstance(GameHistoryRecordPlaySource, {
name: gameHistoryRecordPlaySource.name ?? faker.helpers.arrayElement(gameSourceValues),
players: gameHistoryRecordPlaySource.players ?? [],
players: gameHistoryRecordPlaySource.players ?? [createFakePlayer()],
...override,
}, plainToInstanceDefaultOptions);
}
Expand Down

0 comments on commit be65031

Please sign in to comment.