Merge dev: Node.js scraper migration + CI fix #32

Merged
stephen merged 32 commits from dev into main 2026-02-08 20:35:29 -07:00
23 changed files with 11039 additions and 25 deletions
Showing only changes of commit a1ee25ef17 - Show all commits

View File

@ -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');
});
});
});
}); });