diff --git a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts index 7e30c5668c7..5ab212a5cae 100644 --- a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts +++ b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.spec.ts @@ -1,7 +1,5 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; import { AcceptConsentRequestBody, AcceptLoginRequestBody, @@ -12,11 +10,15 @@ import { ProviderRedirectResponse, RejectRequestBody, } from '@infra/oauth-provider/dto'; +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; -import { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'; -import { of } from 'rxjs'; +import { AxiosError, AxiosHeaders, AxiosRequestConfig, AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; + +import { of, throwError } from 'rxjs'; +import { ProviderConsentSessionResponse } from '../dto'; +import { HydraOauthLoggableException } from '../loggable'; import { HydraAdapter } from './hydra.adapter'; -import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; import resetAllMocks = jest.resetAllMocks; class HydraAdapterSpec extends HydraAdapter { @@ -66,100 +68,66 @@ describe('HydraService', () => { }); describe('request', () => { - it('should return data when called with all parameters', async () => { - const data: { test: string } = { - test: 'data', - }; - - httpService.request.mockReturnValue(of(createAxiosResponse(data))); - - const result: { test: string } = await service.requestSpec( - 'GET', - 'testUrl', - { dataKey: 'dataValue' }, - { headerKey: 'headerValue' } - ); + describe('when called with all parameters', () => { + const setup = () => { + const data: { test: string } = { + test: 'data', + }; - expect(result).toEqual(data); - expect(httpService.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'testUrl', - method: 'GET', - headers: { - 'X-Forwarded-Proto': 'https', - headerKey: 'headerValue', - }, - data: { dataKey: 'dataValue' }, - }) - ); - }); + httpService.request.mockReturnValue(of(createAxiosResponse(data))); - it('should return data when called with only necessary parameters', async () => { - const data: { test: string } = { - test: 'data', + return { + data, + }; }; - httpService.request.mockReturnValue(of(createAxiosResponse(data))); - - const result: { test: string } = await service.requestSpec('GET', 'testUrl'); - - expect(result).toEqual(data); - expect(httpService.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'testUrl', - method: 'GET', - headers: { - 'X-Forwarded-Proto': 'https', - }, - }) - ); - }); - }); - - describe('Client Flow', () => { - describe('listOAuth2Clients', () => { - it('should list all oauth2 clients', async () => { - const data: ProviderOauthClient[] = [ - { - client_id: 'client1', - }, - { - client_id: 'client2', - }, - ]; - - httpService.request.mockReturnValue(of(createAxiosResponse(data))); + it('should return data', async () => { + const { data } = setup(); - const result: ProviderOauthClient[] = await service.listOAuth2Clients(); + const result: { test: string } = await service.requestSpec( + 'GET', + 'testUrl', + { dataKey: 'dataValue' }, + { headerKey: 'headerValue' } + ); expect(result).toEqual(data); expect(httpService.request).toHaveBeenCalledWith( expect.objectContaining({ - url: `${hydraUri}/clients`, + url: 'testUrl', method: 'GET', headers: { 'X-Forwarded-Proto': 'https', + headerKey: 'headerValue', }, + data: { dataKey: 'dataValue' }, }) ); }); + }); - it('should list all oauth2 clients within parameters', async () => { - const data: ProviderOauthClient[] = [ - { - client_id: 'client1', - owner: 'clientOwner', - }, - ]; + describe('when called with only necessary parameters', () => { + const setup = () => { + const data: { test: string } = { + test: 'data', + }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); - const result: ProviderOauthClient[] = await service.listOAuth2Clients(1, 0, 'client1', 'clientOwner'); + return { + data, + }; + }; + + it('should return data', async () => { + const { data } = setup(); + + const result: { test: string } = await service.requestSpec('GET', 'testUrl'); expect(result).toEqual(data); expect(httpService.request).toHaveBeenCalledWith( expect.objectContaining({ - url: `${hydraUri}/clients?limit=1&offset=0&client_name=client1&owner=clientOwner`, + url: 'testUrl', method: 'GET', headers: { 'X-Forwarded-Proto': 'https', @@ -169,13 +137,129 @@ describe('HydraService', () => { }); }); + describe('when error occurs', () => { + const setup = () => { + const axiosError: AxiosError = { + isAxiosError: true, + name: 'AxiosError', + message: 'Axios error message', + config: { + headers: {} as AxiosHeaders, + }, + response: { + status: 400, + statusText: 'Bad Request', + headers: {} as AxiosHeaders, + data: { + error: { + message: 'Some error message', + code: 123, + }, + }, + } as AxiosResponse, + } as AxiosError; + + httpService.request.mockReturnValue(throwError(() => axiosError)); + }; + + it('should throw hydra oauth loggable exception', async () => { + setup(); + + await expect(service.listOAuth2Clients()).rejects.toThrow( + new HydraOauthLoggableException(`${hydraUri}/clients`, 'GET', 'Some error message', 123) + ); + }); + }); + }); + + describe('Client Flow', () => { + describe('listOAuth2Clients', () => { + describe('when only clientIds are given', () => { + const setup = () => { + const data: ProviderOauthClient[] = [ + { + client_id: 'client1', + }, + { + client_id: 'client2', + }, + ]; + + httpService.request.mockReturnValue(of(createAxiosResponse(data))); + + return { + data, + }; + }; + + it('should list all oauth2 clients', async () => { + const { data } = setup(); + + const result: ProviderOauthClient[] = await service.listOAuth2Clients(); + + expect(result).toEqual(data); + expect(httpService.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: `${hydraUri}/clients`, + method: 'GET', + headers: { + 'X-Forwarded-Proto': 'https', + }, + }) + ); + }); + }); + + describe('when clientId and other parameters are given', () => { + const setup = () => { + const data: ProviderOauthClient[] = [ + { + client_id: 'client1', + owner: 'clientOwner', + }, + ]; + + httpService.request.mockReturnValue(of(createAxiosResponse(data))); + + return { + data, + }; + }; + + it('should list all oauth2 clients within parameters', async () => { + const { data } = setup(); + + const result: ProviderOauthClient[] = await service.listOAuth2Clients(1, 0, 'client1', 'clientOwner'); + + expect(result).toEqual(data); + expect(httpService.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: `${hydraUri}/clients?limit=1&offset=0&client_name=client1&owner=clientOwner`, + method: 'GET', + headers: { + 'X-Forwarded-Proto': 'https', + }, + }) + ); + }); + }); + }); + describe('getOAuth2Client', () => { - it('should get oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should get oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.getOAuth2Client('clientId'); expect(result).toEqual(data); @@ -192,12 +276,20 @@ describe('HydraService', () => { }); describe('createOAuth2Client', () => { - it('should create oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should create oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.createOAuth2Client(data); expect(result).toEqual(data); @@ -215,12 +307,20 @@ describe('HydraService', () => { }); describe('updateOAuth2Client', () => { - it('should update oauth2 client', async () => { + const setup = () => { const data: ProviderOauthClient = { client_id: 'client', }; httpService.request.mockReturnValue(of(createAxiosResponse(data))); + return { + data, + }; + }; + + it('should update oauth2 client', async () => { + const { data } = setup(); + const result: ProviderOauthClient = await service.updateOAuth2Client('clientId', data); expect(result).toEqual(data); @@ -238,8 +338,12 @@ describe('HydraService', () => { }); describe('deleteOAuth2Client', () => { - it('should delete oauth2 client', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse({}))); + }; + + it('should delete oauth2 client', async () => { + setup(); await service.deleteOAuth2Client('clientId'); @@ -268,26 +372,30 @@ describe('HydraService', () => { }); describe('getConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const config: AxiosRequestConfig = { method: 'GET', url: `${hydraUri}/oauth2/auth/requests/consent?consent_challenge=${challenge}`, }; httpService.request.mockReturnValue(of(createAxiosResponse({ challenge }))); - // Act + return { + config, + }; + }; + + it('should make http request', async () => { + const { config } = setup(); + const result: ProviderConsentResponse = await service.getConsentRequest(challenge); - // Assert expect(result.challenge).toEqual(challenge); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('acceptConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const body: AcceptConsentRequestBody = { grant_scope: ['offline', 'openid'], }; @@ -301,18 +409,25 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should make http request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.acceptConsentRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('rejectConsentRequest', () => { - it('should make http request', async () => { - // Arrange + const setup = () => { const body: RejectRequestBody = { error: 'error', }; @@ -326,20 +441,36 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should make http request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.rejectConsentRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); }); describe('listConsentSessions', () => { - it('should list all consent sessions', async () => { + const setup = () => { const response: ProviderConsentSessionResponse[] = [{ consent_request: { challenge: 'challenge' } }]; httpService.request.mockReturnValue(of(createAxiosResponse(response))); + return { + response, + }; + }; + + it('should list all consent sessions', async () => { + const { response } = setup(); + const result: ProviderConsentSessionResponse[] = await service.listConsentSessions('userId'); expect(result).toEqual(response); @@ -356,8 +487,12 @@ describe('HydraService', () => { }); describe('revokeConsentSession', () => { - it('should revoke all consent sessions', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse({}))); + }; + + it('should revoke all consent sessions', async () => { + setup(); await service.revokeConsentSession('userId', 'clientId'); @@ -375,7 +510,7 @@ describe('HydraService', () => { describe('Logout Flow', () => { describe('acceptLogoutRequest', () => { - it('should make http request', async () => { + const setup = () => { const responseMock: ProviderRedirectResponse = { redirect_to: 'redirect_mock' }; httpService.request.mockReturnValue(of(createAxiosResponse(responseMock))); const config: AxiosRequestConfig = { @@ -384,6 +519,15 @@ describe('HydraService', () => { headers: { 'X-Forwarded-Proto': 'https' }, }; + return { + responseMock, + config, + }; + }; + + it('should make http request', async () => { + const { responseMock, config } = setup(); + const response: ProviderRedirectResponse = await service.acceptLogoutRequest('challenge_mock'); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); @@ -394,12 +538,20 @@ describe('HydraService', () => { describe('Miscellaneous', () => { describe('introspectOAuth2Token', () => { - it('should return introspect', async () => { + const setup = () => { const response: IntrospectResponse = { active: true, }; httpService.request.mockReturnValue(of(createAxiosResponse(response))); + return { + response, + }; + }; + + it('should return introspect', async () => { + const { response } = setup(); + const result: IntrospectResponse = await service.introspectOAuth2Token('token', 'scope'); expect(result).toEqual(response); @@ -418,8 +570,12 @@ describe('HydraService', () => { }); describe('isInstanceAlive', () => { - it('should check if hydra is alive', async () => { + const setup = () => { httpService.request.mockReturnValue(of(createAxiosResponse(true))); + }; + + it('should check if hydra is alive', async () => { + setup(); const result: boolean = await service.isInstanceAlive(); @@ -459,25 +615,30 @@ describe('HydraService', () => { }); describe('getLoginRequest', () => { - it('should send login request', async () => { - // Arrange + const setup = () => { const requestConfig: AxiosRequestConfig = { method: 'GET', url: `${hydraUri}/oauth2/auth/requests/login?login_challenge=${challenge}`, }; httpService.request.mockReturnValue(of(createAxiosResponse(providerLoginResponse))); - // Act + return { + requestConfig, + }; + }; + + it('should send login request', async () => { + const { requestConfig } = setup(); + const response: ProviderLoginResponse = await service.getLoginRequest(challenge); - // Assert expect(response).toEqual(providerLoginResponse); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(requestConfig)); }); }); describe('acceptLoginRequest', () => { - it('should send accept login request', async () => { + const setup = () => { const body: AcceptLoginRequestBody = { subject: '', force_subject_identifier: '', @@ -494,6 +655,16 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should send accept login request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.acceptLoginRequest(challenge, body); expect(result.redirect_to).toEqual(expectedRedirectTo); @@ -502,8 +673,7 @@ describe('HydraService', () => { }); describe('rejectLoginRequest', () => { - it('should send reject login request', async () => { - // Arrange + const setup = () => { const body: RejectRequestBody = { error: 'error', }; @@ -517,10 +687,18 @@ describe('HydraService', () => { of(createAxiosResponse({ redirect_to: expectedRedirectTo })) ); - // Act + return { + body, + config, + expectedRedirectTo, + }; + }; + + it('should send reject login request', async () => { + const { body, config, expectedRedirectTo } = setup(); + const result: ProviderRedirectResponse = await service.rejectLoginRequest(challenge, body); - // Assert expect(result.redirect_to).toEqual(expectedRedirectTo); expect(httpService.request).toHaveBeenCalledWith(expect.objectContaining(config)); }); diff --git a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts index f554a15abd3..91cf6bbbb7b 100644 --- a/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts +++ b/apps/server/src/infra/oauth-provider/hydra/hydra.adapter.ts @@ -1,21 +1,24 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; -import { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; +import { isAxiosError } from '@shared/common'; +import { AxiosError, AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; import QueryString from 'qs'; -import { Observable, firstValueFrom } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { URL } from 'url'; import { AcceptConsentRequestBody, AcceptLoginRequestBody, IntrospectResponse, ProviderConsentResponse, + ProviderConsentSessionResponse, ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, RejectRequestBody, } from '../dto'; -import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; +import { HydraOauthLoggableException } from '../loggable'; import { OauthProviderService } from '../oauth-provider.service'; @Injectable() @@ -160,15 +163,29 @@ export class HydraAdapter extends OauthProviderService { data?: unknown, additionalHeaders: RawAxiosRequestHeaders = {} ): Promise { - const observable: Observable> = this.httpService.request({ - url, - method, - headers: { - 'X-Forwarded-Proto': 'https', - ...additionalHeaders, - }, - data, - }); + const observable: Observable> = this.httpService + .request({ + url, + method, + headers: { + 'X-Forwarded-Proto': 'https', + ...additionalHeaders, + }, + data, + }) + .pipe( + catchError((error: unknown) => { + if (isAxiosError(error)) { + const responseData = error.response?.data; + const errorMessage = (responseData as { error: { message: string } }).error.message; + const errorCode = (responseData as { error: { code: number } }).error.code; + throw new HydraOauthLoggableException(url, method, errorMessage, errorCode); + } else { + throw error; + } + }) + ); + const response: AxiosResponse = await firstValueFrom(observable); return response.data; } diff --git a/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.spec.ts b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.spec.ts new file mode 100644 index 00000000000..b7b89ac741d --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.spec.ts @@ -0,0 +1,38 @@ +import { HydraOauthLoggableException } from './hydra-oauth-loggable-exception'; + +describe(HydraOauthLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const url = 'url'; + const method = 'method'; + const errorMessage = 'thisIsTheMessage'; + const errorStatusCode = 127; + + const exception = new HydraOauthLoggableException(url, method, errorMessage, errorStatusCode); + + return { + exception, + url, + method, + errorMessage, + errorStatusCode, + }; + }; + + it('should return the correct log message', () => { + const { exception, url, method, errorMessage } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'HYDRA_OAUTH_ERROR', + message: 'Hydra oauth error occurred', + stack: expect.stringContaining(`HydraOauthLoggableException: ${errorMessage}`) as string, + data: { + url, + method, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.ts b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.ts new file mode 100644 index 00000000000..1aba3fc0c71 --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/hydra-oauth-loggable-exception.ts @@ -0,0 +1,25 @@ +import { HttpException } from '@nestjs/common/exceptions/http.exception'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class HydraOauthLoggableException extends HttpException implements Loggable { + constructor( + private readonly url: string, + private readonly method: string, + private readonly errorMessage: string, + private readonly errorStatusCode: number + ) { + super(errorMessage, errorStatusCode); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'HYDRA_OAUTH_ERROR', + message: 'Hydra oauth error occurred', + stack: this.stack, + data: { + url: this.url, + method: this.method, + }, + }; + } +} diff --git a/apps/server/src/infra/oauth-provider/loggable/index.ts b/apps/server/src/infra/oauth-provider/loggable/index.ts new file mode 100644 index 00000000000..bf560234e7e --- /dev/null +++ b/apps/server/src/infra/oauth-provider/loggable/index.ts @@ -0,0 +1 @@ +export * from './hydra-oauth-loggable-exception'; diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts index 12c0a381d8b..a0b524fb2be 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; +import { AxiosError, AxiosResponse } from 'axios/index'; import { of, throwError } from 'rxjs'; import { OAuthGrantType } from '../interface/oauth-grant-type.enum'; import { OAuthSSOError } from '../loggable'; @@ -101,5 +102,47 @@ describe('OauthAdapterServive', () => { await expect(resp).rejects.toEqual(new OAuthSSOError('Requesting token failed.', 'sso_auth_code_step')); }); }); + + describe('when error got returned', () => { + describe('when error is a unknown error', () => { + const setup = () => { + httpService.post.mockReturnValueOnce(throwError(() => new Error('unknown error'))); + }; + + it('should throw the default sso error', async () => { + setup(); + + const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + + await expect(resp).rejects.toEqual(new OAuthSSOError('Requesting token failed.', 'sso_auth_code_step')); + }); + }); + + describe('when error is a axios error', () => { + const setup = () => { + const axiosError: AxiosError = { + isAxiosError: true, + response: { + data: { + error: { + message: 'Some error message', + code: 123, + }, + }, + } as AxiosResponse, + } as AxiosError; + + httpService.post.mockReturnValueOnce(throwError(() => axiosError)); + }; + + it('should throw an error', async () => { + setup(); + + const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + + await expect(resp).rejects.toEqual(new OAuthSSOError('Some error message', 'sso_auth_code_step')); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts index 6b008b610cf..266c018a852 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts @@ -1,6 +1,8 @@ +import { HydraOauthLoggableException } from '@infra/oauth-provider/loggable'; import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common/decorators'; -import { AxiosResponse } from 'axios'; +import { isAxiosError } from '@shared/common'; +import { AxiosError, AxiosResponse } from 'axios'; import JwksRsa from 'jwks-rsa'; import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; @@ -37,10 +39,20 @@ export class OauthAdapterService { private async resolveTokenRequest( observable: Observable> ): Promise { - let responseToken: AxiosResponse; + let responseToken: AxiosResponse | undefined; try { responseToken = await lastValueFrom(observable); - } catch (error) { + } catch (error: unknown) { + if (isAxiosError(error)) { + const response = error.response as AxiosResponse; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const errorMessage: string = (response?.data?.error?.message as string) ?? 'Requesting token failed.'; + throw new OAuthSSOError(errorMessage, 'sso_auth_code_step'); + } + } + + if (!responseToken) { throw new OAuthSSOError('Requesting token failed.', 'sso_auth_code_step'); } diff --git a/apps/server/src/shared/common/utils/axios.util.spec.ts b/apps/server/src/shared/common/utils/axios.util.spec.ts new file mode 100644 index 00000000000..febdce58aaa --- /dev/null +++ b/apps/server/src/shared/common/utils/axios.util.spec.ts @@ -0,0 +1,21 @@ +import { isAxiosError } from './axios.util'; + +describe('axios.util', () => { + describe('isAxiosError', () => { + it('should return true when error is an axios error', () => { + const axiosError = { + isAxiosError: true, + }; + expect(isAxiosError(axiosError)).toBe(true); + }); + + it('should return false when error is not an axios error', () => { + const error = new Error('some error'); + expect(isAxiosError(error)).toBe(false); + }); + + it('should return false when error is undefined', () => { + expect(isAxiosError(undefined)).toBe(false); + }); + }); +}); diff --git a/apps/server/src/shared/common/utils/axios.util.ts b/apps/server/src/shared/common/utils/axios.util.ts new file mode 100644 index 00000000000..40bdae3e1fd --- /dev/null +++ b/apps/server/src/shared/common/utils/axios.util.ts @@ -0,0 +1,5 @@ +import { AxiosError } from 'axios'; + +export function isAxiosError(error: unknown): error is AxiosError { + return !!(error && typeof error === 'object' && 'isAxiosError' in error); +} diff --git a/apps/server/src/shared/common/utils/index.ts b/apps/server/src/shared/common/utils/index.ts index 64a60811569..5179cae7794 100644 --- a/apps/server/src/shared/common/utils/index.ts +++ b/apps/server/src/shared/common/utils/index.ts @@ -1,2 +1,3 @@ export * from './converter.util'; export * from './guard-against'; +export * from './axios.util';