From 43d04c5c4e570578605ba5a9915584934c33a353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 19 Nov 2024 14:42:25 +0100 Subject: [PATCH 1/2] chore: fix web build outputs --- turbo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbo.json b/turbo.json index aa8e2d962..c38b2d9de 100644 --- a/turbo.json +++ b/turbo.json @@ -40,7 +40,7 @@ "vite.config.js", "tsconfig.json" ], - "outputs": ["src/**/*.{cjs,css,js,jsx}", "vite.config.js"] + "outputs": ["dist/*", "dist/**"] }, "@cornie-js/web-ui#format": { "dependsOn": ["^build"], From 159e544651ea1831a060f79e50a5793e9b57b31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 19 Nov 2024 14:43:26 +0100 Subject: [PATCH 2/2] docs: update PATCH /v1/games/{gameId} with missing response codes updated endpoint with 400 and 422 codes --- .../generated/HttpClientEndpoints.ts | 2 + .../schemas/src/one-game.yaml | 12 + .../resolvers/GameMutationResolver.spec.ts | 541 +++++++++--------- .../resolvers/GameMutationResolver.ts | 10 + 4 files changed, 290 insertions(+), 275 deletions(-) diff --git a/packages/api/libraries/api-http-client/src/httpClient/generated/HttpClientEndpoints.ts b/packages/api/libraries/api-http-client/src/httpClient/generated/HttpClientEndpoints.ts index d330b2ee0..7e5e8c7bc 100644 --- a/packages/api/libraries/api-http-client/src/httpClient/generated/HttpClientEndpoints.ts +++ b/packages/api/libraries/api-http-client/src/httpClient/generated/HttpClientEndpoints.ts @@ -116,9 +116,11 @@ export class HttpClientEndpoints { body: apiModels.GameIdUpdateQueryV1, ): Promise< | Response, apiModels.GameV1, 200> + | Response, apiModels.ErrorV1, 400> | Response, apiModels.ErrorV1, 401> | Response, apiModels.ErrorV1, 403> | Response, apiModels.ErrorV1, 404> + | Response, apiModels.ErrorV1, 422> > { return this.#internalHttpClient.callEndpoint({ body: body, diff --git a/packages/api/libraries/api-openapi-schema/schemas/src/one-game.yaml b/packages/api/libraries/api-openapi-schema/schemas/src/one-game.yaml index f398c7d1c..2e686b4d0 100644 --- a/packages/api/libraries/api-openapi-schema/schemas/src/one-game.yaml +++ b/packages/api/libraries/api-openapi-schema/schemas/src/one-game.yaml @@ -233,6 +233,12 @@ paths: application/json: schema: $ref: 'https://onegame.schemas/api/v1/games/game.json' + '400': + description: Bad request + content: + application/json: + schema: + $ref: 'https://onegame.schemas/api/v1/errors/error.json' '401': description: Unauthorized content: @@ -251,6 +257,12 @@ paths: application/json: schema: $ref: 'https://onegame.schemas/api/v1/errors/error.json' + '422': + description: Unprocessable operation + content: + application/json: + schema: + $ref: 'https://onegame.schemas/api/v1/errors/error.json' security: - accessToken: [] tags: diff --git a/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.spec.ts b/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.spec.ts index b51e958b3..2073fca91 100644 --- a/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.spec.ts +++ b/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.spec.ts @@ -453,14 +453,15 @@ describe(GameMutationResolver.name, () => { }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an UNAUTHORIZED response', () => { - let errorV1: apiModels.ErrorV1; + describe('when called, and httpClient.endpoints.updateGame() returns a NOT_FOUND response', () => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; let result: unknown; beforeAll(async () => { - errorV1 = { + errorV1Fixture = { description: 'error description fixture', }; @@ -475,25 +476,21 @@ describe(GameMutationResolver.name, () => { } as Partial as Context; httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, + body: errorV1Fixture, headers: {}, - statusCode: HttpStatus.UNAUTHORIZED, + statusCode: HttpStatus.NOT_FOUND, }); - try { - await gameMutationResolver.drawGameCards( - undefined, - { - gameDrawCardsInput: { - slotIndex: slotIndexFixture, - }, - gameId: gameIdFixture, + result = await gameMutationResolver.drawGameCards( + undefined, + { + gameDrawCardsInput: { + slotIndex: slotIndexFixture, }, - contextFixture, - ); - } catch (error: unknown) { - result = error; - } + gameId: gameIdFixture, + }, + contextFixture, + ); }); afterAll(() => { @@ -516,94 +513,94 @@ describe(GameMutationResolver.name, () => { ); }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.missingCredentials, - message: errorV1.description, - }; - - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); + it('should return null', () => { + expect(result).toBeNull(); }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an FORBIDDEN response', () => { - let errorV1: apiModels.ErrorV1; - let contextFixture: Context; + describe.each<[400 | 401 | 403 | 422, AppErrorKind]>([ + [HttpStatus.BAD_REQUEST, AppErrorKind.contractViolation], + [HttpStatus.UNAUTHORIZED, AppErrorKind.missingCredentials], + [HttpStatus.FORBIDDEN, AppErrorKind.invalidCredentials], + [HttpStatus.UNPROCESSABLE_ENTITY, AppErrorKind.unprocessableOperation], + ])( + 'when called, and httpClient.endpoints.updateGame() returns a %s response', + (httpStatus: 400 | 401 | 403 | 422, appErrorKind: AppErrorKind) => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; - let result: unknown; + let result: unknown; - beforeAll(async () => { - errorV1 = { - description: 'error description fixture', - }; + beforeAll(async () => { + errorV1Fixture = { + description: 'error description fixture', + }; - contextFixture = { - request: { - headers: { - foo: 'bar', + contextFixture = { + request: { + headers: { + foo: 'bar', + }, + query: {}, + urlParameters: {}, }, - query: {}, - urlParameters: {}, - }, - } as Partial as Context; + } as Partial as Context; - httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, - headers: {}, - statusCode: HttpStatus.FORBIDDEN, + httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ + body: errorV1Fixture, + headers: {}, + statusCode: httpStatus, + }); + + try { + await gameMutationResolver.drawGameCards( + undefined, + { + gameDrawCardsInput: { + slotIndex: slotIndexFixture, + }, + gameId: gameIdFixture, + }, + contextFixture, + ); + } catch (error: unknown) { + result = error; + } }); - try { - await gameMutationResolver.drawGameCards( - undefined, + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call httpClient.endpoints.updateGame()', () => { + const expectedBody: apiModels.GameIdDrawCardsQueryV1 = { + kind: 'drawCards', + slotIndex: slotIndexFixture, + }; + + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( + contextFixture.request.headers, { - gameDrawCardsInput: { - slotIndex: slotIndexFixture, - }, gameId: gameIdFixture, }, - contextFixture, + expectedBody, ); - } catch (error: unknown) { - result = error; - } - }); - - afterAll(() => { - jest.clearAllMocks(); - }); - - it('should call httpClient.endpoints.updateGame()', () => { - const expectedBody: apiModels.GameIdDrawCardsQueryV1 = { - kind: 'drawCards', - slotIndex: slotIndexFixture, - }; - - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( - contextFixture.request.headers, - { - gameId: gameIdFixture, - }, - expectedBody, - ); - }); + }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.invalidCredentials, - message: errorV1.description, - }; + it('should throw an AppError', () => { + const expectedErrorProperties: Partial = { + kind: appErrorKind, + message: errorV1Fixture.description, + }; - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); - }); - }); + expect(result).toBeInstanceOf(AppError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }, + ); }); describe('.passGameTurn', () => { @@ -691,14 +688,15 @@ describe(GameMutationResolver.name, () => { }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an UNAUTHORIZED response', () => { - let errorV1: apiModels.ErrorV1; + describe('when called, and httpClient.endpoints.updateGame() returns a NOT_FOUND response', () => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; let result: unknown; beforeAll(async () => { - errorV1 = { + errorV1Fixture = { description: 'error description fixture', }; @@ -713,25 +711,21 @@ describe(GameMutationResolver.name, () => { } as Partial as Context; httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, + body: errorV1Fixture, headers: {}, - statusCode: HttpStatus.UNAUTHORIZED, + statusCode: HttpStatus.NOT_FOUND, }); - try { - await gameMutationResolver.passGameTurn( - undefined, - { - gameId: gameIdFixture, - gamePassTurnInput: { - slotIndex: slotIndexFixture, - }, + result = await gameMutationResolver.passGameTurn( + undefined, + { + gameId: gameIdFixture, + gamePassTurnInput: { + slotIndex: slotIndexFixture, }, - contextFixture, - ); - } catch (error: unknown) { - result = error; - } + }, + contextFixture, + ); }); afterAll(() => { @@ -754,94 +748,94 @@ describe(GameMutationResolver.name, () => { ); }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.missingCredentials, - message: errorV1.description, - }; - - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); + it('should return null', () => { + expect(result).toBeNull(); }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an FORBIDDEN response', () => { - let errorV1: apiModels.ErrorV1; - let contextFixture: Context; + describe.each<[400 | 401 | 403 | 422, AppErrorKind]>([ + [HttpStatus.BAD_REQUEST, AppErrorKind.contractViolation], + [HttpStatus.UNAUTHORIZED, AppErrorKind.missingCredentials], + [HttpStatus.FORBIDDEN, AppErrorKind.invalidCredentials], + [HttpStatus.UNPROCESSABLE_ENTITY, AppErrorKind.unprocessableOperation], + ])( + 'when called, and httpClient.endpoints.updateGame() returns a %s response', + (httpStatus: 400 | 401 | 403 | 422, appErrorKind: AppErrorKind) => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; - let result: unknown; + let result: unknown; - beforeAll(async () => { - errorV1 = { - description: 'error description fixture', - }; + beforeAll(async () => { + errorV1Fixture = { + description: 'error description fixture', + }; - contextFixture = { - request: { - headers: { - foo: 'bar', + contextFixture = { + request: { + headers: { + foo: 'bar', + }, + query: {}, + urlParameters: {}, }, - query: {}, - urlParameters: {}, - }, - } as Partial as Context; + } as Partial as Context; - httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, - headers: {}, - statusCode: HttpStatus.FORBIDDEN, + httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ + body: errorV1Fixture, + headers: {}, + statusCode: httpStatus, + }); + + try { + await gameMutationResolver.passGameTurn( + undefined, + { + gameId: gameIdFixture, + gamePassTurnInput: { + slotIndex: slotIndexFixture, + }, + }, + contextFixture, + ); + } catch (error: unknown) { + result = error; + } }); - try { - await gameMutationResolver.passGameTurn( - undefined, + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call httpClient.endpoints.updateGame()', () => { + const expectedBody: apiModels.GameIdPassTurnQueryV1 = { + kind: 'passTurn', + slotIndex: slotIndexFixture, + }; + + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( + contextFixture.request.headers, { gameId: gameIdFixture, - gamePassTurnInput: { - slotIndex: slotIndexFixture, - }, }, - contextFixture, + expectedBody, ); - } catch (error: unknown) { - result = error; - } - }); - - afterAll(() => { - jest.clearAllMocks(); - }); - - it('should call httpClient.endpoints.updateGame()', () => { - const expectedBody: apiModels.GameIdPassTurnQueryV1 = { - kind: 'passTurn', - slotIndex: slotIndexFixture, - }; - - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( - contextFixture.request.headers, - { - gameId: gameIdFixture, - }, - expectedBody, - ); - }); + }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.invalidCredentials, - message: errorV1.description, - }; + it('should throw an AppError', () => { + const expectedErrorProperties: Partial = { + kind: appErrorKind, + message: errorV1Fixture.description, + }; - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); - }); - }); + expect(result).toBeInstanceOf(AppError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }, + ); }); describe('.playGameCards', () => { @@ -934,14 +928,15 @@ describe(GameMutationResolver.name, () => { }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an UNAUTHORIZED response', () => { - let errorV1: apiModels.ErrorV1; + describe('when called, and httpClient.endpoints.updateGame() returns a NOT_FOUND response', () => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; let result: unknown; beforeAll(async () => { - errorV1 = { + errorV1Fixture = { description: 'error description fixture', }; @@ -956,27 +951,23 @@ describe(GameMutationResolver.name, () => { } as Partial as Context; httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, + body: errorV1Fixture, headers: {}, - statusCode: HttpStatus.UNAUTHORIZED, + statusCode: HttpStatus.NOT_FOUND, }); - try { - await gameMutationResolver.playGameCards( - undefined, - { - gameId: gameIdFixture, - gamePlayCardsInput: { - cardIndexes: cardIndexesFixture, - colorChoice: null, - slotIndex: slotIndexFixture, - }, + result = await gameMutationResolver.playGameCards( + undefined, + { + gameId: gameIdFixture, + gamePlayCardsInput: { + cardIndexes: cardIndexesFixture, + colorChoice: null, + slotIndex: slotIndexFixture, }, - contextFixture, - ); - } catch (error: unknown) { - result = error; - } + }, + contextFixture, + ); }); afterAll(() => { @@ -1000,96 +991,96 @@ describe(GameMutationResolver.name, () => { ); }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.missingCredentials, - message: errorV1.description, - }; - - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); + it('should return null', () => { + expect(result).toBeNull(); }); }); - describe('when called, and httpClient.endpoints.updateGame() returns an FORBIDDEN response', () => { - let errorV1: apiModels.ErrorV1; - let contextFixture: Context; + describe.each<[400 | 401 | 403 | 422, AppErrorKind]>([ + [HttpStatus.BAD_REQUEST, AppErrorKind.contractViolation], + [HttpStatus.UNAUTHORIZED, AppErrorKind.missingCredentials], + [HttpStatus.FORBIDDEN, AppErrorKind.invalidCredentials], + [HttpStatus.UNPROCESSABLE_ENTITY, AppErrorKind.unprocessableOperation], + ])( + 'when called, and httpClient.endpoints.updateGame() returns a %s response', + (httpStatus: 400 | 401 | 403 | 422, appErrorKind: AppErrorKind) => { + let errorV1Fixture: apiModels.ErrorV1; + let contextFixture: Context; - let result: unknown; + let result: unknown; - beforeAll(async () => { - errorV1 = { - description: 'error description fixture', - }; + beforeAll(async () => { + errorV1Fixture = { + description: 'error description fixture', + }; - contextFixture = { - request: { - headers: { - foo: 'bar', + contextFixture = { + request: { + headers: { + foo: 'bar', + }, + query: {}, + urlParameters: {}, }, - query: {}, - urlParameters: {}, - }, - } as Partial as Context; + } as Partial as Context; - httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ - body: errorV1, - headers: {}, - statusCode: HttpStatus.FORBIDDEN, + httpClientMock.endpoints.updateGame.mockResolvedValueOnce({ + body: errorV1Fixture, + headers: {}, + statusCode: httpStatus, + }); + + try { + await gameMutationResolver.playGameCards( + undefined, + { + gameId: gameIdFixture, + gamePlayCardsInput: { + cardIndexes: cardIndexesFixture, + colorChoice: null, + slotIndex: slotIndexFixture, + }, + }, + contextFixture, + ); + } catch (error: unknown) { + result = error; + } }); - try { - await gameMutationResolver.playGameCards( - undefined, + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call httpClient.endpoints.updateGame()', () => { + const expectedBody: apiModels.GameIdPlayCardsQueryV1 = { + cardIndexes: cardIndexesFixture, + kind: 'playCards', + slotIndex: slotIndexFixture, + }; + + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); + expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( + contextFixture.request.headers, { gameId: gameIdFixture, - gamePlayCardsInput: { - cardIndexes: cardIndexesFixture, - colorChoice: null, - slotIndex: slotIndexFixture, - }, }, - contextFixture, + expectedBody, ); - } catch (error: unknown) { - result = error; - } - }); - - afterAll(() => { - jest.clearAllMocks(); - }); - - it('should call httpClient.endpoints.updateGame()', () => { - const expectedBody: apiModels.GameIdPlayCardsQueryV1 = { - cardIndexes: cardIndexesFixture, - kind: 'playCards', - slotIndex: slotIndexFixture, - }; - - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledTimes(1); - expect(httpClientMock.endpoints.updateGame).toHaveBeenCalledWith( - contextFixture.request.headers, - { - gameId: gameIdFixture, - }, - expectedBody, - ); - }); + }); - it('should throw an AppError', () => { - const expectedErrorProperties: Partial = { - kind: AppErrorKind.invalidCredentials, - message: errorV1.description, - }; + it('should throw an AppError', () => { + const expectedErrorProperties: Partial = { + kind: appErrorKind, + message: errorV1Fixture.description, + }; - expect(result).toBeInstanceOf(AppError); - expect(result).toStrictEqual( - expect.objectContaining(expectedErrorProperties), - ); - }); - }); + expect(result).toBeInstanceOf(AppError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }, + ); }); }); diff --git a/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.ts b/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.ts index 79f28bdfe..ac7dc5f44 100644 --- a/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.ts +++ b/packages/backend/apps/gateway/backend-gateway-application/src/games/application/resolvers/GameMutationResolver.ts @@ -197,6 +197,11 @@ export class GameMutationResolver switch (httpResponse.statusCode) { case 200: return this.#gameGraphQlFromGameV1Builder.build(httpResponse.body); + case 400: + throw new AppError( + AppErrorKind.contractViolation, + httpResponse.body.description, + ); case 401: throw new AppError( AppErrorKind.missingCredentials, @@ -209,6 +214,11 @@ export class GameMutationResolver ); case 404: return null; + case 422: + throw new AppError( + AppErrorKind.unprocessableOperation, + httpResponse.body.description, + ); } } }