Add comprehensive auth middleware test coverage for scraper routes
All checks were successful
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 13s
CI/CD Pipeline - Apartment API / Lint & Test (pull_request) Successful in 41s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Successful in 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
All checks were successful
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 13s
CI/CD Pipeline - Apartment API / Lint & Test (pull_request) Successful in 41s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Successful in 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
Add 29 new tests verifying requireAuth and requireAdmin middleware behavior across all 3 scraper endpoints (POST /run, GET /status, GET /history). Test coverage includes: - Missing JWT token returns 401 - Invalid JWT token returns 401 - Expired JWT token returns 401 - Token signed with wrong secret returns 401 - Disabled user returns 401 - Non-existent user returns 401 - Non-admin user returns 403 - Valid admin user receives expected 200/202 responses - Middleware ordering: auth failures return 401 before admin check Total scraperRoutes tests: 81 (52 existing + 29 new)
This commit is contained in:
@ -33,6 +33,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const request = require('supertest');
|
const request = require('supertest');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
const { MongoClient, ObjectId } = require('mongodb');
|
const { MongoClient, ObjectId } = require('mongodb');
|
||||||
const {
|
const {
|
||||||
createTestApp,
|
createTestApp,
|
||||||
@ -1333,4 +1334,223 @@ describe('Scraper Routes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Authentication & Authorization Middleware Tests (SCRAPE-21)
|
||||||
|
// Comprehensive tests for requireAuth + requireAdmin on all
|
||||||
|
// /admin/scraper/* routes
|
||||||
|
// ============================================================
|
||||||
|
describe('Authentication & Authorization Middleware', () => {
|
||||||
|
|
||||||
|
const scraperRoutes = [
|
||||||
|
{ method: 'post', path: '/api/admin/scraper/run', expectedAdminStatus: 202 },
|
||||||
|
{ method: 'get', path: '/api/admin/scraper/status', expectedAdminStatus: 200 },
|
||||||
|
{ method: 'get', path: '/api/admin/scraper/history', expectedAdminStatus: 200 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Missing JWT token
|
||||||
|
// ============================================================
|
||||||
|
describe('missing JWT token (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} without auth token`, async () => {
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Invalid JWT token
|
||||||
|
// ============================================================
|
||||||
|
describe('invalid JWT token (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} with invalid token`, async () => {
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', ['auth_token=this-is-not-a-valid-jwt-token'])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Invalid token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Expired JWT token
|
||||||
|
// ============================================================
|
||||||
|
describe('expired JWT token (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} with expired token`, async () => {
|
||||||
|
const adminUser = createTestAdmin();
|
||||||
|
await insertTestUser(db, adminUser);
|
||||||
|
|
||||||
|
// Create a token that expired 1 hour ago
|
||||||
|
const secret = process.env.JWT_SECRET || 'test-jwt-secret-for-testing-only';
|
||||||
|
const expiredToken = jwt.sign(
|
||||||
|
{ userId: adminUser._id.toString() },
|
||||||
|
secret,
|
||||||
|
{ expiresIn: '-1h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${expiredToken}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Invalid token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Token signed with wrong secret
|
||||||
|
// ============================================================
|
||||||
|
describe('token signed with wrong secret (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} with wrong-secret token`, async () => {
|
||||||
|
const adminUser = createTestAdmin();
|
||||||
|
await insertTestUser(db, adminUser);
|
||||||
|
|
||||||
|
// Sign token with a different secret
|
||||||
|
const wrongSecretToken = jwt.sign(
|
||||||
|
{ userId: adminUser._id.toString() },
|
||||||
|
'completely-wrong-secret-key',
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${wrongSecretToken}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Invalid token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Disabled user (isActive: false)
|
||||||
|
// ============================================================
|
||||||
|
describe('disabled user (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} when user is disabled`, async () => {
|
||||||
|
const disabledAdmin = createTestAdmin({ isActive: false });
|
||||||
|
await insertTestUser(db, disabledAdmin);
|
||||||
|
const token = generateTestToken(disabledAdmin._id);
|
||||||
|
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${token}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 401 - Token for non-existent user
|
||||||
|
// ============================================================
|
||||||
|
describe('non-existent user (401)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 401 for ${method.toUpperCase()} ${path} when user does not exist`, async () => {
|
||||||
|
const nonExistentId = new ObjectId();
|
||||||
|
const token = generateTestToken(nonExistentId);
|
||||||
|
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${token}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 403 - Non-admin user
|
||||||
|
// ============================================================
|
||||||
|
describe('non-admin user (403)', () => {
|
||||||
|
scraperRoutes.forEach(({ method, path }) => {
|
||||||
|
it(`should return 403 for ${method.toUpperCase()} ${path} for non-admin user`, async () => {
|
||||||
|
const regularUser = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, regularUser);
|
||||||
|
const token = generateTestToken(regularUser._id);
|
||||||
|
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${token}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
expect(res.body.error).toBe('Admin access required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 200/202 - Valid admin user
|
||||||
|
// ============================================================
|
||||||
|
describe('valid admin user (200/202)', () => {
|
||||||
|
let adminUser;
|
||||||
|
let adminToken;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
adminUser = createTestAdmin();
|
||||||
|
await insertTestUser(db, adminUser);
|
||||||
|
adminToken = generateTestToken(adminUser._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
scraperRoutes.forEach(({ method, path, expectedAdminStatus }) => {
|
||||||
|
it(`should return ${expectedAdminStatus} for ${method.toUpperCase()} ${path} for admin user`, async () => {
|
||||||
|
const res = await request(app)[method](path)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send(method === 'post' ? {} : undefined);
|
||||||
|
|
||||||
|
expect(res.status).toBe(expectedAdminStatus);
|
||||||
|
expect(res.body).toHaveProperty('data');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Middleware ordering: requireAuth runs before requireAdmin
|
||||||
|
// ============================================================
|
||||||
|
describe('middleware ordering', () => {
|
||||||
|
it('should return 401 (not 403) when token is missing, even for non-admin scenario', async () => {
|
||||||
|
// Without any token, requireAuth should reject with 401
|
||||||
|
// before requireAdmin ever runs
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/admin/scraper/status');
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 (not 403) for disabled admin user', async () => {
|
||||||
|
// A disabled admin should get 401 from requireAuth
|
||||||
|
// even though they have the admin role
|
||||||
|
const disabledAdmin = createTestAdmin({ isActive: false, role: 'admin' });
|
||||||
|
await insertTestUser(db, disabledAdmin);
|
||||||
|
const token = generateTestToken(disabledAdmin._id);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/admin/scraper/status')
|
||||||
|
.set('Cookie', [`auth_token=${token}`]);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user