-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
created controllers, services for /api/login endpoint for user authen…
…tication on keycloak
- Loading branch information
Showing
20 changed files
with
627 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Catch } from '@nestjs/common'; | ||
import { KeycloakClientError } from '../../../shared/error/index.js'; | ||
import { UiBackendExceptionFilter } from './ui-backend-exception-filter.js'; | ||
|
||
@Catch(KeycloakClientError) | ||
export class KeyCloakExceptionFilter extends UiBackendExceptionFilter<KeycloakClientError> {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { LoginController } from './login.controller.js'; | ||
import { createMock, DeepMocked } from '@golevelup/ts-jest'; | ||
import { LoginService } from '../domain/login.service.js'; | ||
import { faker } from '@faker-js/faker'; | ||
import { TokenSet } from 'openid-client'; | ||
import { UserParams } from './user.params.js'; | ||
import { KeycloakClientError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; | ||
import { NewLoginService } from '../domain/new-login.service.js'; | ||
|
||
describe('LoginController', () => { | ||
let module: TestingModule; | ||
let loginController: LoginController; | ||
let loginServiceMock: DeepMocked<LoginService>; | ||
let someServiceMock: DeepMocked<NewLoginService>; | ||
let tokenSet: DeepMocked<TokenSet>; | ||
|
||
beforeAll(async () => { | ||
module = await Test.createTestingModule({ | ||
imports: [], | ||
providers: [ | ||
LoginController, | ||
{ | ||
provide: LoginService, | ||
useValue: createMock<LoginService>(), | ||
}, | ||
{ | ||
provide: NewLoginService, | ||
useValue: createMock<NewLoginService>(), | ||
}, | ||
{ | ||
provide: TokenSet, | ||
useValue: createMock<TokenSet>(), | ||
}, | ||
], | ||
}).compile(); | ||
loginController = module.get(LoginController); | ||
loginServiceMock = module.get(LoginService); | ||
someServiceMock = module.get(NewLoginService); | ||
tokenSet = module.get(TokenSet); | ||
}); | ||
|
||
afterAll(async () => { | ||
await module.close(); | ||
}); | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(loginController).toBeDefined(); | ||
}); | ||
|
||
describe('when getting result from service', () => { | ||
it('should not throw', async () => { | ||
const userParams: UserParams = { | ||
username: faker.string.alpha(), | ||
password: faker.string.alpha(), | ||
}; | ||
loginServiceMock.getTokenForUser.mockResolvedValue(tokenSet); | ||
await expect(loginController.loginUser(userParams)).resolves.not.toThrow(); | ||
expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('when getting KeyCloak-error from service', () => { | ||
it('should throw', async () => { | ||
const errorMsg: string = 'keycloak not available'; | ||
const userParams: UserParams = { | ||
username: faker.string.alpha(), | ||
password: faker.string.alpha(), | ||
}; | ||
loginServiceMock.getTokenForUser.mockImplementation(() => { | ||
throw new KeycloakClientError(errorMsg); | ||
}); | ||
await expect(loginController.loginUser(userParams)).rejects.toThrow(errorMsg); | ||
expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('when getting User-authentication-failed-error from service', () => { | ||
it('should throw', async () => { | ||
const errorMsg: string = 'user could not be authenticated'; | ||
const userParams: UserParams = { | ||
username: faker.string.alpha(), | ||
password: faker.string.alpha(), | ||
}; | ||
loginServiceMock.getTokenForUser.mockImplementation(() => { | ||
throw new UserAuthenticationFailedError(errorMsg); | ||
}); | ||
await expect(loginController.loginUser(userParams)).rejects.toThrow(errorMsg); | ||
expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('when getting User-authentication-failed-error from service', () => { | ||
it('should throw', async () => { | ||
const userParams: UserParams = { | ||
username: faker.string.alpha(), | ||
password: faker.string.alpha(), | ||
}; | ||
someServiceMock.auth.mockResolvedValueOnce({ | ||
ok: false, | ||
error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), | ||
}); | ||
await expect(loginController.loginUserResult(userParams)).resolves.toStrictEqual< | ||
Result<UserAuthenticationFailedError> | ||
>({ | ||
ok: false, | ||
error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), | ||
}); | ||
expect(someServiceMock.auth).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Body, Controller, HttpStatus, Post, UseFilters } from '@nestjs/common'; | ||
import { | ||
ApiInternalServerErrorResponse, | ||
ApiNotFoundResponse, | ||
ApiServiceUnavailableResponse, | ||
ApiTags, | ||
} from '@nestjs/swagger'; | ||
import { UserParams } from './user.params.js'; | ||
import { LoginService } from '../domain/login.service.js'; | ||
import { TokenSet } from 'openid-client'; | ||
import { KeyCloakExceptionFilter } from './keycloak-exception-filter.js'; | ||
import { UserAuthenticationFailedExceptionFilter } from './user-authentication-failed-exception-filter.js'; | ||
import { NewLoginService } from '../domain/new-login.service.js'; | ||
import { DomainError } from '../../../shared/error/index.js'; | ||
|
||
@ApiTags('api/login') | ||
@Controller({ path: 'login'}) | ||
export class LoginController { | ||
public constructor(private loginService: LoginService, private someService: NewLoginService) {} | ||
|
||
@Post() | ||
@UseFilters( | ||
new KeyCloakExceptionFilter(HttpStatus.SERVICE_UNAVAILABLE), | ||
new UserAuthenticationFailedExceptionFilter(HttpStatus.NOT_FOUND), | ||
) | ||
@ApiNotFoundResponse({ | ||
description: 'USER_AUTHENTICATION_FAILED_ERROR: User could not be authenticated successfully.', | ||
}) | ||
@ApiInternalServerErrorResponse({ description: 'Internal server error while retrieving token.' }) | ||
@ApiServiceUnavailableResponse({ description: 'KEYCLOAK_CLIENT_ERROR: KeyCloak service did not respond.' }) | ||
public async loginUser(@Body() params: UserParams): Promise<TokenSet> { | ||
return this.loginService.getTokenForUser(params.username, params.password); | ||
} | ||
|
||
@Post('result') | ||
@UseFilters( | ||
new KeyCloakExceptionFilter(HttpStatus.SERVICE_UNAVAILABLE), | ||
new UserAuthenticationFailedExceptionFilter(HttpStatus.NOT_FOUND), | ||
) | ||
@ApiNotFoundResponse({ | ||
description: 'USER_AUTHENTICATION_FAILED_ERROR: User could not be authenticated successfully.', | ||
}) | ||
@ApiInternalServerErrorResponse({ description: 'Internal server error while retrieving token.' }) | ||
public async loginUserResult(@Body() params: UserParams): Promise<Result<string, DomainError>> { | ||
return this.someService.auth(params.username, params.password); | ||
} | ||
} |
Oops, something went wrong.