Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend tests and coverage #27

Merged
merged 16 commits into from
Nov 19, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![codecov](https://codecov.io/gh/w3bdesign/frisorsalong-booking/graph/badge.svg?token=YDY1N2NMWA)](https://codecov.io/gh/w3bdesign/frisorsalong-booking)

# Hair Salon Booking System

## This is still Work In Progress (WIP)!
Expand Down
17 changes: 16 additions & 1 deletion backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ module.exports = {
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
collectCoverageFrom: [
'**/*.(t|j)s',
'!**/*.spec.ts',
'!**/*.test.ts',
'!**/node_modules/**',
'!**/dist/**',
'!**/coverage/**',
],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/$1',
},
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
108 changes: 108 additions & 0 deletions backend/src/auth/guards/roles.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import { ExecutionContext } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
import { UserRole } from '../../users/entities/user.entity';

describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;

const mockReflector = {
getAllAndOverride: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RolesGuard,
{
provide: Reflector,
useValue: mockReflector,
},
],
}).compile();

guard = module.get<RolesGuard>(RolesGuard);
reflector = module.get<Reflector>(Reflector);
});

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

describe('canActivate', () => {
let mockExecutionContext: ExecutionContext;

beforeEach(() => {
mockExecutionContext = {
getHandler: jest.fn(),
getClass: jest.fn(),
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
user: {
role: UserRole.CUSTOMER,
},
}),
}),
} as unknown as ExecutionContext;
});

it('should allow access when no roles are required', () => {
mockReflector.getAllAndOverride.mockReturnValue(null);

const result = guard.canActivate(mockExecutionContext);

expect(result).toBe(true);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith('roles', [
mockExecutionContext.getHandler(),
mockExecutionContext.getClass(),
]);
});

it('should allow access when user has required role', () => {
mockReflector.getAllAndOverride.mockReturnValue([UserRole.CUSTOMER]);

const result = guard.canActivate(mockExecutionContext);

expect(result).toBe(true);
});

it('should deny access when user does not have required role', () => {
mockReflector.getAllAndOverride.mockReturnValue([UserRole.ADMIN]);

const result = guard.canActivate(mockExecutionContext);

expect(result).toBe(false);
});

it('should allow access when user has one of multiple required roles', () => {
mockReflector.getAllAndOverride.mockReturnValue([
UserRole.ADMIN,
UserRole.CUSTOMER,
]);

const result = guard.canActivate(mockExecutionContext);

expect(result).toBe(true);
});

it('should handle roles defined at both handler and class level', () => {
mockReflector.getAllAndOverride.mockReturnValue([UserRole.CUSTOMER]);

const mockContext = {
...mockExecutionContext,
getHandler: jest.fn(),
getClass: jest.fn(),
};

const result = guard.canActivate(mockContext as ExecutionContext);

expect(result).toBe(true);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith('roles', [
mockContext.getHandler(),
mockContext.getClass(),
]);
});
});
});
154 changes: 154 additions & 0 deletions backend/src/database/migrations/1731981975581-InitialMigration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { QueryRunner } from 'typeorm';
import { InitialMigration1731981975581 } from './1731981975581-InitialMigration';

describe('InitialMigration1731981975581', () => {
let migration: InitialMigration1731981975581;
let queryRunner: QueryRunner;

beforeEach(() => {
migration = new InitialMigration1731981975581();
queryRunner = {
query: jest.fn(),
} as unknown as QueryRunner;
});

it('should have correct name', () => {
expect(migration.name).toBe('InitialMigration1731981975581');
});

describe('up', () => {
it('should create user role enum', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TYPE "public"."users_role_enum"'),
);
});

it('should create users table', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TABLE "users"'),
);
});

it('should create employees table', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TABLE "employees"'),
);
});

it('should create services table', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TABLE "services"'),
);
});

it('should create booking status enum', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TYPE "public"."bookings_status_enum"'),
);
});

it('should create bookings table', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TABLE "bookings"'),
);
});

it('should create employee_services table', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE TABLE "employee_services"'),
);
});

it('should create indexes', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('CREATE INDEX'),
);
});

it('should create foreign key constraints', async () => {
await migration.up(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('ALTER TABLE'),
);
expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('FOREIGN KEY'),
);
});
});

describe('down', () => {
it('should drop foreign key constraints', async () => {
await migration.down(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('DROP CONSTRAINT'),
);
});

it('should drop indexes', async () => {
await migration.down(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('DROP INDEX'),
);
});

it('should drop tables in correct order', async () => {
await migration.down(queryRunner);

const calls = (queryRunner.query as jest.Mock).mock.calls.map(
call => call[0],
);

// Verify drop order: employee_services -> bookings -> services -> employees -> users
const employeeServicesIndex = calls.findIndex(call =>
call.includes('DROP TABLE "employee_services"'),
);
const bookingsIndex = calls.findIndex(call =>
call.includes('DROP TABLE "bookings"'),
);
const servicesIndex = calls.findIndex(call =>
call.includes('DROP TABLE "services"'),
);
const employeesIndex = calls.findIndex(call =>
call.includes('DROP TABLE "employees"'),
);
const usersIndex = calls.findIndex(call =>
call.includes('DROP TABLE "users"'),
);

expect(employeeServicesIndex).toBeLessThan(bookingsIndex);
expect(bookingsIndex).toBeLessThan(servicesIndex);
expect(servicesIndex).toBeLessThan(employeesIndex);
expect(employeesIndex).toBeLessThan(usersIndex);
});

it('should drop enums', async () => {
await migration.down(queryRunner);

expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('DROP TYPE "public"."users_role_enum"'),
);
expect(queryRunner.query).toHaveBeenCalledWith(
expect.stringContaining('DROP TYPE "public"."bookings_status_enum"'),
);
});
});
});
Loading
Loading