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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
final_remediation_report.md
analysis_report.md
54 changes: 0 additions & 54 deletions analysis_report.md

This file was deleted.

127 changes: 127 additions & 0 deletions backend/src/__tests__/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# MusicApp — Backend Unit Tests

> **Testing Framework:** [Vitest](https://vitest.dev/) v4.1.1
> **Language:** TypeScript
> **Location:** `backend/src/__tests__/`

---

## 📁 Test Structure

```
backend/
└── src/
└── __tests__/
├── admin.controller.test.ts # RBAC / Admin auth tests
├── user.controller.test.ts # User listing & message fetch tests
├── message.controller.test.ts # Message send & unread count tests
└── README.md # This file
```

---

## 🚀 Running Tests

From the `backend/` directory:

```bash
# Run all tests once
npm test

# Run tests in watch mode
npx vitest

# Run a specific test file
npx vitest run src/__tests__/admin.controller.test.ts
```

---

## 📊 Test Suites

### `admin.controller.test.ts` — 3 tests
Tests the `checkAdmin` endpoint which verifies if the authenticated user is an admin using Clerk's user management API.

| Test | What it covers |
|------|---------------|
| Returns 401 if no userId | Unauthenticated request guard |
| Returns `admin: true` for matching email | Correct admin identification |
| Returns `admin: false` for non-admin email | Non-admin correctly rejected |

**Key challenge:** Clerk SDK validation requires a live key, so we use a monkey-patching strategy to intercept `clerkClient.users.getUser` directly at runtime — bypassing the CommonJS module cache.

---

### `user.controller.test.ts` — 8 tests
Tests `getAllUser` and `getMessage` controller functions.

| Test | What it covers |
|------|---------------|
| Returns 200 with user list | Happy path — excludes current user |
| Returns empty array | Edge case — only user in system |
| Calls `next(err)` on DB failure | Error propagation |
| Calls `next()` if auth is null | Unauthenticated request guard |
| Returns 200 with messages | Correct query for message history |
| Returns empty array for no messages | Edge case — no conversation yet |
| Calls `next(err)` on DB failure | Error propagation |
| Sorts by `createdAt` ascending | Chronological message ordering |

---

### `message.controller.test.ts` — 7 tests
Tests `sendMessage` (input validation) and `getUnreadCount`.

| Test | What it covers |
|------|---------------|
| Returns 400 if receiverId missing | Required field validation |
| Returns 400 if content is empty | Required field validation |
| Returns 400 for whitespace-only content | Whitespace trim guard |
| Returns 400 if receiverId is empty | Required field validation |
| Returns 200 with count | Happy path — unread messages |
| Returns count of 0 | Edge case — all messages read |
| Calls `next(err)` on DB error | Error propagation |

---

## 🧰 Mocking Strategy

> **Why we don't use `vi.mock()` the normal way for Clerk**

This project uses [Clerk](https://clerk.com/) for authentication. The Clerk SDK (`@clerk/express`) bundles `@clerk/backend` internally, which means standard ES module mocking (`vi.mock('@clerk/backend')`) does **not** intercept calls because the bundled version is already resolved.

**Solution — Monkey-patching:**
We directly override the `clerkClient.users.getUser` function *after* importing the library, forcing our mock into the exact same runtime reference the controller uses:

```ts
const clerk = require('@clerk/express');
clerk.clerkClient.users.getUser = vi.fn().mockResolvedValue({ ... });
```

This is the only reliable approach in a CommonJS environment with bundled dependencies.

Similarly, Mongoose models export via `module.exports = Model` (no `.default`), so we import them with `require()` and mutate their methods directly.

---

## ⚙️ Configuration

`vitest.config.js` is at the `backend/` root and targets this folder:

```js
test: {
include: ['src/__tests__/**/*.test.{js,ts}'],
}
```

---

## 📈 Current Results

```
Test Files 3 passed (3)
Tests 18 passed (18)
Pass Rate 100%
Duration ~3.8s
```

Open [`../../report/test-report.html`](../../report/test-report.html) in a browser for the full visual report.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';

const clerk = require('@clerk/express');
const { checkAdmin } = require('./admin.controller');
const { checkAdmin } = require('../controllers/admin.controller');

describe('admin.controller - checkAdmin', () => {
let req: any, res: any, next: any;

beforeEach(() => {
vi.stubEnv('CLERK_SECRET_KEY', 'sk_test_51MzS2XSCXk1fP8BkJZ1t2y3u4v5w6x7y8z9a');
vi.stubEnv('ADMIN_EMAIL', 'admin@example.com');
// Monkey-patch the library directly

// Monkey-patch the Clerk library directly (required due to CommonJS module resolution)
clerk.clerkClient.users.getUser = vi.fn();

req = {
Expand Down
103 changes: 103 additions & 0 deletions backend/src/__tests__/message.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';

// ─── Top-level mocks ──────────────────────────────────────────
const mockGetUnreadCount = vi.fn();
const mockMessageFind = vi.fn();

vi.mock('../models/message.model', () => ({
default: undefined,
find: mockMessageFind,
getUnreadCount: mockGetUnreadCount,
}));

vi.mock('../models/user.model', () => ({
default: undefined,
findOne: vi.fn().mockResolvedValue({ fullName: 'Test Sender' }),
}));

vi.mock('../models/notification.model', () => {
function MockNotification(data: any) {
Object.assign(this, data);
(this as any)._id = 'notif_mock_id';
(this as any).createdAt = new Date();
(this as any).save = vi.fn().mockResolvedValue(this);
}
MockNotification.getUnreadCount = vi.fn().mockResolvedValue(0);
return { default: MockNotification };
});

const Message = require('../models/message.model');
const { sendMessage, getUnreadCount } = require('../controllers/message.controller');

describe('message.controller', () => {
let req: any, res: any, next: any;

beforeEach(() => {
req = {
auth: { userId: 'clerk_sender_123' },
body: { receiverId: 'clerk_receiver_456', content: 'Hello World' },
app: { get: vi.fn().mockReturnValue(null) },
};
res = {
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
};
next = vi.fn();
vi.clearAllMocks();
});

// ─────────────────────────────────────────────
// sendMessage — Input Validation
// ─────────────────────────────────────────────
describe('sendMessage', () => {
it('should return 400 if receiverId is missing', async () => {
req.body = { content: 'Hello' };
await sendMessage(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
});

it('should return 400 if content is an empty string', async () => {
req.body = { receiverId: 'clerk_receiver_456', content: '' };
await sendMessage(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
});

it('should return 400 if content is only whitespace', async () => {
req.body = { receiverId: 'clerk_receiver_456', content: ' ' };
await sendMessage(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
});

it('should return 400 if receiverId is an empty string', async () => {
req.body = { receiverId: '', content: 'Hello' };
await sendMessage(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
});
});

// ─────────────────────────────────────────────
// getUnreadCount
// ─────────────────────────────────────────────
describe('getUnreadCount', () => {
it('should return 200 with unread count for authenticated user', async () => {
Message.getUnreadCount = vi.fn().mockResolvedValue(5);
await getUnreadCount(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: true, count: 5 }));
});

it('should return count of 0 when no unread messages exist', async () => {
Message.getUnreadCount = vi.fn().mockResolvedValue(0);
await getUnreadCount(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ count: 0 }));
});

it('should call next() when the database throws an error', async () => {
Message.getUnreadCount = vi.fn().mockRejectedValue(new Error('DB Error'));
await getUnreadCount(req, res, next);
expect(next).toHaveBeenCalled();
});
});
});
Loading
Loading