Skip to content

Commit

Permalink
Ci-setup (#7)
Browse files Browse the repository at this point in the history
* Initial CI, with just format checking

* added name to action

* typo in folder name

* Updated lock

* Simplified package.json and added testing to CI

* Tests for validation pipe

* Added test for authorization guard, modifying the user service to make it testable

* Used automocking in existing tests, added tests for users service

* Added mock of users service for e2e testing
Data is chatGPT generated

* Added tests on Joi schema createUser and some to checkAbilities

* Tests on createUserSchema

* Doubled speed of running tests with swc

* Run tests in separate steps for each module

* Chaged node version on CI

* Added swcrc which should fix the problems with the action

* Removed @swc/cli from api

* Changed logic on role change, added first tests on abilities on person

* Refactored role change check for unit testing predisposition

* added rimraf as dev dependency

* Added test on users controller
  • Loading branch information
friedbyalice authored May 1, 2023
1 parent b811390 commit 48874cc
Show file tree
Hide file tree
Showing 24 changed files with 2,793 additions and 1,349 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/fullstack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Fullstack

on:
- push
- pull_request

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20

- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 7
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install

- name: Check fomatting
run: pnpm run format-check

- name: Test building
run: pnpm run build && pnpm run clean
- name: Run shared module tests
run: pnpm --filter shared test
- name: Run api tests
run: pnpm --filter api test
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ api/src/modules/GAuth/authInfo
/**/*.tsbuildinfo
.vscode

/fontend_tmp
/frontend_tmp/**

tmp-*

**/.DS_Store

.*
!.github
!.gitignore
!**/.swcrc

coverage/
26 changes: 26 additions & 0 deletions api/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"module": {
"type": "commonjs"
},
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"keepClassNames": true,
"baseUrl": "./",
"paths": {
"@hkrecruitment/shared": ["../shared/src"],
"@hkrecruitment/shared/*": ["../shared/src/*"]
}
},
"minify": false
}
18 changes: 13 additions & 5 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"format-check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "ts-node src/index.ts",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/api/src/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test": "jest -i --verbose",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
Expand Down Expand Up @@ -40,15 +41,18 @@
"passport-jwt": "^4.0.0",
"pg": "^8.4.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typeorm": "0.3.11",
"webpack": "^5.75.0"
},
"devDependencies": {
"@automock/jest": "^1.0.1",
"@golevelup/ts-jest": "^0.3.6",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@swc/core": "^1.3.56",
"@swc/jest": "^0.2.26",
"@types/express": "^4.17.14",
"@types/jest": "28.1.8",
"@types/node": "^16.11.10",
Expand All @@ -59,7 +63,9 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3",
"nodemon": "^2.0.22",
"prettier": "^2.8.5",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
Expand All @@ -74,15 +80,17 @@
"json",
"ts"
],
"rootDir": "src",
"rootDir": ".",
"modulePaths": [
"<rootDir>"
],
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
"^.+\\.(t|j)s$": "@swc/jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
5 changes: 0 additions & 5 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { AuthenticationModule } from './authentication/authentication.module';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { JwtGuard } from './authentication/jwt-guard.guard';
import { AuthorizationModule } from './authorization/authorization.module';
import { TimerInterceptor } from './timer/timer.interceptor';
import { AuthorizationGuard } from './authorization/authorization.guard';

@Module({
Expand Down Expand Up @@ -42,10 +41,6 @@ import { AuthorizationGuard } from './authorization/authorization.guard';
provide: APP_GUARD,
useClass: AuthorizationGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: TimerInterceptor,
},
],
})
export class AppModule {}
3 changes: 2 additions & 1 deletion api/src/authorization/authenticated-request.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppAbility, UserAuth } from '@hkrecruitment/shared';
import { AppAbility, RoleChangeChecker, UserAuth } from '@hkrecruitment/shared';

export type AuthenticatedRequest = Request & {
user: UserAuth;
ability: AppAbility;
roleChangeChecker: RoleChangeChecker;
};
67 changes: 64 additions & 3 deletions api/src/authorization/authorization.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,68 @@
import { AuthorizationGuard } from './authorization.guard';
import { Reflector } from '@nestjs/core';
import { UsersService } from 'src/users/users.service';
import { Role, abilityForUser } from '@hkrecruitment/shared';
import { ExecutionContext } from '@nestjs/common';
import { TestBed } from '@automock/jest';
import { createMock } from '@golevelup/ts-jest';

