Skip to content

Commit

Permalink
created controllers, services for /api/login endpoint for user authen…
Browse files Browse the repository at this point in the history
…tication on keycloak
  • Loading branch information
DPDS93CT committed Oct 10, 2023
1 parent 3b783af commit be0e30d
Show file tree
Hide file tree
Showing 20 changed files with 627 additions and 0 deletions.
6 changes: 6 additions & 0 deletions config/config.dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
"BASE_URL": "http://127.0.0.1:8080",
"REALM_NAME": "master",
"CLIENT_ID": "admin-cli"
},
"SCHULPORTAL": {
"BASE_URL": "http://127.0.0.1:8080",
"REALM_NAME": "schulportal",
"CLIENT_ID": "schulportal",
"USERNAME": "dummy"
}
}
6 changes: 6 additions & 0 deletions config/config.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
"BASE_URL": "http://127.0.0.1:8080",
"REALM_NAME": "master",
"CLIENT_ID": "admin-cli"
},
"SCHULPORTAL": {
"BASE_URL": "http://127.0.0.1:8080",
"REALM_NAME": "schulportal",
"CLIENT_ID": "schulportal",
"USERNAME": "dummy"
}
}
97 changes: 97 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
"axios": "^1.5.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"express": "^4.18.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nest-commander": "^3.9.0",
"nest-keycloak-connect": "^1.9.2",
"openid-client": "^5.6.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
Expand Down
6 changes: 6 additions & 0 deletions src/modules/ui-backend/api/keycloak-exception-filter.ts
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> {}
116 changes: 116 additions & 0 deletions src/modules/ui-backend/api/login.controller.spec.ts
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);
});
});
});
47 changes: 47 additions & 0 deletions src/modules/ui-backend/api/login.controller.ts
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'})

Check warning on line 17 in src/modules/ui-backend/api/login.controller.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Insert `·`
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);
}
}
Loading

0 comments on commit be0e30d

Please sign in to comment.