Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,13 @@ jobs:
run: npx nx run @zuko/models:seed

# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: npx nx-cloud record -- echo Hello World
# - run: npx nx run-many -t lint test build typecheck

# typecheck & Build (parallel)
- name: Typecheck
run: npx nx affected -t typecheck --parallel=50% --nxBail

- name: Build
run: npx nx affected -t build --parallel=50% --nxBail

- name: E2E tests
run: npx nx affected -t e2e --exclude=backend-e2e --nxBail
Expand Down
94 changes: 47 additions & 47 deletions apps/backend-e2e/src/backend/roles.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import axios from 'axios';
import { PrismaClient } from '@prisma/client';
import { createTestUser, getAuthCookie } from '../support/auth-helper';

describe('Role-Based Access Control', () => {
import axios from "axios";
import { PrismaClient } from "@prisma/client";
import {
createTestUserWithSession,
getAuthCookie,
} from "../support/auth-helper";

describe("Role-Based Access Control", () => {
const prisma = new PrismaClient();
const baseUrl = process.env.API_URL || 'http://localhost:3001';
const baseUrl = process.env.API_URL || "http://localhost:3001";

let adminUser: any;
let accountantUser: any;
Expand All @@ -15,22 +18,19 @@ describe('Role-Based Access Control', () => {

beforeAll(async () => {
// Create test users with different roles
adminUser = await createTestUser('admin-test@example.com', 'Admin User');
adminUser = await createTestUserWithSession();
await prisma.user.update({
where: { id: adminUser.id },
data: { role: 'admin' },
data: { role: "admin" },
});

accountantUser = await createTestUser(
'accountant-test@example.com',
'Accountant User',
);
accountantUser = await createTestUserWithSession();
await prisma.user.update({
where: { id: accountantUser.id },
data: { role: 'accountant' },
data: { role: "accountant" },
});

normalUser = await createTestUser('normal-test@example.com', 'Normal User');
normalUser = await createTestUserWithSession();
// normalUser will have 'none' role by default

// Get auth cookies for each user
Expand All @@ -45,136 +45,136 @@ describe('Role-Based Access Control', () => {
where: {
email: {
in: [
'admin-test@example.com',
'accountant-test@example.com',
'normal-test@example.com',
"admin-test@example.com",
"accountant-test@example.com",
"normal-test@example.com",
],
},
},
});
await prisma.$disconnect();
});

describe('Admin-only endpoints', () => {
it('should allow admin to access admin dashboard', async () => {
describe("Admin-only endpoints", () => {
it("should allow admin to access admin dashboard", async () => {
const response = await axios.get(`${baseUrl}/api/admin/dashboard`, {
headers: { Cookie: adminCookie },
});

expect(response.status).toBe(200);
expect(response.data).toHaveProperty('message');
expect(response.data).toHaveProperty('stats');
expect(response.data).toHaveProperty("message");
expect(response.data).toHaveProperty("stats");
});

it('should deny accountant access to admin dashboard', async () => {
it("should deny accountant access to admin dashboard", async () => {
try {
await axios.get(`${baseUrl}/api/admin/dashboard`, {
headers: { Cookie: accountantCookie },
});
fail('Should have thrown an error');
fail("Should have thrown an error");
} catch (error: any) {
expect(error.response.status).toBe(403);
}
});

it('should deny normal user access to admin dashboard', async () => {
it("should deny normal user access to admin dashboard", async () => {
try {
await axios.get(`${baseUrl}/api/admin/dashboard`, {
headers: { Cookie: normalCookie },
});
fail('Should have thrown an error');
fail("Should have thrown an error");
} catch (error: any) {
expect(error.response.status).toBe(403);
}
});

it('should deny unauthenticated access to admin dashboard', async () => {
it("should deny unauthenticated access to admin dashboard", async () => {
try {
await axios.get(`${baseUrl}/api/admin/dashboard`);
fail('Should have thrown an error');
fail("Should have thrown an error");
} catch (error: any) {
expect([401, 403]).toContain(error.response.status);
}
});
});

describe('Multi-role endpoints', () => {
it('should allow admin to access reports', async () => {
describe("Multi-role endpoints", () => {
it("should allow admin to access reports", async () => {
const response = await axios.get(`${baseUrl}/api/admin/reports`, {
headers: { Cookie: adminCookie },
});

expect(response.status).toBe(200);
expect(response.data).toHaveProperty('message');
expect(response.data).toHaveProperty('reports');
expect(response.data).toHaveProperty("message");
expect(response.data).toHaveProperty("reports");
expect(Array.isArray(response.data.reports)).toBe(true);
});

it('should allow accountant to access reports', async () => {
it("should allow accountant to access reports", async () => {
const response = await axios.get(`${baseUrl}/api/admin/reports`, {
headers: { Cookie: accountantCookie },
});

expect(response.status).toBe(200);
expect(response.data).toHaveProperty('reports');
expect(response.data).toHaveProperty("reports");
});

it('should deny normal user access to reports', async () => {
it("should deny normal user access to reports", async () => {
try {
await axios.get(`${baseUrl}/api/admin/reports`, {
headers: { Cookie: normalCookie },
});
fail('Should have thrown an error');
fail("Should have thrown an error");
} catch (error: any) {
expect(error.response.status).toBe(403);
}
});
});

describe('Role verification', () => {
it('should verify admin user has correct role in database', async () => {
describe("Role verification", () => {
it("should verify admin user has correct role in database", async () => {
const user = await prisma.user.findUnique({
where: { id: adminUser.id },
});

expect(user?.role).toBe('admin');
expect(user?.role).toBe("admin");
});

it('should verify accountant user has correct role in database', async () => {
it("should verify accountant user has correct role in database", async () => {
const user = await prisma.user.findUnique({
where: { id: accountantUser.id },
});

expect(user?.role).toBe('accountant');
expect(user?.role).toBe("accountant");
});

it('should verify normal user has none role in database', async () => {
it("should verify normal user has none role in database", async () => {
const user = await prisma.user.findUnique({
where: { id: normalUser.id },
});

expect(user?.role).toBe('none');
expect(user?.role).toBe("none");
});
});

describe('Role changes', () => {
it('should update user role successfully', async () => {
describe("Role changes", () => {
it("should update user role successfully", async () => {
// Change normal user to accountant
await prisma.user.update({
where: { id: normalUser.id },
data: { role: 'accountant' },
data: { role: "accountant" },
});

const updatedUser = await prisma.user.findUnique({
where: { id: normalUser.id },
});

expect(updatedUser?.role).toBe('accountant');
expect(updatedUser?.role).toBe("accountant");

// Change back to none
await prisma.user.update({
where: { id: normalUser.id },
data: { role: 'none' },
data: { role: "none" },
});
});
});
Expand Down
16 changes: 5 additions & 11 deletions apps/backend-e2e/src/support/auth-helper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import axios from 'axios';
import { PrismaClient } from '@prisma/client';
import axios from "axios";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
const prisma = new PrismaClient();

export interface TestUser {
id: number;
Expand All @@ -22,7 +16,7 @@ export interface TestUser {
*/
export async function createTestUserWithSession(): Promise<TestUser> {
const testEmail = `test-${Date.now()}@example.com`;
const testName = 'Test User';
const testName = "Test User";

// Create user
const user = await prisma.user.create({
Expand Down Expand Up @@ -66,7 +60,7 @@ export async function cleanupTestUsers(): Promise<void> {
await prisma.user.deleteMany({
where: {
email: {
startsWith: 'test-',
startsWith: "test-",
},
},
});
Expand Down
16 changes: 8 additions & 8 deletions apps/backend-e2e/src/support/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { waitForPortOpen } from '@nx/node/utils';
import { spawn } from 'child_process';
import { waitForPortOpen } from "@nx/node/utils";
import { spawn } from "child_process";

/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;

module.exports = async function () {
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
console.log('\nSetting up...\n');
console.log("\nSetting up...\n");

const host = process.env.HOST ?? 'localhost';
const host = process.env.HOST ?? "localhost";
const port = process.env.PORT ? Number(process.env.PORT) : 3000;

// Start the backend server
console.log(`Starting backend server on port ${port}...`);
const serverProcess = spawn('npx', ['nx', 'serve', 'backend'], {
const serverProcess = spawn("npx", ["nx", "serve", "backend"], {
env: { ...process.env, PORT: port.toString() },
detached: false,
stdio: 'ignore',
stdio: "ignore",
});

// Wait for the server to be ready
await waitForPortOpen(port, { host, timeout: 60000 });
await waitForPortOpen(port, { host, retries: 60, retryDelay: 1000 });
console.log(`Backend server is ready on port ${port}`);

// Store the server process for cleanup in teardown
globalThis.__SERVER_PROCESS__ = serverProcess;
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
globalThis.__TEARDOWN_MESSAGE__ = "\nTearing down...\n";
};
56 changes: 28 additions & 28 deletions apps/backend/src/app/admin/admin.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { AdminController } from './admin.controller';
import { AuthGuard } from '@thallesp/nestjs-better-auth';
import { RolesGuard } from '../../common/auth';
import { describe, it, expect, beforeEach } from "@jest/globals";
import { Test, TestingModule } from "@nestjs/testing";
import { AdminController } from "./admin.controller";
import { AuthGuard } from "@thallesp/nestjs-better-auth";
import { RolesGuard } from "../../common/auth/roles.guard";

describe('AdminController', () => {
describe("AdminController", () => {
let controller: AdminController;

beforeEach(async () => {
Expand All @@ -20,44 +20,44 @@ describe('AdminController', () => {
controller = module.get<AdminController>(AdminController);
});

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

describe('getDashboard', () => {
it('should return admin dashboard data', () => {
describe("getDashboard", () => {
it("should return admin dashboard data", () => {
const result = controller.getDashboard();
expect(result).toHaveProperty('message');
expect(result).toHaveProperty('stats');
expect(result.stats).toHaveProperty('totalUsers');
expect(result.stats).toHaveProperty('activeChats');
expect(result.stats).toHaveProperty('systemHealth');
expect(result).toHaveProperty("message");
expect(result).toHaveProperty("stats");
expect(result.stats).toHaveProperty("totalUsers");
expect(result.stats).toHaveProperty("activeChats");
expect(result.stats).toHaveProperty("systemHealth");
});

it('should return expected dashboard structure', () => {
it("should return expected dashboard structure", () => {
const result = controller.getDashboard();
expect(result.message).toBe('Welcome to the admin dashboard');
expect(typeof result.stats.totalUsers).toBe('number');
expect(typeof result.stats.activeChats).toBe('number');
expect(typeof result.stats.systemHealth).toBe('string');
expect(result.message).toBe("Welcome to the admin dashboard");
expect(typeof result.stats.totalUsers).toBe("number");
expect(typeof result.stats.activeChats).toBe("number");
expect(typeof result.stats.systemHealth).toBe("string");
});
});

describe('getReports', () => {
it('should return financial reports', () => {
describe("getReports", () => {
it("should return financial reports", () => {
const result = controller.getReports();
expect(result).toHaveProperty('message');
expect(result).toHaveProperty('reports');
expect(result).toHaveProperty("message");
expect(result).toHaveProperty("reports");
expect(Array.isArray(result.reports)).toBe(true);
});

it('should return reports with correct structure', () => {
it("should return reports with correct structure", () => {
const result = controller.getReports();
expect(result.reports.length).toBeGreaterThan(0);
result.reports.forEach((report) => {
expect(report).toHaveProperty('id');
expect(report).toHaveProperty('name');
expect(report).toHaveProperty('date');
result.reports.forEach((report: any) => {
expect(report).toHaveProperty("id");
expect(report).toHaveProperty("name");
expect(report).toHaveProperty("date");
});
});
});
Expand Down
Loading