Admin Dashboard Backend (Phases 1-4) #5
156
__tests__/helpers/testHelpers.js
Normal file
156
__tests__/helpers/testHelpers.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
const { MongoClient, ObjectId } = require('mongodb');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
// Counter for generating unique identifiers within the same millisecond
|
||||||
|
let uniqueCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test Express app instance with database connection
|
||||||
|
* This creates a minimal Express app for testing admin routes
|
||||||
|
*/
|
||||||
|
async function createTestApp(db) {
|
||||||
|
const express = require('express');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
// Store db in app.locals for middleware access
|
||||||
|
app.locals.db = db;
|
||||||
|
|
||||||
|
// Mount the auth middleware module
|
||||||
|
const { requireAuth, requireAdmin } = require('../../middleware/auth');
|
||||||
|
|
||||||
|
// Health check endpoint (should remain public)
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to mount admin routes if they exist
|
||||||
|
try {
|
||||||
|
const adminRoutes = require('../../routes/admin');
|
||||||
|
app.use('/api/admin', requireAuth, requireAdmin, adminRoutes);
|
||||||
|
} catch (error) {
|
||||||
|
// Admin routes don't exist yet - this is expected for failing tests
|
||||||
|
// Create placeholder routes that will 404
|
||||||
|
app.use('/api/admin', requireAuth, requireAdmin, (req, res) => {
|
||||||
|
res.status(404).json({ error: 'Admin routes not implemented' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a valid JWT token for a test user
|
||||||
|
* @param {string|ObjectId} userId - The user's MongoDB _id
|
||||||
|
* @returns {string} JWT token
|
||||||
|
*/
|
||||||
|
function generateTestToken(userId) {
|
||||||
|
const secret = process.env.JWT_SECRET || 'test-jwt-secret-for-testing-only';
|
||||||
|
return jwt.sign(
|
||||||
|
{ userId: userId.toString() },
|
||||||
|
secret,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test user document
|
||||||
|
* @param {Object} overrides - Fields to override in the default user
|
||||||
|
* @returns {Object} User document
|
||||||
|
*/
|
||||||
|
function createTestUser(overrides = {}) {
|
||||||
|
const now = new Date();
|
||||||
|
const uniqueId = `${Date.now()}-${++uniqueCounter}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
return {
|
||||||
|
_id: new ObjectId(),
|
||||||
|
googleId: `google-${uniqueId}`,
|
||||||
|
email: `test-${uniqueId}@example.com`,
|
||||||
|
name: 'Test User',
|
||||||
|
picture: 'https://example.com/photo.jpg',
|
||||||
|
role: 'user',
|
||||||
|
isActive: true,
|
||||||
|
loginCount: 1,
|
||||||
|
createdAt: now,
|
||||||
|
lastLoginAt: now,
|
||||||
|
disabledAt: null,
|
||||||
|
disabledBy: null,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an admin user document
|
||||||
|
* @param {Object} overrides - Fields to override in the default admin
|
||||||
|
* @returns {Object} Admin user document
|
||||||
|
*/
|
||||||
|
function createTestAdmin(overrides = {}) {
|
||||||
|
const uniqueId = `${Date.now()}-${++uniqueCounter}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
return createTestUser({
|
||||||
|
role: 'admin',
|
||||||
|
email: `admin-${uniqueId}@example.com`,
|
||||||
|
name: 'Test Admin',
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a user into the database
|
||||||
|
* @param {Db} db - MongoDB database instance
|
||||||
|
* @param {Object} user - User document to insert
|
||||||
|
* @returns {Promise<Object>} Inserted user with _id
|
||||||
|
*/
|
||||||
|
async function insertTestUser(db, user) {
|
||||||
|
const result = await db.collection('users').insertOne(user);
|
||||||
|
return { ...user, _id: result.insertedId };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up test users from the database
|
||||||
|
* @param {Db} db - MongoDB database instance
|
||||||
|
*/
|
||||||
|
async function cleanupTestUsers(db) {
|
||||||
|
await db.collection('users').deleteMany({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all test data from the database
|
||||||
|
* @param {Db} db - MongoDB database instance
|
||||||
|
*/
|
||||||
|
async function cleanupTestData(db) {
|
||||||
|
await Promise.all([
|
||||||
|
db.collection('users').deleteMany({}),
|
||||||
|
db.collection('user_activity').deleteMany({})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test activity document
|
||||||
|
* @param {Object} overrides - Fields to override in the default activity
|
||||||
|
* @returns {Object} Activity document
|
||||||
|
*/
|
||||||
|
function createTestActivity(overrides = {}) {
|
||||||
|
const now = new Date();
|
||||||
|
return {
|
||||||
|
_id: new ObjectId(),
|
||||||
|
userId: new ObjectId(),
|
||||||
|
action: 'PAGE_VIEW',
|
||||||
|
metadata: { path: '/test' },
|
||||||
|
timestamp: now,
|
||||||
|
sessionId: `session-${Date.now()}`,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createTestApp,
|
||||||
|
generateTestToken,
|
||||||
|
createTestUser,
|
||||||
|
createTestAdmin,
|
||||||
|
insertTestUser,
|
||||||
|
cleanupTestUsers,
|
||||||
|
cleanupTestData,
|
||||||
|
createTestActivity
|
||||||
|
};
|
||||||
905
__tests__/phase1-foundation.test.js
Normal file
905
__tests__/phase1-foundation.test.js
Normal file
@ -0,0 +1,905 @@
|
|||||||
|
/**
|
||||||
|
* Phase 1: Foundation Tests for Admin Dashboard
|
||||||
|
*
|
||||||
|
* These tests verify the implementation of Phase 1 requirements from ADMIN.md:
|
||||||
|
* - 1.1 Database Schema (DB-1.x): User role field, disabledAt/disabledBy fields, indexes
|
||||||
|
* - 1.2 Middleware (MW-1.x): requireAdmin middleware functionality
|
||||||
|
* - 1.3 User Management API (API-1.x): CRUD operations for admin user management
|
||||||
|
*
|
||||||
|
* These tests are designed to FAIL initially as the implementation does not exist yet.
|
||||||
|
* Run with: npm test
|
||||||
|
*/
|
||||||
|
|
||||||
|
const request = require('supertest');
|
||||||
|
const { MongoClient, ObjectId } = require('mongodb');
|
||||||
|
const {
|
||||||
|
createTestApp,
|
||||||
|
generateTestToken,
|
||||||
|
createTestUser,
|
||||||
|
createTestAdmin,
|
||||||
|
insertTestUser,
|
||||||
|
cleanupTestUsers,
|
||||||
|
cleanupTestData
|
||||||
|
} = require('./helpers/testHelpers');
|
||||||
|
|
||||||
|
describe('Phase 1: Foundation', () => {
|
||||||
|
let connection;
|
||||||
|
let db;
|
||||||
|
let app;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Connect to the in-memory MongoDB instance
|
||||||
|
const uri = process.env.MONGO_URI;
|
||||||
|
connection = await MongoClient.connect(uri);
|
||||||
|
db = connection.db('apartments_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (connection) {
|
||||||
|
await connection.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clean up test data before each test
|
||||||
|
await cleanupTestData(db);
|
||||||
|
// Create fresh app instance for each test
|
||||||
|
app = await createTestApp(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.1 DATABASE SCHEMA TESTS
|
||||||
|
// These tests verify the user schema has been properly updated for admin features
|
||||||
|
// =============================================================================
|
||||||
|
describe('1.1 Database Schema', () => {
|
||||||
|
/**
|
||||||
|
* DB-1.1: Users must have a `role` field
|
||||||
|
* Valid values: 'user' | 'admin'
|
||||||
|
* Default: 'user'
|
||||||
|
*/
|
||||||
|
describe('DB-1.1: User role field', () => {
|
||||||
|
it('should have role field with default value "user" for new users', async () => {
|
||||||
|
// When a new user is created without specifying role
|
||||||
|
const user = createTestUser();
|
||||||
|
delete user.role; // Remove role to test default
|
||||||
|
|
||||||
|
const { findOrCreateUser } = require('../models/user');
|
||||||
|
|
||||||
|
// Create a user profile to simulate OAuth flow
|
||||||
|
const profile = {
|
||||||
|
googleId: user.googleId,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
picture: user.picture
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdUser = await findOrCreateUser(db, profile);
|
||||||
|
|
||||||
|
// Then the role should default to 'user'
|
||||||
|
expect(createdUser).toBeDefined();
|
||||||
|
expect(createdUser.role).toBe('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only allow "user" or "admin" as valid role values', async () => {
|
||||||
|
// Attempt to insert a user with an invalid role
|
||||||
|
const userWithInvalidRole = createTestUser({ role: 'superadmin' });
|
||||||
|
|
||||||
|
// This test verifies schema validation exists
|
||||||
|
// The exact implementation depends on whether validation is done at
|
||||||
|
// the application level or database level
|
||||||
|
await expect(async () => {
|
||||||
|
await db.collection('users').insertOne(userWithInvalidRole);
|
||||||
|
// Query the user back to verify role validation
|
||||||
|
const inserted = await db.collection('users').findOne({ _id: userWithInvalidRole._id });
|
||||||
|
// If no validation exists, this should be caught by application logic
|
||||||
|
if (inserted.role !== 'user' && inserted.role !== 'admin') {
|
||||||
|
throw new Error('Invalid role should not be allowed');
|
||||||
|
}
|
||||||
|
}).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB-1.2: Users must have `disabledAt` field
|
||||||
|
* Type: Date | null
|
||||||
|
* Set when user is disabled, null when active
|
||||||
|
*/
|
||||||
|
describe('DB-1.2: User disabledAt field', () => {
|
||||||
|
it('should have disabledAt field set to null for active users', async () => {
|
||||||
|
const user = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
|
||||||
|
const foundUser = await db.collection('users').findOne({ _id: user._id });
|
||||||
|
|
||||||
|
expect(foundUser.disabledAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have disabledAt field set to Date when user is disabled', async () => {
|
||||||
|
const user = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
|
||||||
|
// Import the user model function that handles disabling
|
||||||
|
const { setUserActive } = require('../models/user');
|
||||||
|
|
||||||
|
// Disable the user (this should set disabledAt)
|
||||||
|
const disabledUser = await setUserActive(db, user._id, false);
|
||||||
|
|
||||||
|
expect(disabledUser.isActive).toBe(false);
|
||||||
|
expect(disabledUser.disabledAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB-1.3: Users must have `disabledBy` field
|
||||||
|
* Type: ObjectId | null
|
||||||
|
* References the admin who disabled the user
|
||||||
|
*/
|
||||||
|
describe('DB-1.3: User disabledBy field', () => {
|
||||||
|
it('should have disabledBy field set to null for active users', async () => {
|
||||||
|
const user = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
|
||||||
|
const foundUser = await db.collection('users').findOne({ _id: user._id });
|
||||||
|
|
||||||
|
expect(foundUser.disabledBy).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have disabledBy field set to admin ObjectId when disabled', async () => {
|
||||||
|
const admin = createTestAdmin();
|
||||||
|
const user = createTestUser();
|
||||||
|
await insertTestUser(db, admin);
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
|
||||||
|
// Import the updated user model function that accepts disabledBy
|
||||||
|
const { setUserActive } = require('../models/user');
|
||||||
|
|
||||||
|
// Disable the user with admin reference
|
||||||
|
const disabledUser = await setUserActive(db, user._id, false, admin._id);
|
||||||
|
|
||||||
|
expect(disabledUser.isActive).toBe(false);
|
||||||
|
expect(disabledUser.disabledBy).toEqual(admin._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB-1.4: Required indexes for admin queries
|
||||||
|
* Indexes needed:
|
||||||
|
* - { role: 1 }
|
||||||
|
* - { isActive: 1, role: 1 }
|
||||||
|
* - { createdAt: -1 }
|
||||||
|
* - { lastLoginAt: -1 }
|
||||||
|
*/
|
||||||
|
describe('DB-1.4: Required indexes', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create indexes (this should be done by createIndexes function)
|
||||||
|
const { createIndexes } = require('../models/user');
|
||||||
|
await createIndexes(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have index on role field', async () => {
|
||||||
|
const indexes = await db.collection('users').indexes();
|
||||||
|
const roleIndex = indexes.find(idx =>
|
||||||
|
idx.key && idx.key.role === 1 && Object.keys(idx.key).length === 1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(roleIndex).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have compound index on isActive and role', async () => {
|
||||||
|
const indexes = await db.collection('users').indexes();
|
||||||
|
const compoundIndex = indexes.find(idx =>
|
||||||
|
idx.key && idx.key.isActive === 1 && idx.key.role === 1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(compoundIndex).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have index on createdAt descending', async () => {
|
||||||
|
const indexes = await db.collection('users').indexes();
|
||||||
|
const createdAtIndex = indexes.find(idx =>
|
||||||
|
idx.key && idx.key.createdAt === -1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(createdAtIndex).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have index on lastLoginAt descending', async () => {
|
||||||
|
const indexes = await db.collection('users').indexes();
|
||||||
|
const lastLoginIndex = indexes.find(idx =>
|
||||||
|
idx.key && idx.key.lastLoginAt === -1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastLoginIndex).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.2 MIDDLEWARE TESTS
|
||||||
|
// These tests verify the requireAdmin middleware behaves correctly
|
||||||
|
// =============================================================================
|
||||||
|
describe('1.2 Middleware', () => {
|
||||||
|
/**
|
||||||
|
* MW-1.1: requireAdmin middleware must exist
|
||||||
|
*/
|
||||||
|
describe('MW-1.1: requireAdmin middleware exists', () => {
|
||||||
|
it('should export requireAdmin middleware from auth module', () => {
|
||||||
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
|
|
||||||
|
expect(requireAdmin).toBeDefined();
|
||||||
|
expect(typeof requireAdmin).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MW-1.2: requireAdmin returns 403 if user.role !== 'admin'
|
||||||
|
*/
|
||||||
|
describe('MW-1.2: requireAdmin authorization', () => {
|
||||||
|
it('should return 403 with "Admin access required" for non-admin users', async () => {
|
||||||
|
// Create a regular user (not admin)
|
||||||
|
const user = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
const token = generateTestToken(user._id);
|
||||||
|
|
||||||
|
// Attempt to access an admin endpoint
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${token}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body.error).toBe('Admin access required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow access for users with admin role', async () => {
|
||||||
|
// Create an admin user
|
||||||
|
const admin = createTestAdmin();
|
||||||
|
await insertTestUser(db, admin);
|
||||||
|
const token = generateTestToken(admin._id);
|
||||||
|
|
||||||
|
// Access admin endpoint - should not get 403
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${token}`]);
|
||||||
|
|
||||||
|
// Should either succeed (200) or 404 if routes not implemented
|
||||||
|
// but NOT 403 (forbidden)
|
||||||
|
expect(response.status).not.toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MW-1.3: requireAdmin must work after requireAuth
|
||||||
|
*/
|
||||||
|
describe('MW-1.3: Middleware chaining', () => {
|
||||||
|
it('should return 401 if no authentication token provided', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users');
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body.error).toBe('Authentication required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 for invalid token before checking admin role', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', ['auth_token=invalid-token']);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 for disabled users even if they have admin role', async () => {
|
||||||
|
// Create a disabled admin
|
||||||
|
const disabledAdmin = createTestAdmin({ isActive: false });
|
||||||
|
await insertTestUser(db, disabledAdmin);
|
||||||
|
const token = generateTestToken(disabledAdmin._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${token}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MW-1.4: /api/health must remain public
|
||||||
|
*/
|
||||||
|
describe('MW-1.4: Health endpoint remains public', () => {
|
||||||
|
it('should return 200 on /api/health without authentication', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/health');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.status).toBe('ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not require admin role for /api/health', async () => {
|
||||||
|
// Regular user accessing health endpoint
|
||||||
|
const user = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, user);
|
||||||
|
const token = generateTestToken(user._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/health')
|
||||||
|
.set('Cookie', [`auth_token=${token}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1.3 USER MANAGEMENT API TESTS
|
||||||
|
// These tests verify the admin user management endpoints
|
||||||
|
// =============================================================================
|
||||||
|
describe('1.3 User Management API', () => {
|
||||||
|
let adminUser;
|
||||||
|
let adminToken;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create an admin user for testing admin endpoints
|
||||||
|
adminUser = createTestAdmin();
|
||||||
|
await insertTestUser(db, adminUser);
|
||||||
|
adminToken = generateTestToken(adminUser._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-1.1: GET /api/admin/users - Paginated user list
|
||||||
|
*/
|
||||||
|
describe('API-1.1: GET /api/admin/users', () => {
|
||||||
|
it('should return paginated list of users', async () => {
|
||||||
|
// Create some test users
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
await insertTestUser(db, createTestUser({
|
||||||
|
email: `user${i}@example.com`,
|
||||||
|
name: `User ${i}`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.users).toBeDefined();
|
||||||
|
expect(Array.isArray(response.body.users)).toBe(true);
|
||||||
|
expect(response.body.pagination).toBeDefined();
|
||||||
|
expect(response.body.pagination.page).toBe(1);
|
||||||
|
expect(response.body.pagination.limit).toBe(20);
|
||||||
|
expect(response.body.pagination.total).toBeGreaterThan(0);
|
||||||
|
expect(response.body.pagination.pages).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support page parameter', async () => {
|
||||||
|
// Create 30 test users
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
await insertTestUser(db, createTestUser({
|
||||||
|
email: `user${i}@example.com`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?page=2')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.pagination.page).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support limit parameter with max 100', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?limit=50')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.pagination.limit).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cap limit at 100', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?limit=200')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.pagination.limit).toBeLessThanOrEqual(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support search parameter for name', async () => {
|
||||||
|
await insertTestUser(db, createTestUser({ name: 'John Smith' }));
|
||||||
|
await insertTestUser(db, createTestUser({ name: 'Jane Doe' }));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?search=John')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.users.some(u => u.name.includes('John'))).toBe(true);
|
||||||
|
expect(response.body.users.every(u =>
|
||||||
|
u.name.toLowerCase().includes('john') ||
|
||||||
|
u.email.toLowerCase().includes('john')
|
||||||
|
)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support search parameter for email', async () => {
|
||||||
|
await insertTestUser(db, createTestUser({ email: 'unique-test@example.com' }));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?search=unique-test')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.users.some(u => u.email.includes('unique-test'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support status filter "active"', async () => {
|
||||||
|
await insertTestUser(db, createTestUser({ isActive: true }));
|
||||||
|
await insertTestUser(db, createTestUser({ isActive: false }));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?status=active')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.users.every(u => u.isActive === true)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support status filter "disabled"', async () => {
|
||||||
|
await insertTestUser(db, createTestUser({ isActive: false }));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?status=disabled')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.users.every(u => u.isActive === false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support sort parameter', async () => {
|
||||||
|
const oldUser = createTestUser({
|
||||||
|
createdAt: new Date('2023-01-01'),
|
||||||
|
email: 'old@example.com'
|
||||||
|
});
|
||||||
|
const newUser = createTestUser({
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
email: 'new@example.com'
|
||||||
|
});
|
||||||
|
await insertTestUser(db, oldUser);
|
||||||
|
await insertTestUser(db, newUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?sort=createdAt&order=desc')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
// Newest first when sorted descending
|
||||||
|
const createdDates = response.body.users.map(u => new Date(u.createdAt));
|
||||||
|
for (let i = 0; i < createdDates.length - 1; i++) {
|
||||||
|
expect(createdDates[i] >= createdDates[i + 1]).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support order parameter "asc" and "desc"', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users?sort=loginCount&order=asc')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
// Verify ascending order
|
||||||
|
const counts = response.body.users.map(u => u.loginCount);
|
||||||
|
for (let i = 0; i < counts.length - 1; i++) {
|
||||||
|
expect(counts[i] <= counts[i + 1]).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role', async () => {
|
||||||
|
const regularUser = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, regularUser);
|
||||||
|
const userToken = generateTestToken(regularUser._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${userToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-1.2: GET /api/admin/users/:id - User details with recent activity
|
||||||
|
*/
|
||||||
|
describe('API-1.2: GET /api/admin/users/:id', () => {
|
||||||
|
it('should return user details for valid user ID', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toBeDefined();
|
||||||
|
expect(response.body.user._id.toString()).toBe(testUser._id.toString());
|
||||||
|
expect(response.body.user.email).toBe(testUser.email);
|
||||||
|
expect(response.body.user.name).toBe(testUser.name);
|
||||||
|
expect(response.body.user.role).toBe(testUser.role);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include recent activity for the user', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
// Create some activity for this user
|
||||||
|
await db.collection('user_activity').insertMany([
|
||||||
|
{
|
||||||
|
userId: testUser._id,
|
||||||
|
action: 'PAGE_VIEW',
|
||||||
|
metadata: { path: '/dashboard' },
|
||||||
|
timestamp: new Date(),
|
||||||
|
sessionId: 'session-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: testUser._id,
|
||||||
|
action: 'LOGIN',
|
||||||
|
timestamp: new Date(),
|
||||||
|
sessionId: 'session-1'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user.recentActivity).toBeDefined();
|
||||||
|
expect(Array.isArray(response.body.user.recentActivity)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 for non-existent user ID', async () => {
|
||||||
|
const nonExistentId = new ObjectId();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/admin/users/${nonExistentId}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.body.error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for invalid user ID format', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users/invalid-id')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role', async () => {
|
||||||
|
const regularUser = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, regularUser);
|
||||||
|
const userToken = generateTestToken(regularUser._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/admin/users/${regularUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${userToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-1.3: PATCH /api/admin/users/:id - Enable/disable user
|
||||||
|
*/
|
||||||
|
describe('API-1.3: PATCH /api/admin/users/:id (enable/disable)', () => {
|
||||||
|
it('should disable a user successfully', async () => {
|
||||||
|
const testUser = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toBeDefined();
|
||||||
|
expect(response.body.user.isActive).toBe(false);
|
||||||
|
expect(response.body.message).toContain('disabled');
|
||||||
|
|
||||||
|
// Verify in database
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(dbUser.isActive).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable a disabled user successfully', async () => {
|
||||||
|
const testUser = createTestUser({ isActive: false });
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: true });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toBeDefined();
|
||||||
|
expect(response.body.user.isActive).toBe(true);
|
||||||
|
expect(response.body.message).toContain('enabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set disabledAt timestamp when disabling', async () => {
|
||||||
|
const testUser = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const beforeDisable = new Date();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(dbUser.disabledAt).toBeInstanceOf(Date);
|
||||||
|
expect(dbUser.disabledAt.getTime()).toBeGreaterThanOrEqual(beforeDisable.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set disabledBy to admin ID when disabling', async () => {
|
||||||
|
const testUser = createTestUser({ isActive: true });
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(dbUser.disabledBy).toEqual(adminUser._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear disabledAt and disabledBy when enabling', async () => {
|
||||||
|
const testUser = createTestUser({
|
||||||
|
isActive: false,
|
||||||
|
disabledAt: new Date(),
|
||||||
|
disabledBy: new ObjectId()
|
||||||
|
});
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: true });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(dbUser.disabledAt).toBeNull();
|
||||||
|
expect(dbUser.disabledBy).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 when trying to disable yourself', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${adminUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toContain('yourself');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 for non-existent user', async () => {
|
||||||
|
const nonExistentId = new ObjectId();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${nonExistentId}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role', async () => {
|
||||||
|
const regularUser = createTestUser({ role: 'user' });
|
||||||
|
const targetUser = createTestUser();
|
||||||
|
await insertTestUser(db, regularUser);
|
||||||
|
await insertTestUser(db, targetUser);
|
||||||
|
const userToken = generateTestToken(regularUser._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${targetUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${userToken}`])
|
||||||
|
.send({ isActive: false });
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-1.4: PATCH /api/admin/users/:id/role - Promote/demote user role
|
||||||
|
*/
|
||||||
|
describe('API-1.4: PATCH /api/admin/users/:id/role (promote/demote)', () => {
|
||||||
|
it('should promote a user to admin', async () => {
|
||||||
|
const testUser = createTestUser({ role: 'user' });
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: 'admin' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toBeDefined();
|
||||||
|
expect(response.body.user.role).toBe('admin');
|
||||||
|
expect(response.body.message).toContain('promoted');
|
||||||
|
|
||||||
|
// Verify in database
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(dbUser.role).toBe('admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should demote an admin to user', async () => {
|
||||||
|
const anotherAdmin = createTestAdmin({ email: 'another@admin.com' });
|
||||||
|
await insertTestUser(db, anotherAdmin);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${anotherAdmin._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: 'user' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toBeDefined();
|
||||||
|
expect(response.body.user.role).toBe('user');
|
||||||
|
expect(response.body.message).toContain('demoted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 when trying to demote yourself from admin', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${adminUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: 'user' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toContain('yourself');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for invalid role value', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: 'superadmin' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 when role is not provided', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 for non-existent user', async () => {
|
||||||
|
const nonExistentId = new ObjectId();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${nonExistentId}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: 'admin' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role', async () => {
|
||||||
|
const regularUser = createTestUser({ role: 'user' });
|
||||||
|
const targetUser = createTestUser();
|
||||||
|
await insertTestUser(db, regularUser);
|
||||||
|
await insertTestUser(db, targetUser);
|
||||||
|
const userToken = generateTestToken(regularUser._id);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${targetUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${userToken}`])
|
||||||
|
.send({ role: 'admin' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only allow "user" or "admin" roles', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const invalidRoles = ['superuser', 'moderator', 'guest', '', null, 123];
|
||||||
|
|
||||||
|
for (const invalidRole of invalidRoles) {
|
||||||
|
const response = await request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}/role`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ role: invalidRole });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-1.5: General API security tests
|
||||||
|
*/
|
||||||
|
describe('API-1.5: API Security', () => {
|
||||||
|
it('should not expose sensitive user data in list response', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
// Check that sensitive fields are not exposed
|
||||||
|
const userInResponse = response.body.users.find(
|
||||||
|
u => u._id.toString() === testUser._id.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userInResponse) {
|
||||||
|
// googleId should potentially be hidden or sanitized in responses
|
||||||
|
// This depends on your security requirements
|
||||||
|
expect(userInResponse.password).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate ObjectId format in URL parameters', async () => {
|
||||||
|
const invalidIds = ['123', 'abc', '!@#$%', ' ', ''];
|
||||||
|
|
||||||
|
for (const invalidId of invalidIds) {
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/admin/users/${invalidId}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`]);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle concurrent requests safely', async () => {
|
||||||
|
const testUser = createTestUser();
|
||||||
|
await insertTestUser(db, testUser);
|
||||||
|
|
||||||
|
// Send multiple concurrent disable/enable requests
|
||||||
|
const requests = [
|
||||||
|
request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false }),
|
||||||
|
request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: true }),
|
||||||
|
request(app)
|
||||||
|
.patch(`/api/admin/users/${testUser._id}`)
|
||||||
|
.set('Cookie', [`auth_token=${adminToken}`])
|
||||||
|
.send({ isActive: false })
|
||||||
|
];
|
||||||
|
|
||||||
|
const responses = await Promise.all(requests);
|
||||||
|
|
||||||
|
// All requests should complete without errors (200) or with consistent state
|
||||||
|
responses.forEach(response => {
|
||||||
|
expect([200, 400, 404, 409]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final state should be consistent
|
||||||
|
const dbUser = await db.collection('users').findOne({ _id: testUser._id });
|
||||||
|
expect(typeof dbUser.isActive).toBe('boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1223
__tests__/phase2-activity.test.js
Normal file
1223
__tests__/phase2-activity.test.js
Normal file
File diff suppressed because it is too large
Load Diff
1032
__tests__/phase3-statistics.test.js
Normal file
1032
__tests__/phase3-statistics.test.js
Normal file
File diff suppressed because it is too large
Load Diff
979
__tests__/phase4-settings-security.test.js
Normal file
979
__tests__/phase4-settings-security.test.js
Normal file
@ -0,0 +1,979 @@
|
|||||||
|
/**
|
||||||
|
* Phase 4: Settings & Security Tests
|
||||||
|
*
|
||||||
|
* Tests for Admin Dashboard Phase 4 implementation as specified in ADMIN.md
|
||||||
|
* Reference: Implementation Phases -> Phase 4: Settings & Polish
|
||||||
|
*
|
||||||
|
* Endpoints tested:
|
||||||
|
* - GET /api/admin/settings (API-4.1)
|
||||||
|
* - PATCH /api/admin/settings (API-4.2, API-4.3)
|
||||||
|
*
|
||||||
|
* Security features tested:
|
||||||
|
* - Admin action audit logging (SEC-4.1)
|
||||||
|
* - Last admin protection (SEC-4.2)
|
||||||
|
* - Rate limiting on admin endpoints (SEC-4.3)
|
||||||
|
*
|
||||||
|
* These tests are designed to FAIL initially as the endpoints do not exist yet.
|
||||||
|
* They serve as the specification for implementing the Settings & Polish features.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const request = require('supertest');
|
||||||
|
const { MongoClient, ObjectId } = require('mongodb');
|
||||||
|
const {
|
||||||
|
createTestApp,
|
||||||
|
generateTestToken,
|
||||||
|
createTestUser,
|
||||||
|
createTestAdmin,
|
||||||
|
cleanupTestData
|
||||||
|
} = require('./helpers/testHelpers');
|
||||||
|
|
||||||
|
// Test configuration - uses environment set by global setup
|
||||||
|
const TEST_JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret-for-testing-only';
|
||||||
|
|
||||||
|
let app;
|
||||||
|
let db;
|
||||||
|
let client;
|
||||||
|
|
||||||
|
// Test user fixtures - created fresh for each test
|
||||||
|
let testUsers = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to make authenticated requests
|
||||||
|
* @param {string} method - HTTP method (get, post, patch, delete)
|
||||||
|
* @param {string} url - Request URL
|
||||||
|
* @param {Object} user - User to authenticate as
|
||||||
|
* @returns {Object} Supertest request
|
||||||
|
*/
|
||||||
|
const authenticatedRequest = (method, url, user) => {
|
||||||
|
const token = generateTestToken(user._id);
|
||||||
|
return request(app)[method](url)
|
||||||
|
.set('Cookie', `auth_token=${token}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper object for cleaner authenticated request syntax
|
||||||
|
* @param {Object} user - User to authenticate as
|
||||||
|
* @returns {Object} Object with HTTP method functions
|
||||||
|
*/
|
||||||
|
const authAs = (user) => ({
|
||||||
|
get: (url) => authenticatedRequest('get', url, user),
|
||||||
|
post: (url) => authenticatedRequest('post', url, user),
|
||||||
|
patch: (url) => authenticatedRequest('patch', url, user),
|
||||||
|
delete: (url) => authenticatedRequest('delete', url, user)
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Phase 4: Settings & Security', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Connect to test database (MongoDB Memory Server from global setup)
|
||||||
|
const mongoUri = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||||
|
client = new MongoClient(mongoUri);
|
||||||
|
await client.connect();
|
||||||
|
db = client.db('apartments_test');
|
||||||
|
|
||||||
|
// Create the test app with our database
|
||||||
|
app = await createTestApp(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (db) {
|
||||||
|
await cleanupTestData(db);
|
||||||
|
await db.collection('settings').deleteMany({});
|
||||||
|
}
|
||||||
|
if (client) {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clean up all test data
|
||||||
|
await cleanupTestData(db);
|
||||||
|
await db.collection('settings').deleteMany({});
|
||||||
|
|
||||||
|
// Create fresh test users with new ObjectIds for each test
|
||||||
|
testUsers = {
|
||||||
|
admin: createTestAdmin({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'admin@example.com',
|
||||||
|
name: 'Test Admin'
|
||||||
|
}),
|
||||||
|
secondAdmin: createTestAdmin({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'admin2@example.com',
|
||||||
|
name: 'Second Admin'
|
||||||
|
}),
|
||||||
|
regularUser: createTestUser({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'user@example.com',
|
||||||
|
name: 'Test User'
|
||||||
|
}),
|
||||||
|
disabledUser: createTestUser({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'disabled@example.com',
|
||||||
|
name: 'Disabled User',
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert primary admin and regular user
|
||||||
|
await db.collection('users').insertMany([
|
||||||
|
testUsers.admin,
|
||||||
|
testUsers.regularUser
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Insert default settings document
|
||||||
|
await db.collection('settings').insertOne({
|
||||||
|
_id: 'admin_settings',
|
||||||
|
activityRetentionDays: 90,
|
||||||
|
activityLogLevel: 'all',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API-4.1: GET /api/admin/settings - Get admin settings
|
||||||
|
// ============================================================
|
||||||
|
describe('API-4.1: GET /api/admin/settings', () => {
|
||||||
|
describe('Authorization', () => {
|
||||||
|
it('should return 401 when no authentication token is provided', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 403 when user is not an admin', async () => {
|
||||||
|
const response = await authAs(testUsers.regularUser)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/admin/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 when admin user is disabled', async () => {
|
||||||
|
// Insert disabled admin
|
||||||
|
const disabledAdmin = createTestAdmin({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'disabled-admin@example.com',
|
||||||
|
isActive: false
|
||||||
|
});
|
||||||
|
await db.collection('users').insertOne(disabledAdmin);
|
||||||
|
|
||||||
|
const response = await authAs(disabledAdmin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 200 when user is an active admin', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('activityRetentionDays');
|
||||||
|
expect(response.body).toHaveProperty('activityLogLevel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Response format', () => {
|
||||||
|
it('should return activityRetentionDays as a number', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(typeof response.body.activityRetentionDays).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return activityLogLevel as a string', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(typeof response.body.activityLogLevel).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default values when no settings document exists', async () => {
|
||||||
|
// Remove settings document
|
||||||
|
await db.collection('settings').deleteMany({});
|
||||||
|
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
// Should return defaults: 90 days retention, 'all' log level
|
||||||
|
expect(response.body.activityRetentionDays).toBe(90);
|
||||||
|
expect(response.body.activityLogLevel).toBe('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stored values when settings exist', async () => {
|
||||||
|
// Update settings to non-default values
|
||||||
|
await db.collection('settings').updateOne(
|
||||||
|
{ _id: 'admin_settings' },
|
||||||
|
{ $set: { activityRetentionDays: 180, activityLogLevel: 'navigation' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.activityRetentionDays).toBe(180);
|
||||||
|
expect(response.body.activityLogLevel).toBe('navigation');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API-4.2: PATCH /api/admin/settings - Update settings
|
||||||
|
// ============================================================
|
||||||
|
describe('API-4.2: PATCH /api/admin/settings', () => {
|
||||||
|
describe('Authorization', () => {
|
||||||
|
it('should return 401 when no authentication token is provided', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 60 })
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 403 when user is not an admin', async () => {
|
||||||
|
const response = await authAs(testUsers.regularUser)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 60 })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/admin/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 200 when user is an active admin', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 60 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('settings');
|
||||||
|
expect(response.body).toHaveProperty('message');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation - Retention period (API-4.2)', () => {
|
||||||
|
it('should return 400 for retention period below 30 days', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 29 })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/30|365|range|invalid/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for retention period above 365 days', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 366 })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/30|365|range|invalid/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept retention period at minimum boundary (30 days)', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 30 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.settings.activityRetentionDays).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept retention period at maximum boundary (365 days)', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 365 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.settings.activityRetentionDays).toBe(365);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid retention period within range (180 days)', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 180 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.settings.activityRetentionDays).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for non-numeric retention period', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 'ninety' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for negative retention period', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: -30 })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for decimal retention period', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 90.5 })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Successful update', () => {
|
||||||
|
it('should update retention period and return success message', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 120 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.settings.activityRetentionDays).toBe(120);
|
||||||
|
expect(response.body.message).toBe('Settings updated successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist the updated settings in database', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 150 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const settings = await db.collection('settings').findOne({ _id: 'admin_settings' });
|
||||||
|
expect(settings.activityRetentionDays).toBe(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update updatedAt timestamp', async () => {
|
||||||
|
const beforeUpdate = new Date();
|
||||||
|
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 100 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const afterUpdate = new Date();
|
||||||
|
|
||||||
|
const settings = await db.collection('settings').findOne({ _id: 'admin_settings' });
|
||||||
|
expect(new Date(settings.updatedAt).getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
|
||||||
|
expect(new Date(settings.updatedAt).getTime()).toBeLessThanOrEqual(afterUpdate.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record which admin updated the settings', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 100 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const settings = await db.collection('settings').findOne({ _id: 'admin_settings' });
|
||||||
|
expect(settings.updatedBy.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API-4.3: TTL Index Update
|
||||||
|
// ============================================================
|
||||||
|
describe('API-4.3: TTL Index Update', () => {
|
||||||
|
it('should update TTL index when retention period changes to 60 days', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 60 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
// Check the TTL index on user_activity collection
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(
|
||||||
|
idx => idx.name === 'timestamp_ttl' || (idx.key && idx.key.timestamp === 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ttlIndex).toBeDefined();
|
||||||
|
// 60 days = 60 * 24 * 60 * 60 = 5,184,000 seconds
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(60 * 24 * 60 * 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update TTL index when retention period changes to 30 days', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 30 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(
|
||||||
|
idx => idx.name === 'timestamp_ttl' || (idx.key && idx.key.timestamp === 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ttlIndex).toBeDefined();
|
||||||
|
// 30 days = 2,592,000 seconds
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(30 * 24 * 60 * 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update TTL index when retention period changes to 365 days', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 365 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(
|
||||||
|
idx => idx.name === 'timestamp_ttl' || (idx.key && idx.key.timestamp === 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ttlIndex).toBeDefined();
|
||||||
|
// 365 days = 31,536,000 seconds
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(365 * 24 * 60 * 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly calculate TTL seconds for various retention periods', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{ days: 30, expectedSeconds: 2592000 },
|
||||||
|
{ days: 90, expectedSeconds: 7776000 },
|
||||||
|
{ days: 180, expectedSeconds: 15552000 },
|
||||||
|
{ days: 365, expectedSeconds: 31536000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { days, expectedSeconds } of testCases) {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: days })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(
|
||||||
|
idx => idx.name === 'timestamp_ttl' || (idx.key && idx.key.timestamp === 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ttlIndex).toBeDefined();
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(expectedSeconds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SEC-4.1: Admin Action Audit Logging
|
||||||
|
// ============================================================
|
||||||
|
describe('SEC-4.1: Admin Action Audit Logging', () => {
|
||||||
|
describe('ADMIN_UPDATE_SETTINGS', () => {
|
||||||
|
it('should log ADMIN_UPDATE_SETTINGS action when updating settings', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 120 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity).toBeDefined();
|
||||||
|
expect(activity).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record admin user ID in audit log', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 120 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity.userId.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record new retention value in audit log metadata', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 150 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity.metadata).toHaveProperty('activityRetentionDays', 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include timestamp in audit log', async () => {
|
||||||
|
const beforeTime = new Date();
|
||||||
|
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch('/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 100 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const afterTime = new Date();
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity.timestamp).toBeDefined();
|
||||||
|
expect(new Date(activity.timestamp).getTime()).toBeGreaterThanOrEqual(beforeTime.getTime());
|
||||||
|
expect(new Date(activity.timestamp).getTime()).toBeLessThanOrEqual(afterTime.getTime());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ADMIN_DISABLE_USER', () => {
|
||||||
|
it('should log ADMIN_DISABLE_USER action when disabling a user', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.regularUser._id}`)
|
||||||
|
.send({ isActive: false })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_DISABLE_USER'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity).toBeDefined();
|
||||||
|
expect(activity).not.toBeNull();
|
||||||
|
expect(activity.userId.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
expect(activity.metadata.targetUserId).toBe(testUsers.regularUser._id.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ADMIN_ENABLE_USER', () => {
|
||||||
|
it('should log ADMIN_ENABLE_USER action when enabling a user', async () => {
|
||||||
|
// First disable the user
|
||||||
|
await db.collection('users').updateOne(
|
||||||
|
{ _id: testUsers.regularUser._id },
|
||||||
|
{ $set: { isActive: false } }
|
||||||
|
);
|
||||||
|
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.regularUser._id}`)
|
||||||
|
.send({ isActive: true })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_ENABLE_USER'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity).toBeDefined();
|
||||||
|
expect(activity).not.toBeNull();
|
||||||
|
expect(activity.userId.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
expect(activity.metadata.targetUserId).toBe(testUsers.regularUser._id.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ADMIN_PROMOTE_USER', () => {
|
||||||
|
it('should log ADMIN_PROMOTE_USER action when promoting to admin', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.regularUser._id}/role`)
|
||||||
|
.send({ role: 'admin' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_PROMOTE_USER'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity).toBeDefined();
|
||||||
|
expect(activity).not.toBeNull();
|
||||||
|
expect(activity.userId.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
expect(activity.metadata.targetUserId).toBe(testUsers.regularUser._id.toString());
|
||||||
|
expect(activity.metadata.newRole).toBe('admin');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ADMIN_DEMOTE_USER', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Insert second admin for demotion tests
|
||||||
|
await db.collection('users').insertOne(testUsers.secondAdmin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log ADMIN_DEMOTE_USER action when demoting from admin', async () => {
|
||||||
|
await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.secondAdmin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const activity = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_DEMOTE_USER'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity).toBeDefined();
|
||||||
|
expect(activity).not.toBeNull();
|
||||||
|
expect(activity.userId.toString()).toBe(testUsers.admin._id.toString());
|
||||||
|
expect(activity.metadata.targetUserId).toBe(testUsers.secondAdmin._id.toString());
|
||||||
|
expect(activity.metadata.newRole).toBe('user');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SEC-4.2: Last Admin Protection
|
||||||
|
// ============================================================
|
||||||
|
describe('SEC-4.2: Last Admin Protection', () => {
|
||||||
|
it('should return 400 when trying to demote the last admin', async () => {
|
||||||
|
// Ensure only one admin exists
|
||||||
|
await db.collection('users').deleteMany({
|
||||||
|
role: 'admin',
|
||||||
|
_id: { $ne: testUsers.admin._id }
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.admin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/last admin|at least one admin|cannot demote/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow demoting an admin when other active admins exist', async () => {
|
||||||
|
// Insert second admin
|
||||||
|
await db.collection('users').insertOne(testUsers.secondAdmin);
|
||||||
|
|
||||||
|
// Demote second admin (should succeed)
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.secondAdmin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.user.role).toBe('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow self-demotion even when other admins exist', async () => {
|
||||||
|
// Insert second admin
|
||||||
|
await db.collection('users').insertOne(testUsers.secondAdmin);
|
||||||
|
|
||||||
|
// Try to demote self
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.admin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
expect(response.body.error).toMatch(/cannot demote yourself|self|own/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should count only active admins when checking for last admin', async () => {
|
||||||
|
// Insert a disabled admin (should not count)
|
||||||
|
const disabledAdmin = createTestAdmin({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'disabled-admin@example.com',
|
||||||
|
isActive: false
|
||||||
|
});
|
||||||
|
await db.collection('users').insertOne(disabledAdmin);
|
||||||
|
|
||||||
|
// Try to demote self (should fail - only one active admin)
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.admin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error).toMatch(/last admin|at least one admin|cannot demote/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify admin count after demotion would leave at least one admin', async () => {
|
||||||
|
// Insert second admin
|
||||||
|
await db.collection('users').insertOne(testUsers.secondAdmin);
|
||||||
|
|
||||||
|
// Verify we can demote the second admin
|
||||||
|
const response1 = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.secondAdmin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response1.body.user.role).toBe('user');
|
||||||
|
|
||||||
|
// Now the primary admin is the last one, cannot demote
|
||||||
|
const response2 = await authAs(testUsers.admin)
|
||||||
|
.patch(`/api/admin/users/${testUsers.admin._id}/role`)
|
||||||
|
.send({ role: 'user' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response2.body.error).toMatch(/last admin|at least one admin/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SEC-4.3: Rate Limiting on Admin Endpoints
|
||||||
|
// ============================================================
|
||||||
|
describe('SEC-4.3: Rate Limiting', () => {
|
||||||
|
/**
|
||||||
|
* Helper to make multiple rapid requests
|
||||||
|
* @param {string} endpoint - API endpoint
|
||||||
|
* @param {number} count - Number of requests
|
||||||
|
* @param {string} method - HTTP method
|
||||||
|
* @param {Object} body - Request body for POST/PATCH
|
||||||
|
* @returns {Promise<Array>} Array of responses
|
||||||
|
*/
|
||||||
|
async function makeRapidRequests(endpoint, count, method = 'get', body = null) {
|
||||||
|
const requests = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let req = authAs(testUsers.admin)[method](endpoint);
|
||||||
|
if (body && (method === 'patch' || method === 'post')) {
|
||||||
|
req = req.send(body);
|
||||||
|
}
|
||||||
|
requests.push(req);
|
||||||
|
}
|
||||||
|
return Promise.all(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('User list endpoint rate limiting (60 req/min)', () => {
|
||||||
|
it('should allow up to 60 requests per minute to user list', async () => {
|
||||||
|
const responses = await makeRapidRequests('/api/admin/users', 60);
|
||||||
|
|
||||||
|
// All 60 requests should succeed
|
||||||
|
const successCount = responses.filter(r => r.status === 200).length;
|
||||||
|
expect(successCount).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 429 when exceeding 60 requests per minute', async () => {
|
||||||
|
const responses = await makeRapidRequests('/api/admin/users', 65);
|
||||||
|
|
||||||
|
// At least some should be rate limited
|
||||||
|
const rateLimited = responses.filter(r => r.status === 429);
|
||||||
|
expect(rateLimited.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Rate limited response should have appropriate error
|
||||||
|
if (rateLimited.length > 0) {
|
||||||
|
expect(rateLimited[0].body).toHaveProperty('error');
|
||||||
|
expect(rateLimited[0].body.error).toMatch(/rate limit|too many requests/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User updates endpoint rate limiting (30 req/min)', () => {
|
||||||
|
it('should allow up to 30 requests per minute for user updates', async () => {
|
||||||
|
const responses = await makeRapidRequests(
|
||||||
|
`/api/admin/users/${testUsers.regularUser._id}`,
|
||||||
|
30,
|
||||||
|
'patch',
|
||||||
|
{ isActive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const successCount = responses.filter(r => r.status === 200).length;
|
||||||
|
expect(successCount).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 429 when exceeding 30 user update requests per minute', async () => {
|
||||||
|
const responses = await makeRapidRequests(
|
||||||
|
`/api/admin/users/${testUsers.regularUser._id}`,
|
||||||
|
35,
|
||||||
|
'patch',
|
||||||
|
{ isActive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const rateLimited = responses.filter(r => r.status === 429);
|
||||||
|
expect(rateLimited.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Stats endpoints rate limiting (30 req/min)', () => {
|
||||||
|
const statsEndpoints = [
|
||||||
|
'/api/admin/stats/overview',
|
||||||
|
'/api/admin/stats/active-users',
|
||||||
|
'/api/admin/stats/actions',
|
||||||
|
'/api/admin/stats/peak-times'
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should allow up to 30 requests per minute to stats overview', async () => {
|
||||||
|
const responses = await makeRapidRequests('/api/admin/stats/overview', 30);
|
||||||
|
const successCount = responses.filter(r => r.status === 200).length;
|
||||||
|
expect(successCount).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 429 when exceeding 30 stats requests per minute', async () => {
|
||||||
|
const responses = await makeRapidRequests('/api/admin/stats/overview', 35);
|
||||||
|
const rateLimited = responses.filter(r => r.status === 429);
|
||||||
|
expect(rateLimited.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share rate limit across all stats endpoints', async () => {
|
||||||
|
// Make requests across different stats endpoints
|
||||||
|
const requests = [];
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const endpoint = statsEndpoints[i % statsEndpoints.length];
|
||||||
|
requests.push(authAs(testUsers.admin).get(endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.all(requests);
|
||||||
|
const rateLimited = responses.filter(r => r.status === 429);
|
||||||
|
|
||||||
|
// After 30 total requests, should start rate limiting
|
||||||
|
expect(rateLimited.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rate limit headers', () => {
|
||||||
|
it('should include rate limit headers in response', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
// Check for common rate limit header variations
|
||||||
|
const hasRateLimitHeader =
|
||||||
|
response.headers['x-ratelimit-limit'] ||
|
||||||
|
response.headers['ratelimit-limit'] ||
|
||||||
|
response.headers['x-rate-limit-limit'];
|
||||||
|
|
||||||
|
expect(hasRateLimitHeader).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include remaining requests in headers', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const hasRemainingHeader =
|
||||||
|
response.headers['x-ratelimit-remaining'] ||
|
||||||
|
response.headers['ratelimit-remaining'] ||
|
||||||
|
response.headers['x-rate-limit-remaining'];
|
||||||
|
|
||||||
|
expect(hasRemainingHeader).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include reset time in headers', async () => {
|
||||||
|
const response = await authAs(testUsers.admin)
|
||||||
|
.get('/api/admin/users')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const hasResetHeader =
|
||||||
|
response.headers['x-ratelimit-reset'] ||
|
||||||
|
response.headers['ratelimit-reset'] ||
|
||||||
|
response.headers['x-rate-limit-reset'];
|
||||||
|
|
||||||
|
expect(hasResetHeader).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Integration Test: Full Settings Update Workflow
|
||||||
|
// ============================================================
|
||||||
|
describe('Integration: Settings Update Workflow', () => {
|
||||||
|
let app;
|
||||||
|
let db;
|
||||||
|
let client;
|
||||||
|
let testAdmin;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const mongoUri = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||||
|
client = new MongoClient(mongoUri);
|
||||||
|
await client.connect();
|
||||||
|
db = client.db('apartments_test');
|
||||||
|
app = await createTestApp(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (db) {
|
||||||
|
await cleanupTestData(db);
|
||||||
|
await db.collection('settings').deleteMany({});
|
||||||
|
}
|
||||||
|
if (client) {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cleanupTestData(db);
|
||||||
|
await db.collection('settings').deleteMany({});
|
||||||
|
|
||||||
|
testAdmin = createTestAdmin({
|
||||||
|
_id: new ObjectId(),
|
||||||
|
email: 'integration-admin@example.com'
|
||||||
|
});
|
||||||
|
await db.collection('users').insertOne(testAdmin);
|
||||||
|
|
||||||
|
await db.collection('settings').insertOne({
|
||||||
|
_id: 'admin_settings',
|
||||||
|
activityRetentionDays: 90,
|
||||||
|
activityLogLevel: 'all',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should complete full settings update workflow', async () => {
|
||||||
|
const authAdmin = (method, url) => {
|
||||||
|
const token = generateTestToken(testAdmin._id);
|
||||||
|
return request(app)[method](url).set('Cookie', `auth_token=${token}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: Get current settings
|
||||||
|
const getResponse = await authAdmin('get', '/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(getResponse.body.activityRetentionDays).toBe(90);
|
||||||
|
|
||||||
|
// Step 2: Update settings to new value
|
||||||
|
const updateResponse = await authAdmin('patch', '/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: 180 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(updateResponse.body.settings.activityRetentionDays).toBe(180);
|
||||||
|
expect(updateResponse.body.message).toBe('Settings updated successfully');
|
||||||
|
|
||||||
|
// Step 3: Verify settings persisted
|
||||||
|
const verifyResponse = await authAdmin('get', '/api/admin/settings')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(verifyResponse.body.activityRetentionDays).toBe(180);
|
||||||
|
|
||||||
|
// Step 4: Verify audit log was created
|
||||||
|
const auditLog = await db.collection('user_activity').findOne({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(auditLog).toBeDefined();
|
||||||
|
expect(auditLog).not.toBeNull();
|
||||||
|
expect(auditLog.metadata.activityRetentionDays).toBe(180);
|
||||||
|
expect(auditLog.userId.toString()).toBe(testAdmin._id.toString());
|
||||||
|
|
||||||
|
// Step 5: Verify TTL index was updated
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(idx => idx.name === 'timestamp_ttl');
|
||||||
|
|
||||||
|
expect(ttlIndex).toBeDefined();
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(180 * 24 * 60 * 60);
|
||||||
|
|
||||||
|
// Step 6: Verify database document was updated
|
||||||
|
const dbSettings = await db.collection('settings').findOne({ _id: 'admin_settings' });
|
||||||
|
expect(dbSettings.activityRetentionDays).toBe(180);
|
||||||
|
expect(dbSettings.updatedBy.toString()).toBe(testAdmin._id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple sequential settings updates', async () => {
|
||||||
|
const authAdmin = (method, url) => {
|
||||||
|
const token = generateTestToken(testAdmin._id);
|
||||||
|
return request(app)[method](url).set('Cookie', `auth_token=${token}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const retentionValues = [30, 60, 90, 180, 365];
|
||||||
|
|
||||||
|
for (const value of retentionValues) {
|
||||||
|
const response = await authAdmin('patch', '/api/admin/settings')
|
||||||
|
.send({ activityRetentionDays: value })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.settings.activityRetentionDays).toBe(value);
|
||||||
|
|
||||||
|
// Verify TTL index after each update
|
||||||
|
const indexes = await db.collection('user_activity').indexes();
|
||||||
|
const ttlIndex = indexes.find(idx => idx.name === 'timestamp_ttl');
|
||||||
|
expect(ttlIndex.expireAfterSeconds).toBe(value * 24 * 60 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have 5 audit log entries
|
||||||
|
const auditLogCount = await db.collection('user_activity').countDocuments({
|
||||||
|
action: 'ADMIN_UPDATE_SETTINGS'
|
||||||
|
});
|
||||||
|
expect(auditLogCount).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
__tests__/setup.js
Normal file
13
__tests__/setup.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
// Create an in-memory MongoDB instance for testing
|
||||||
|
const mongod = await MongoMemoryServer.create();
|
||||||
|
const uri = mongod.getUri();
|
||||||
|
|
||||||
|
// Store the URI and instance globally so tests can access them
|
||||||
|
global.__MONGOD__ = mongod;
|
||||||
|
process.env.MONGO_URI = uri;
|
||||||
|
process.env.JWT_SECRET = 'test-jwt-secret-for-testing-only';
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
};
|
||||||
11
__tests__/setupAfterEnv.js
Normal file
11
__tests__/setupAfterEnv.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Increase timeout for tests that interact with MongoDB
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
|
||||||
|
// Suppress console logs during tests (optional - comment out for debugging)
|
||||||
|
// global.console = {
|
||||||
|
// ...console,
|
||||||
|
// log: jest.fn(),
|
||||||
|
// debug: jest.fn(),
|
||||||
|
// info: jest.fn(),
|
||||||
|
// warn: jest.fn(),
|
||||||
|
// };
|
||||||
6
__tests__/teardown.js
Normal file
6
__tests__/teardown.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = async () => {
|
||||||
|
// Stop the in-memory MongoDB instance
|
||||||
|
if (global.__MONGOD__) {
|
||||||
|
await global.__MONGOD__.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
17
jest.config.js
Normal file
17
jest.config.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/__tests__/**/*.test.js'],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'**/*.js',
|
||||||
|
'!**/node_modules/**',
|
||||||
|
'!**/coverage/**',
|
||||||
|
'!jest.config.js'
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
verbose: true,
|
||||||
|
testTimeout: 30000,
|
||||||
|
// Setup file to handle MongoDB memory server lifecycle
|
||||||
|
globalSetup: './__tests__/setup.js',
|
||||||
|
globalTeardown: './__tests__/teardown.js',
|
||||||
|
setupFilesAfterEnv: ['./__tests__/setupAfterEnv.js']
|
||||||
|
};
|
||||||
5013
package-lock.json
generated
5013
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"apartments",
|
"apartments",
|
||||||
@ -28,7 +30,10 @@
|
|||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.2"
|
"jest": "^30.2.0",
|
||||||
|
"mongodb-memory-server": "^11.0.1",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user