From 7f0a6d8df9f917072bfceee3bb07e8f47e075f90 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:50:30 +0100 Subject: [PATCH] add get player stat api --- src/docs/game-stat-api.docs.ts | 36 ++++++- src/policies/api/game-stat-api.policy.ts | 8 ++ src/services/api/game-stat-api.service.ts | 28 ++++++ .../_api/game-stat-api/getPlayerStat.test.ts | 99 +++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tests/services/_api/game-stat-api/getPlayerStat.test.ts diff --git a/src/docs/game-stat-api.docs.ts b/src/docs/game-stat-api.docs.ts index 45744dd9..69c759bf 100644 --- a/src/docs/game-stat-api.docs.ts +++ b/src/docs/game-stat-api.docs.ts @@ -71,7 +71,41 @@ const GameStatAPIDocs: APIDocs = { } ] }, - + getPlayerStat: { + description: 'Get the current value of a player\'s stat', + params: { + route: { + internalName: 'The internal name of the stat' + } + }, + samples: [ + { + title: 'Sample response', + sample: { + playerStat: { + id: 15, + stat: { + id: 4, + internalName: 'gold-collected', + name: 'Gold collected', + global: true, + globalValue: 5839, + defaultValue: 0, + maxChange: null, + minValue: 0, + maxValue: null, + minTimeBetweenUpdates: 5, + createdAt: '2021-12-24T12:45:39.409Z', + updatedAt: '2021-12-24T12:49:14.315Z' + }, + value: 52, + createdAt: '2025-06-19T06:18:11.881Z', + updatedAt: '2025-06-19T08:32:46.123Z' + } + } + } + ] + }, put: { description: 'Update a stat value', params: { diff --git a/src/policies/api/game-stat-api.policy.ts b/src/policies/api/game-stat-api.policy.ts index 1eac8ad4..f04b66e0 100644 --- a/src/policies/api/game-stat-api.policy.ts +++ b/src/policies/api/game-stat-api.policy.ts @@ -57,6 +57,14 @@ export default class GameStatAPIPolicy extends Policy { return this.hasScope(APIKeyScope.READ_GAME_STATS) } + async getPlayerStat(req: Request): Promise { + const [stat, alias] = await Promise.all([this.getStat(req), this.getAlias()]) + if (!stat) return new PolicyDenial({ message: 'Stat not found' }, 404) + if (!alias) return new PolicyDenial({ message: 'Player not found' }, 404) + + return this.hasScope(APIKeyScope.READ_GAME_STATS) + } + async put(req: Request): Promise { const [stat, alias] = await Promise.all([this.getStat(req), this.getAlias()]) if (!stat) return new PolicyDenial({ message: 'Stat not found' }, 404) diff --git a/src/services/api/game-stat-api.service.ts b/src/services/api/game-stat-api.service.ts index e43b8166..b642edd2 100644 --- a/src/services/api/game-stat-api.service.ts +++ b/src/services/api/game-stat-api.service.ts @@ -51,6 +51,34 @@ export default class GameStatAPIService extends APIService { } + @Route({ + method: 'GET', + path: '/:internalName/player-stat', + docs: GameStatAPIDocs.getPlayerStat + }) + @Validate({ + headers: ['x-talo-alias'] + }) + @HasPermission(GameStatAPIPolicy, 'getPlayerStat') + async getPlayerStat(req: Request) : Promise { + const em: EntityManager = req.ctx.em + + const stat: GameStat = req.ctx.state.stat + const alias: PlayerAlias = req.ctx.state.alias + const playerStat = await em.repo(PlayerGameStat).findOne({ + player: alias.player, + stat + }) + + return { + status: 200, + body: { + playerStat + } + } + + } + @Route({ method: 'PUT', path: '/:internalName', diff --git a/tests/services/_api/game-stat-api/getPlayerStat.test.ts b/tests/services/_api/game-stat-api/getPlayerStat.test.ts new file mode 100644 index 00000000..0312605c --- /dev/null +++ b/tests/services/_api/game-stat-api/getPlayerStat.test.ts @@ -0,0 +1,99 @@ +import request from 'supertest' +import { APIKeyScope } from '../../../../src/entities/api-key' +import PlayerFactory from '../../../fixtures/PlayerFactory' +import GameStatFactory from '../../../fixtures/GameStatFactory' +import createAPIKeyAndToken from '../../../utils/createAPIKeyAndToken' +import Game from '../../../../src/entities/game' +import PlayerGameStatFactory from '../../../fixtures/PlayerGameStatFactory' +import GameStat from '../../../../src/entities/game-stat' +import PlayerGameStat from '../../../../src/entities/player-game-stat' + +describe('Game stats API service - get player stat', () => { + const createStat = async (game: Game) => { + const stat = await new GameStatFactory([game]).state(() => ({ maxValue: 999, maxChange: 99 })).one() + em.persist(stat) + + return stat + } + + const createPlayerStat = async (stat: GameStat, extra: Partial = {}) => { + const player = await new PlayerFactory([stat.game]).one() + const playerStat = await new PlayerGameStatFactory().construct(player, stat).state(() => extra).one() + em.persist(playerStat) + + return playerStat + } + + it('should return a player stat if the scope is valid', async () => { + const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.READ_GAME_STATS]) + const stat = await createStat(apiKey.game) + const playerStat = await createPlayerStat(stat, { value: 42 }) + await em.flush() + + const res = await request(app) + .get(`/v1/game-stats/${stat.internalName}/player-stat`) + .set('x-talo-alias', String(playerStat.player.aliases[0].id)) + .auth(token, { type: 'bearer' }) + .expect(200) + + expect(res.body.playerStat).toBeDefined() + expect(res.body.playerStat.value).toBe(42) + expect(res.body.playerStat.stat.id).toBe(stat.id) + }) + + it('should return a null playerStat if the player has no stats', async () => { + const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.READ_GAME_STATS]) + const stat = await createStat(apiKey.game) + const player = await new PlayerFactory([apiKey.game]).one() + await em.persistAndFlush(player) + + const res = await request(app) + .get(`/v1/game-stats/${stat.internalName}/player-stat`) + .set('x-talo-alias', String(player.aliases[0].id)) + .auth(token, { type: 'bearer' }) + .expect(200) + + expect(res.body.playerStat).toBeNull() + }) + + it('should not return a player stat if the scope is not valid', async () => { + const [apiKey, token] = await createAPIKeyAndToken([]) + const stat = await createStat(apiKey.game) + const playerStat = await createPlayerStat(stat) + await em.flush() + + await request(app) + .get(`/v1/game-stats/${stat.internalName}/player-stat`) + .set('x-talo-alias', String(playerStat.player.aliases[0].id)) + .auth(token, { type: 'bearer' }) + .expect(403) + }) + + it('should return a 404 for a non-existent stat', async () => { + const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.READ_GAME_STATS]) + const player = await new PlayerFactory([apiKey.game]).one() + await em.persistAndFlush(player) + + const res = await request(app) + .get('/v1/game-stats/non-existent/player-stat') + .set('x-talo-alias', String(player.aliases[0].id)) + .auth(token, { type: 'bearer' }) + .expect(404) + + expect(res.body).toStrictEqual({ message: 'Stat not found' }) + }) + + it('should return a 404 for a non-existent alias', async () => { + const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.READ_GAME_STATS]) + const stat = await createStat(apiKey.game) + await em.flush() + + const res = await request(app) + .get(`/v1/game-stats/${stat.internalName}/player-stat`) + .set('x-talo-alias', '21312321') + .auth(token, { type: 'bearer' }) + .expect(404) + + expect(res.body).toStrictEqual({ message: 'Player not found' }) + }) +})