chore: update test fixtures and frontend for UUID identity (refs #217)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s
Backend test fixtures: - Replace auth0|xxx format with UUID in all test userId values - Update admin tests for new id/userProfileId schema - Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks - Fix admin integration test supertest usage (app.server) Frontend: - AdminUser type: auth0Sub -> id + userProfileId - admin.api.ts: all user management methods use userId (UUID) params - useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations - AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id - Remove encodeURIComponent (UUIDs don't need encoding) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,18 +4,19 @@
|
||||
*/
|
||||
|
||||
import request from 'supertest';
|
||||
import { app } from '../../../../app';
|
||||
import { buildApp } from '../../../../app';
|
||||
import pool from '../../../../core/config/database';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { setAdminGuardPool } from '../../../../core/plugins/admin-guard.plugin';
|
||||
|
||||
const DEFAULT_ADMIN_SUB = 'test-admin-123';
|
||||
const DEFAULT_ADMIN_ID = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
|
||||
const DEFAULT_ADMIN_EMAIL = 'test-admin@motovaultpro.com';
|
||||
|
||||
let currentUser = {
|
||||
sub: DEFAULT_ADMIN_SUB,
|
||||
sub: 'auth0|test-admin-123',
|
||||
email: DEFAULT_ADMIN_EMAIL,
|
||||
};
|
||||
|
||||
@@ -25,11 +26,15 @@ jest.mock('../../../../core/plugins/auth.plugin', () => {
|
||||
default: fastifyPlugin(async function(fastify) {
|
||||
fastify.decorate('authenticate', async function(request, _reply) {
|
||||
// Inject dynamic test user context
|
||||
// JWT sub is still auth0|xxx format
|
||||
request.user = { sub: currentUser.sub };
|
||||
request.userContext = {
|
||||
userId: currentUser.sub,
|
||||
userId: DEFAULT_ADMIN_ID,
|
||||
email: currentUser.email,
|
||||
emailVerified: true,
|
||||
onboardingCompleted: true,
|
||||
isAdmin: false, // Will be set by admin guard
|
||||
subscriptionTier: 'free',
|
||||
};
|
||||
});
|
||||
}, { name: 'auth-plugin' })
|
||||
@@ -37,10 +42,14 @@ jest.mock('../../../../core/plugins/auth.plugin', () => {
|
||||
});
|
||||
|
||||
describe('Admin Management Integration Tests', () => {
|
||||
let testAdminAuth0Sub: string;
|
||||
let testNonAdminAuth0Sub: string;
|
||||
let app: FastifyInstance;
|
||||
let testAdminId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Build the app
|
||||
app = await buildApp();
|
||||
await app.ready();
|
||||
|
||||
// Run the admin migration directly using the migration file
|
||||
const migrationFile = join(__dirname, '../../migrations/001_create_admin_users.sql');
|
||||
const migrationSQL = readFileSync(migrationFile, 'utf-8');
|
||||
@@ -50,33 +59,31 @@ describe('Admin Management Integration Tests', () => {
|
||||
setAdminGuardPool(pool);
|
||||
|
||||
// Create test admin user
|
||||
testAdminAuth0Sub = DEFAULT_ADMIN_SUB;
|
||||
testAdminId = DEFAULT_ADMIN_ID;
|
||||
await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (auth0_sub) DO NOTHING
|
||||
`, [testAdminAuth0Sub, DEFAULT_ADMIN_EMAIL, 'admin', 'system']);
|
||||
|
||||
// Create test non-admin auth0Sub for permission tests
|
||||
testNonAdminAuth0Sub = 'test-non-admin-456';
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_profile_id) DO NOTHING
|
||||
`, [testAdminId, testAdminId, DEFAULT_ADMIN_EMAIL, 'admin', 'system']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test database
|
||||
await pool.query('DROP TABLE IF EXISTS admin_audit_logs CASCADE');
|
||||
await pool.query('DROP TABLE IF EXISTS admin_users CASCADE');
|
||||
await app.close();
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up test data before each test (except the test admin)
|
||||
await pool.query(
|
||||
'DELETE FROM admin_users WHERE auth0_sub != $1 AND auth0_sub != $2',
|
||||
[testAdminAuth0Sub, 'system|bootstrap']
|
||||
'DELETE FROM admin_users WHERE user_profile_id != $1',
|
||||
[testAdminId]
|
||||
);
|
||||
await pool.query('DELETE FROM admin_audit_logs');
|
||||
currentUser = {
|
||||
sub: DEFAULT_ADMIN_SUB,
|
||||
sub: 'auth0|test-admin-123',
|
||||
email: DEFAULT_ADMIN_EMAIL,
|
||||
};
|
||||
});
|
||||
@@ -85,11 +92,11 @@ describe('Admin Management Integration Tests', () => {
|
||||
it('should reject non-admin user trying to list admins', async () => {
|
||||
// Create mock for non-admin user
|
||||
currentUser = {
|
||||
sub: testNonAdminAuth0Sub,
|
||||
sub: 'auth0|test-non-admin-456',
|
||||
email: 'test-user@example.com',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/admins')
|
||||
.expect(403);
|
||||
|
||||
@@ -101,51 +108,51 @@ describe('Admin Management Integration Tests', () => {
|
||||
describe('GET /api/admin/verify', () => {
|
||||
it('should confirm admin access for existing admin', async () => {
|
||||
currentUser = {
|
||||
sub: testAdminAuth0Sub,
|
||||
sub: 'auth0|test-admin-123',
|
||||
email: DEFAULT_ADMIN_EMAIL,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/verify')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.isAdmin).toBe(true);
|
||||
expect(response.body.adminRecord).toMatchObject({
|
||||
auth0Sub: testAdminAuth0Sub,
|
||||
id: testAdminId,
|
||||
email: DEFAULT_ADMIN_EMAIL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should link admin record by email when auth0_sub differs', async () => {
|
||||
const placeholderSub = 'auth0|placeholder-sub';
|
||||
const realSub = 'auth0|real-admin-sub';
|
||||
it('should link admin record by email when user_profile_id differs', async () => {
|
||||
const placeholderId = '9b9a1234-1234-1234-1234-123456789abc';
|
||||
const realId = 'a1b2c3d4-5678-90ab-cdef-123456789def';
|
||||
const email = 'link-admin@example.com';
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [placeholderSub, email, 'admin', testAdminAuth0Sub]);
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [placeholderId, placeholderId, email, 'admin', testAdminId]);
|
||||
|
||||
currentUser = {
|
||||
sub: realSub,
|
||||
sub: 'auth0|real-admin-sub',
|
||||
email,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/verify')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.isAdmin).toBe(true);
|
||||
expect(response.body.adminRecord).toMatchObject({
|
||||
auth0Sub: realSub,
|
||||
userProfileId: realId,
|
||||
email,
|
||||
});
|
||||
|
||||
const record = await pool.query(
|
||||
'SELECT auth0_sub FROM admin_users WHERE email = $1',
|
||||
'SELECT user_profile_id FROM admin_users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
expect(record.rows[0].auth0_sub).toBe(realSub);
|
||||
expect(record.rows[0].user_profile_id).toBe(realId);
|
||||
});
|
||||
|
||||
it('should return non-admin response for unknown user', async () => {
|
||||
@@ -154,7 +161,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
email: 'non-admin@example.com',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/verify')
|
||||
.expect(200);
|
||||
|
||||
@@ -166,17 +173,19 @@ describe('Admin Management Integration Tests', () => {
|
||||
describe('GET /api/admin/admins', () => {
|
||||
it('should list all admin users', async () => {
|
||||
// Create additional test admins
|
||||
const admin1Id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
const admin2Id = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e';
|
||||
await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by)
|
||||
VALUES
|
||||
($1, $2, $3, $4),
|
||||
($5, $6, $7, $8)
|
||||
($1, $2, $3, $4, $5),
|
||||
($6, $7, $8, $9, $10)
|
||||
`, [
|
||||
'auth0|admin1', 'admin1@example.com', 'admin', testAdminAuth0Sub,
|
||||
'auth0|admin2', 'admin2@example.com', 'super_admin', testAdminAuth0Sub
|
||||
admin1Id, admin1Id, 'admin1@example.com', 'admin', testAdminId,
|
||||
admin2Id, admin2Id, 'admin2@example.com', 'super_admin', testAdminId
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/admins')
|
||||
.expect(200);
|
||||
|
||||
@@ -184,7 +193,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
expect(response.body).toHaveProperty('admins');
|
||||
expect(response.body.admins.length).toBeGreaterThanOrEqual(3); // At least test admin + 2 created
|
||||
expect(response.body.admins[0]).toMatchObject({
|
||||
auth0Sub: expect.any(String),
|
||||
id: expect.any(String),
|
||||
email: expect.any(String),
|
||||
role: expect.stringMatching(/^(admin|super_admin)$/),
|
||||
createdAt: expect.any(String),
|
||||
@@ -194,12 +203,13 @@ describe('Admin Management Integration Tests', () => {
|
||||
|
||||
it('should include revoked admins in the list', async () => {
|
||||
// Create and revoke an admin
|
||||
const revokedId = 'f1e2d3c4-b5a6-9788-6543-210fedcba987';
|
||||
await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
`, ['auth0|revoked', 'revoked@example.com', 'admin', testAdminAuth0Sub]);
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
`, [revokedId, revokedId, 'revoked@example.com', 'admin', testAdminId]);
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/admins')
|
||||
.expect(200);
|
||||
|
||||
@@ -218,17 +228,17 @@ describe('Admin Management Integration Tests', () => {
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(newAdminData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
auth0Sub: expect.any(String),
|
||||
id: expect.any(String),
|
||||
email: 'newadmin@example.com',
|
||||
role: 'admin',
|
||||
createdAt: expect.any(String),
|
||||
createdBy: testAdminAuth0Sub,
|
||||
createdBy: testAdminId,
|
||||
revokedAt: null
|
||||
});
|
||||
|
||||
@@ -238,7 +248,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
['CREATE', 'newadmin@example.com']
|
||||
);
|
||||
expect(auditResult.rows.length).toBe(1);
|
||||
expect(auditResult.rows[0].actor_admin_id).toBe(testAdminAuth0Sub);
|
||||
expect(auditResult.rows[0].actor_admin_id).toBe(testAdminId);
|
||||
});
|
||||
|
||||
it('should reject invalid email', async () => {
|
||||
@@ -247,7 +257,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(invalidData)
|
||||
.expect(400);
|
||||
@@ -263,13 +273,13 @@ describe('Admin Management Integration Tests', () => {
|
||||
};
|
||||
|
||||
// Create first admin
|
||||
await request(app)
|
||||
await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(adminData)
|
||||
.expect(201);
|
||||
|
||||
// Try to create duplicate
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(adminData)
|
||||
.expect(400);
|
||||
@@ -284,7 +294,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
role: 'super_admin'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(superAdminData)
|
||||
.expect(201);
|
||||
@@ -297,7 +307,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
email: 'defaultrole@example.com'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send(adminData)
|
||||
.expect(201);
|
||||
@@ -306,23 +316,24 @@ describe('Admin Management Integration Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/admin/admins/:auth0Sub/revoke', () => {
|
||||
describe('PATCH /api/admin/admins/:id/revoke', () => {
|
||||
it('should revoke admin access', async () => {
|
||||
// Create admin to revoke
|
||||
const toRevokeId = 'b1c2d3e4-f5a6-7890-1234-567890abcdef';
|
||||
const createResult = await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING auth0_sub
|
||||
`, ['auth0|to-revoke', 'torevoke@example.com', 'admin', testAdminAuth0Sub]);
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`, [toRevokeId, toRevokeId, 'torevoke@example.com', 'admin', testAdminId]);
|
||||
|
||||
const auth0Sub = createResult.rows[0].auth0_sub;
|
||||
const adminId = createResult.rows[0].id;
|
||||
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/admins/${auth0Sub}/revoke`)
|
||||
const response = await request(app.server)
|
||||
.patch(`/api/admin/admins/${adminId}/revoke`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
auth0Sub,
|
||||
id: adminId,
|
||||
email: 'torevoke@example.com',
|
||||
revokedAt: expect.any(String)
|
||||
});
|
||||
@@ -330,7 +341,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
// Verify audit log
|
||||
const auditResult = await pool.query(
|
||||
'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2',
|
||||
['REVOKE', auth0Sub]
|
||||
['REVOKE', adminId]
|
||||
);
|
||||
expect(auditResult.rows.length).toBe(1);
|
||||
});
|
||||
@@ -338,12 +349,12 @@ describe('Admin Management Integration Tests', () => {
|
||||
it('should prevent revoking last active admin', async () => {
|
||||
// First, ensure only one active admin exists
|
||||
await pool.query(
|
||||
'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE auth0_sub != $1',
|
||||
[testAdminAuth0Sub]
|
||||
'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE user_profile_id != $1',
|
||||
[testAdminId]
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/admins/${testAdminAuth0Sub}/revoke`)
|
||||
const response = await request(app.server)
|
||||
.patch(`/api/admin/admins/${testAdminId}/revoke`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBe('Bad Request');
|
||||
@@ -351,8 +362,8 @@ describe('Admin Management Integration Tests', () => {
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent admin', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/admin/admins/auth0|nonexistent/revoke')
|
||||
const response = await request(app.server)
|
||||
.patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/revoke')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.error).toBe('Not Found');
|
||||
@@ -360,23 +371,24 @@ describe('Admin Management Integration Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/admin/admins/:auth0Sub/reinstate', () => {
|
||||
describe('PATCH /api/admin/admins/:id/reinstate', () => {
|
||||
it('should reinstate revoked admin', async () => {
|
||||
// Create revoked admin
|
||||
const reinstateId = 'c2d3e4f5-a6b7-8901-2345-678901bcdef0';
|
||||
const createResult = await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
RETURNING auth0_sub
|
||||
`, ['auth0|to-reinstate', 'toreinstate@example.com', 'admin', testAdminAuth0Sub]);
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
`, [reinstateId, reinstateId, 'toreinstate@example.com', 'admin', testAdminId]);
|
||||
|
||||
const auth0Sub = createResult.rows[0].auth0_sub;
|
||||
const adminId = createResult.rows[0].id;
|
||||
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/admins/${auth0Sub}/reinstate`)
|
||||
const response = await request(app.server)
|
||||
.patch(`/api/admin/admins/${adminId}/reinstate`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
auth0Sub,
|
||||
id: adminId,
|
||||
email: 'toreinstate@example.com',
|
||||
revokedAt: null
|
||||
});
|
||||
@@ -384,14 +396,14 @@ describe('Admin Management Integration Tests', () => {
|
||||
// Verify audit log
|
||||
const auditResult = await pool.query(
|
||||
'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2',
|
||||
['REINSTATE', auth0Sub]
|
||||
['REINSTATE', adminId]
|
||||
);
|
||||
expect(auditResult.rows.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent admin', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/admin/admins/auth0|nonexistent/reinstate')
|
||||
const response = await request(app.server)
|
||||
.patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/reinstate')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.error).toBe('Not Found');
|
||||
@@ -400,16 +412,17 @@ describe('Admin Management Integration Tests', () => {
|
||||
|
||||
it('should handle reinstating already active admin', async () => {
|
||||
// Create active admin
|
||||
const activeId = 'd3e4f5a6-b7c8-9012-3456-789012cdef01';
|
||||
const createResult = await pool.query(`
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING auth0_sub
|
||||
`, ['auth0|already-active', 'active@example.com', 'admin', testAdminAuth0Sub]);
|
||||
INSERT INTO admin_users (id, user_profile_id, email, role, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`, [activeId, activeId, 'active@example.com', 'admin', testAdminId]);
|
||||
|
||||
const auth0Sub = createResult.rows[0].auth0_sub;
|
||||
const adminId = createResult.rows[0].id;
|
||||
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/admins/${auth0Sub}/reinstate`)
|
||||
const response = await request(app.server)
|
||||
.patch(`/api/admin/admins/${adminId}/reinstate`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.revokedAt).toBeNull();
|
||||
@@ -426,12 +439,12 @@ describe('Admin Management Integration Tests', () => {
|
||||
($5, $6, $7, $8),
|
||||
($9, $10, $11, $12)
|
||||
`, [
|
||||
testAdminAuth0Sub, 'CREATE', 'admin_user', 'test1@example.com',
|
||||
testAdminAuth0Sub, 'REVOKE', 'admin_user', 'test2@example.com',
|
||||
testAdminAuth0Sub, 'REINSTATE', 'admin_user', 'test3@example.com'
|
||||
testAdminId, 'CREATE', 'admin_user', 'test1@example.com',
|
||||
testAdminId, 'REVOKE', 'admin_user', 'test2@example.com',
|
||||
testAdminId, 'REINSTATE', 'admin_user', 'test3@example.com'
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/audit-logs')
|
||||
.expect(200);
|
||||
|
||||
@@ -440,7 +453,7 @@ describe('Admin Management Integration Tests', () => {
|
||||
expect(response.body.logs.length).toBeGreaterThanOrEqual(3);
|
||||
expect(response.body.logs[0]).toMatchObject({
|
||||
id: expect.any(String),
|
||||
actorAdminId: testAdminAuth0Sub,
|
||||
actorAdminId: testAdminId,
|
||||
action: expect.any(String),
|
||||
resourceType: expect.any(String),
|
||||
createdAt: expect.any(String)
|
||||
@@ -453,10 +466,10 @@ describe('Admin Management Integration Tests', () => {
|
||||
await pool.query(`
|
||||
INSERT INTO admin_audit_logs (actor_admin_id, action, resource_type, resource_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [testAdminAuth0Sub, 'CREATE', 'admin_user', `test${i}@example.com`]);
|
||||
`, [testAdminId, 'CREATE', 'admin_user', `test${i}@example.com`]);
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/audit-logs?limit=5&offset=0')
|
||||
.expect(200);
|
||||
|
||||
@@ -473,12 +486,12 @@ describe('Admin Management Integration Tests', () => {
|
||||
($3, $4, CURRENT_TIMESTAMP - INTERVAL '1 minute'),
|
||||
($5, $6, CURRENT_TIMESTAMP)
|
||||
`, [
|
||||
testAdminAuth0Sub, 'FIRST',
|
||||
testAdminAuth0Sub, 'SECOND',
|
||||
testAdminAuth0Sub, 'THIRD'
|
||||
testAdminId, 'FIRST',
|
||||
testAdminId, 'SECOND',
|
||||
testAdminId, 'THIRD'
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
const response = await request(app.server)
|
||||
.get('/api/admin/audit-logs?limit=3')
|
||||
.expect(200);
|
||||
|
||||
@@ -491,45 +504,45 @@ describe('Admin Management Integration Tests', () => {
|
||||
describe('End-to-end workflow', () => {
|
||||
it('should create, revoke, and reinstate admin with full audit trail', async () => {
|
||||
// 1. Create new admin
|
||||
const createResponse = await request(app)
|
||||
const createResponse = await request(app.server)
|
||||
.post('/api/admin/admins')
|
||||
.send({ email: 'workflow@example.com', role: 'admin' })
|
||||
.expect(201);
|
||||
|
||||
const auth0Sub = createResponse.body.auth0Sub;
|
||||
const adminId = createResponse.body.id;
|
||||
|
||||
// 2. Verify admin appears in list
|
||||
const listResponse = await request(app)
|
||||
const listResponse = await request(app.server)
|
||||
.get('/api/admin/admins')
|
||||
.expect(200);
|
||||
|
||||
const createdAdmin = listResponse.body.admins.find(
|
||||
(admin: any) => admin.auth0Sub === auth0Sub
|
||||
(admin: any) => admin.id === adminId
|
||||
);
|
||||
expect(createdAdmin).toBeDefined();
|
||||
expect(createdAdmin.revokedAt).toBeNull();
|
||||
|
||||
// 3. Revoke admin
|
||||
const revokeResponse = await request(app)
|
||||
.patch(`/api/admin/admins/${auth0Sub}/revoke`)
|
||||
const revokeResponse = await request(app.server)
|
||||
.patch(`/api/admin/admins/${adminId}/revoke`)
|
||||
.expect(200);
|
||||
|
||||
expect(revokeResponse.body.revokedAt).toBeTruthy();
|
||||
|
||||
// 4. Reinstate admin
|
||||
const reinstateResponse = await request(app)
|
||||
.patch(`/api/admin/admins/${auth0Sub}/reinstate`)
|
||||
const reinstateResponse = await request(app.server)
|
||||
.patch(`/api/admin/admins/${adminId}/reinstate`)
|
||||
.expect(200);
|
||||
|
||||
expect(reinstateResponse.body.revokedAt).toBeNull();
|
||||
|
||||
// 5. Verify complete audit trail
|
||||
const auditResponse = await request(app)
|
||||
const auditResponse = await request(app.server)
|
||||
.get('/api/admin/audit-logs')
|
||||
.expect(200);
|
||||
|
||||
const workflowLogs = auditResponse.body.logs.filter(
|
||||
(log: any) => log.targetAdminId === auth0Sub || log.resourceId === 'workflow@example.com'
|
||||
(log: any) => log.targetAdminId === adminId || log.resourceId === 'workflow@example.com'
|
||||
);
|
||||
|
||||
expect(workflowLogs.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('admin guard plugin', () => {
|
||||
fastify = Fastify();
|
||||
authenticateMock = jest.fn(async (request: FastifyRequest) => {
|
||||
request.userContext = {
|
||||
userId: 'auth0|admin',
|
||||
userId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
|
||||
email: 'admin@motovaultpro.com',
|
||||
emailVerified: true,
|
||||
onboardingCompleted: true,
|
||||
@@ -41,7 +41,7 @@ describe('admin guard plugin', () => {
|
||||
mockPool = {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
rows: [{
|
||||
auth0_sub: 'auth0|admin',
|
||||
user_profile_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
|
||||
email: 'admin@motovaultpro.com',
|
||||
role: 'admin',
|
||||
revoked_at: null,
|
||||
|
||||
@@ -6,13 +6,23 @@
|
||||
import { AdminService } from '../../domain/admin.service';
|
||||
import { AdminRepository } from '../../data/admin.repository';
|
||||
|
||||
// Mock the audit log service
|
||||
jest.mock('../../../audit-log', () => ({
|
||||
auditLogService: {
|
||||
info: jest.fn().mockResolvedValue(undefined),
|
||||
warn: jest.fn().mockResolvedValue(undefined),
|
||||
error: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AdminService', () => {
|
||||
let adminService: AdminService;
|
||||
let mockRepository: jest.Mocked<AdminRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
getAdminByAuth0Sub: jest.fn(),
|
||||
getAdminById: jest.fn(),
|
||||
getAdminByUserProfileId: jest.fn(),
|
||||
getAdminByEmail: jest.fn(),
|
||||
getAllAdmins: jest.fn(),
|
||||
getActiveAdmins: jest.fn(),
|
||||
@@ -26,30 +36,31 @@ describe('AdminService', () => {
|
||||
adminService = new AdminService(mockRepository);
|
||||
});
|
||||
|
||||
describe('getAdminByAuth0Sub', () => {
|
||||
describe('getAdminById', () => {
|
||||
it('should return admin when found', async () => {
|
||||
const mockAdmin = {
|
||||
auth0Sub: 'auth0|123456',
|
||||
id: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
|
||||
userProfileId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
|
||||
email: 'admin@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockRepository.getAdminByAuth0Sub.mockResolvedValue(mockAdmin);
|
||||
mockRepository.getAdminById.mockResolvedValue(mockAdmin);
|
||||
|
||||
const result = await adminService.getAdminByAuth0Sub('auth0|123456');
|
||||
const result = await adminService.getAdminById('7c9e6679-7425-40de-944b-e07fc1f90ae7');
|
||||
|
||||
expect(result).toEqual(mockAdmin);
|
||||
expect(mockRepository.getAdminByAuth0Sub).toHaveBeenCalledWith('auth0|123456');
|
||||
expect(mockRepository.getAdminById).toHaveBeenCalledWith('7c9e6679-7425-40de-944b-e07fc1f90ae7');
|
||||
});
|
||||
|
||||
it('should return null when admin not found', async () => {
|
||||
mockRepository.getAdminByAuth0Sub.mockResolvedValue(null);
|
||||
mockRepository.getAdminById.mockResolvedValue(null);
|
||||
|
||||
const result = await adminService.getAdminByAuth0Sub('auth0|unknown');
|
||||
const result = await adminService.getAdminById('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -57,12 +68,15 @@ describe('AdminService', () => {
|
||||
|
||||
describe('createAdmin', () => {
|
||||
it('should create new admin and log audit', async () => {
|
||||
const newAdminId = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e';
|
||||
const creatorId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
|
||||
const mockAdmin = {
|
||||
auth0Sub: 'auth0|newadmin',
|
||||
id: newAdminId,
|
||||
userProfileId: newAdminId,
|
||||
email: 'newadmin@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'auth0|existing',
|
||||
createdBy: creatorId,
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -74,16 +88,16 @@ describe('AdminService', () => {
|
||||
const result = await adminService.createAdmin(
|
||||
'newadmin@motovaultpro.com',
|
||||
'admin',
|
||||
'auth0|newadmin',
|
||||
'auth0|existing'
|
||||
newAdminId,
|
||||
creatorId
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockAdmin);
|
||||
expect(mockRepository.createAdmin).toHaveBeenCalled();
|
||||
expect(mockRepository.logAuditAction).toHaveBeenCalledWith(
|
||||
'auth0|existing',
|
||||
creatorId,
|
||||
'CREATE',
|
||||
mockAdmin.auth0Sub,
|
||||
mockAdmin.id,
|
||||
'admin_user',
|
||||
mockAdmin.email,
|
||||
expect.any(Object)
|
||||
@@ -91,12 +105,14 @@ describe('AdminService', () => {
|
||||
});
|
||||
|
||||
it('should reject if admin already exists', async () => {
|
||||
const existingId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
|
||||
const existingAdmin = {
|
||||
auth0Sub: 'auth0|existing',
|
||||
id: existingId,
|
||||
userProfileId: existingId,
|
||||
email: 'admin@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -104,39 +120,46 @@ describe('AdminService', () => {
|
||||
mockRepository.getAdminByEmail.mockResolvedValue(existingAdmin);
|
||||
|
||||
await expect(
|
||||
adminService.createAdmin('admin@motovaultpro.com', 'admin', 'auth0|new', 'auth0|existing')
|
||||
adminService.createAdmin('admin@motovaultpro.com', 'admin', '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e', existingId)
|
||||
).rejects.toThrow('already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeAdmin', () => {
|
||||
it('should revoke admin when multiple active admins exist', async () => {
|
||||
const toRevokeId = 'a1b2c3d4-e5f6-7890-1234-567890abcdef';
|
||||
const admin1Id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
const admin2Id = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e';
|
||||
|
||||
const revokedAdmin = {
|
||||
auth0Sub: 'auth0|toadmin',
|
||||
id: toRevokeId,
|
||||
userProfileId: toRevokeId,
|
||||
email: 'toadmin@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const activeAdmins = [
|
||||
{
|
||||
auth0Sub: 'auth0|admin1',
|
||||
id: admin1Id,
|
||||
userProfileId: admin1Id,
|
||||
email: 'admin1@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
auth0Sub: 'auth0|admin2',
|
||||
id: admin2Id,
|
||||
userProfileId: admin2Id,
|
||||
email: 'admin2@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
@@ -146,20 +169,22 @@ describe('AdminService', () => {
|
||||
mockRepository.revokeAdmin.mockResolvedValue(revokedAdmin);
|
||||
mockRepository.logAuditAction.mockResolvedValue({} as any);
|
||||
|
||||
const result = await adminService.revokeAdmin('auth0|toadmin', 'auth0|admin1');
|
||||
const result = await adminService.revokeAdmin(toRevokeId, admin1Id);
|
||||
|
||||
expect(result).toEqual(revokedAdmin);
|
||||
expect(mockRepository.revokeAdmin).toHaveBeenCalledWith('auth0|toadmin');
|
||||
expect(mockRepository.revokeAdmin).toHaveBeenCalledWith(toRevokeId);
|
||||
expect(mockRepository.logAuditAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent revoking last active admin', async () => {
|
||||
const lastAdminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
|
||||
const lastAdmin = {
|
||||
auth0Sub: 'auth0|lastadmin',
|
||||
id: lastAdminId,
|
||||
userProfileId: lastAdminId,
|
||||
email: 'last@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -167,19 +192,22 @@ describe('AdminService', () => {
|
||||
mockRepository.getActiveAdmins.mockResolvedValue([lastAdmin]);
|
||||
|
||||
await expect(
|
||||
adminService.revokeAdmin('auth0|lastadmin', 'auth0|lastadmin')
|
||||
adminService.revokeAdmin(lastAdminId, lastAdminId)
|
||||
).rejects.toThrow('Cannot revoke the last active admin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reinstateAdmin', () => {
|
||||
it('should reinstate revoked admin and log audit', async () => {
|
||||
const reinstateId = 'b2c3d4e5-f6a7-8901-2345-678901bcdef0';
|
||||
const adminActorId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
|
||||
const reinstatedAdmin = {
|
||||
auth0Sub: 'auth0|reinstate',
|
||||
id: reinstateId,
|
||||
userProfileId: reinstateId,
|
||||
email: 'reinstate@motovaultpro.com',
|
||||
role: 'admin',
|
||||
role: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440000',
|
||||
revokedAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -187,14 +215,14 @@ describe('AdminService', () => {
|
||||
mockRepository.reinstateAdmin.mockResolvedValue(reinstatedAdmin);
|
||||
mockRepository.logAuditAction.mockResolvedValue({} as any);
|
||||
|
||||
const result = await adminService.reinstateAdmin('auth0|reinstate', 'auth0|admin');
|
||||
const result = await adminService.reinstateAdmin(reinstateId, adminActorId);
|
||||
|
||||
expect(result).toEqual(reinstatedAdmin);
|
||||
expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith('auth0|reinstate');
|
||||
expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith(reinstateId);
|
||||
expect(mockRepository.logAuditAction).toHaveBeenCalledWith(
|
||||
'auth0|admin',
|
||||
adminActorId,
|
||||
'REINSTATE',
|
||||
'auth0|reinstate',
|
||||
reinstateId,
|
||||
'admin_user',
|
||||
reinstatedAdmin.email
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user