describe('AuthorizationGuard', () => {
// it('should be defined', () => {
// expect(new AuthorizationGuard()).toBeDefined();
// });
let reflector: Reflector;
let usersService: UsersService;
let authorizationGuard: AuthorizationGuard;

beforeEach(async () => {
const { unit, unitRef } = TestBed.create(AuthorizationGuard).compile();

reflector = unitRef.get(Reflector);
usersService = unitRef.get(UsersService);
authorizationGuard = unit;
});

afterEach(() => {
jest.clearAllMocks();
});

it('should allow access if no policies are defined', async () => {
const context = createMock<ExecutionContext>({
switchToHttp: () => ({
getRequest: () => ({ user: {} }),
}),
});
jest
.spyOn(usersService, 'getRoleAndAbilityForOauthId')
.mockResolvedValue([
Role.None,
abilityForUser({ sub: 'test', role: Role.None }),
]);
const canActivate = await authorizationGuard.canActivate(context);
expect(canActivate).toBe(true);
});

it('should allow access if all policies are fulfilled', async () => {
const mockUser = {};
const context = createMock<ExecutionContext>({
switchToHttp: () => ({
getRequest: () => ({ user: mockUser }),
}),
});
const handler = jest.fn(() => true);
jest.spyOn(reflector, 'get').mockReturnValue([handler]);
const mockAbility = abilityForUser({ sub: 'test', role: Role.None });
jest
.spyOn(usersService, 'getRoleAndAbilityForOauthId')
.mockResolvedValue([Role.None, mockAbility]);

const canActivate = await authorizationGuard.canActivate(context);
expect(canActivate).toBe(true);
expect(mockUser).toHaveProperty('role', Role.None);

expect(usersService.getRoleAndAbilityForOauthId).toHaveBeenCalledTimes(1);
expect(usersService.getRoleAndAbilityForOauthId).toHaveBeenCalledWith(
undefined,
);

expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(mockAbility);
});
});
15 changes: 6 additions & 9 deletions api/src/authorization/authorization.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { abilityForUser } from '@hkrecruitment/shared';
import { abilityForUser, getRoleChangeChecker } from '@hkrecruitment/shared';
import {
CanActivate,
ExecutionContext,
Expand All @@ -19,16 +19,13 @@ export class AuthorizationGuard implements CanActivate {
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const role =
(await this.usersService.getRoleForOauthId(
context.switchToHttp().getRequest().user.sub,
)) ?? 'none';
context.switchToHttp().getRequest().user.role = role;
this.logger.log(
`user.sub: ${context.switchToHttp().getRequest().user.sub} -> ${role}}`,
const [role, ability] = await this.usersService.getRoleAndAbilityForOauthId(
context.switchToHttp().getRequest().user.sub,
);
const ability = abilityForUser(context.switchToHttp().getRequest().user);
context.switchToHttp().getRequest().user.role = role;
context.switchToHttp().getRequest().ability = ability;
context.switchToHttp().getRequest().roleChangeChecker =
getRoleChangeChecker(role);

const handlers =
this.reflector.get<PolicyHandler[]>(
Expand Down
84 changes: 83 additions & 1 deletion api/src/joi-validation/joi-validation-pipe.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
import { BadRequestException } from '@nestjs/common';
import { JoiValidationPipe } from './joi-validation.pipe';
import * as Joi from 'joi';

describe('JoiValidationPipe', () => {});
describe('JoiValidationPipe', () => {
it('should be defined', () => {
expect(new JoiValidationPipe({})).toBeDefined();
});

it('should work with empty schema', () => {
const pipe = new JoiValidationPipe({});

expect(() => pipe.transform({}, { type: 'body' })).toThrow(
BadRequestException,
);
expect(() =>
pipe.transform({}, { type: 'query', data: 'anything' }),
).toThrow(BadRequestException);
expect(() => pipe.transform({}, { type: 'param' })).toThrow(
BadRequestException,
);
expect(() =>
pipe.transform({ internal: 'stuff' }, { type: 'custom' }),
).not.toThrow();
});

it('should work with query schema', () => {
const pipe = new JoiValidationPipe({
query: {
id: Joi.number(),
},
});

expect(() =>
pipe.transform('Not a number', { type: 'query', data: 'id' }),
).toThrow(/Validation of query.id failed: /);
expect(() =>
pipe.transform("Doesn't matter", { type: 'query', data: 'notId' }),
).toThrow(/Validation of query.notId failed: no schema defined/);

expect(pipe.transform(1, { type: 'query', data: 'id' })).toBe(1);

expect(() => pipe.transform({}, { type: 'query' })).toThrow(
/Validation of query\..+ failed/,
);
expect(() => pipe.transform({}, { type: 'param' })).toThrow(
/Validation of param failed: no schema defined/,
);
expect(() => pipe.transform({ key: 'val' }, { type: 'body' })).toThrow(
/Validation of body failed: no schema defined/,
);
expect(() =>
pipe.transform({ internal: 'stuff' }, { type: 'custom' }),
).not.toThrow();
});

it('should work with body schema', () => {
const pipe = new JoiValidationPipe({
body: Joi.object({
id: Joi.number(),
}),
});

expect(() => pipe.transform('Not an object', { type: 'body' })).toThrow(
/Validation of body failed: /,
);
expect(() => pipe.transform({ key: 'val' }, { type: 'body' })).toThrow(
/Validation of body failed: /,
);
expect(() =>
pipe.transform({ id: 'Not a number' }, { type: 'body' }),
).toThrow(/Validation of body failed: /);
expect(pipe.transform({ id: 1 }, { type: 'body' })).toEqual({ id: 1 });

expect(() => pipe.transform({}, { type: 'query', data: 'thing' })).toThrow(
/Validation of query.thing failed: no schema defined/,
);
expect(() => pipe.transform({}, { type: 'param' })).toThrow(
/Validation of param failed: no schema defined/,
);
expect(() =>
pipe.transform({ internal: 'stuff' }, { type: 'custom' }),
).not.toThrow();
});
});
Loading

0 comments on commit 48874cc

Please sign in to comment.