add tests
Some checks failed
Contract Testing / contract-tests (push) Failing after 6m7s
Contract Testing / contract-snapshot (push) Failing after 4m46s

This commit is contained in:
2026-01-22 11:52:42 +01:00
parent 40bc15ff61
commit fb1221701d
112 changed files with 30625 additions and 1059 deletions

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminController } from './AdminController';
import { AdminModule } from './AdminModule';
import { AdminService } from './AdminService';
describe('AdminModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AdminModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide AdminController', () => {
const controller = module.get<AdminController>(AdminController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(AdminController);
});
it('should provide AdminService', () => {
const service = module.get<AdminService>(AdminService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(AdminService);
});
});

View File

@@ -0,0 +1,40 @@
import { SetMetadata } from '@nestjs/common';
import { RequireSystemAdmin, REQUIRE_SYSTEM_ADMIN_METADATA_KEY } from './RequireSystemAdmin';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}),
}));
describe('RequireSystemAdmin', () => {
it('should return a method decorator', () => {
const decorator = RequireSystemAdmin();
expect(typeof decorator).toBe('function');
});
it('should call SetMetadata with correct key and value', () => {
RequireSystemAdmin();
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_SYSTEM_ADMIN_METADATA_KEY, {
required: true,
});
});
it('should return a decorator that can be used as both method and class decorator', () => {
const decorator = RequireSystemAdmin();
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor
expect(result).toBe(mockDescriptor);
});
it('should have correct metadata key', () => {
expect(REQUIRE_SYSTEM_ADMIN_METADATA_KEY).toBe('gridpilot:requireSystemAdmin');
});
});

View File

@@ -0,0 +1,680 @@
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
import { Result } from '@core/shared/domain/Result';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GetDashboardStatsUseCase } from './GetDashboardStatsUseCase';
// Mock dependencies
const mockAdminUserRepo = {
findById: vi.fn(),
list: vi.fn(),
};
describe('GetDashboardStatsUseCase', () => {
describe('TDD - Test First', () => {
let useCase: GetDashboardStatsUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new GetDashboardStatsUseCase(mockAdminUserRepo as never);
});
describe('execute', () => {
it('should return error when actor is not found', async () => {
// Arrange
mockAdminUserRepo.findById.mockResolvedValue(null);
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
expect(error.details.message).toBe('Actor not found');
});
it('should return error when actor is not authorized to view dashboard', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['user'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
expect(error.details.message).toBe('User is not authorized to view dashboard');
});
it('should return empty stats when no users exist', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(0);
expect(stats.activeUsers).toBe(0);
expect(stats.suspendedUsers).toBe(0);
expect(stats.deletedUsers).toBe(0);
expect(stats.systemAdmins).toBe(0);
expect(stats.recentLogins).toBe(0);
expect(stats.newUsersToday).toBe(0);
expect(stats.userGrowth).toEqual([]);
expect(stats.roleDistribution).toEqual([]);
expect(stats.statusDistribution).toEqual({
active: 0,
suspended: 0,
deleted: 0,
});
expect(stats.activityTimeline).toEqual([]);
});
it('should return correct stats when users exist', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'suspended',
createdAt: new Date(),
updatedAt: new Date(),
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['owner'],
status: 'deleted',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(3);
expect(stats.activeUsers).toBe(1);
expect(stats.suspendedUsers).toBe(1);
expect(stats.deletedUsers).toBe(1);
expect(stats.systemAdmins).toBe(2); // actor + user3
expect(stats.recentLogins).toBe(0); // no recent logins
expect(stats.newUsersToday).toBe(3); // all created today
expect(stats.userGrowth).toHaveLength(7);
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.statusDistribution).toEqual({
active: 1,
suspended: 1,
deleted: 1,
});
expect(stats.activityTimeline).toHaveLength(7);
});
it('should count recent logins correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const recentLoginUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000 * 2), // 2 days ago
updatedAt: new Date(Date.now() - 86400000 * 2),
lastLoginAt: new Date(Date.now() - 3600000), // 1 hour ago
});
const oldLoginUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000 * 2),
updatedAt: new Date(Date.now() - 86400000 * 2),
lastLoginAt: new Date(Date.now() - 86400000 * 2), // 2 days ago
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [recentLoginUser, oldLoginUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.recentLogins).toBe(1);
});
it('should count new users today correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const todayUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const yesterdayUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000),
updatedAt: new Date(Date.now() - 86400000),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [todayUser, yesterdayUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.newUsersToday).toBe(1);
});
it('should calculate role distribution correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['owner'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.roleDistribution).toContainEqual({
label: 'Owner',
value: 2,
color: 'text-purple-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'Admin',
value: 1,
color: 'text-blue-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'User',
value: 1,
color: 'text-gray-500',
});
});
it('should handle users with multiple roles', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user', 'admin'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.roleDistribution).toHaveLength(2);
expect(stats.roleDistribution).toContainEqual({
label: 'User',
value: 1,
color: 'text-gray-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'Admin',
value: 1,
color: 'text-blue-500',
});
});
it('should calculate user growth for last 7 days', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const twoDaysAgo = new Date(today);
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: today,
updatedAt: today,
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: yesterday,
updatedAt: yesterday,
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['user'],
status: 'active',
createdAt: twoDaysAgo,
updatedAt: twoDaysAgo,
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.userGrowth).toHaveLength(7);
// Check that today has 1 user
const todayEntry = stats.userGrowth[6];
expect(todayEntry.value).toBe(1);
// Check that yesterday has 1 user
const yesterdayEntry = stats.userGrowth[5];
expect(yesterdayEntry.value).toBe(1);
// Check that two days ago has 1 user
const twoDaysAgoEntry = stats.userGrowth[4];
expect(twoDaysAgoEntry.value).toBe(1);
});
it('should calculate activity timeline for last 7 days', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const newUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: today,
updatedAt: today,
});
const recentLoginUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: yesterday,
updatedAt: yesterday,
lastLoginAt: new Date(Date.now() - 3600000), // 1 hour ago
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [newUser, recentLoginUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.activityTimeline).toHaveLength(7);
// Check today's entry
const todayEntry = stats.activityTimeline[6];
expect(todayEntry.newUsers).toBe(1);
expect(todayEntry.logins).toBe(1);
// Check yesterday's entry
const yesterdayEntry = stats.activityTimeline[5];
expect(yesterdayEntry.newUsers).toBe(0);
expect(yesterdayEntry.logins).toBe(0);
});
it('should handle repository errors gracefully', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockRejectedValue(new Error('Database connection failed'));
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Database connection failed');
});
it('should handle non-Error exceptions', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockRejectedValue('String error');
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Failed to get dashboard stats');
});
it('should work with owner role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
});
it('should work with admin role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['admin'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
});
it('should reject user role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['user'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle suspended actor', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'suspended',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle deleted actor', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'deleted',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle large number of users efficiently', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const users = Array.from({ length: 1000 }, (_, i) =>
AdminUser.create({
id: `user-${i}`,
email: `user${i}@example.com`,
displayName: `User ${i}`,
roles: i % 3 === 0 ? ['owner'] : i % 3 === 1 ? ['admin'] : ['user'],
status: i % 4 === 0 ? 'suspended' : i % 4 === 1 ? 'deleted' : 'active',
createdAt: new Date(Date.now() - i * 3600000),
updatedAt: new Date(Date.now() - i * 3600000),
lastLoginAt: i % 10 === 0 ? new Date(Date.now() - i * 3600000) : undefined,
})
);
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(1000);
expect(stats.activeUsers).toBe(500);
expect(stats.suspendedUsers).toBe(250);
expect(stats.deletedUsers).toBe(250);
expect(stats.systemAdmins).toBe(334); // owner + admin
expect(stats.recentLogins).toBe(100); // 10% of users
expect(stats.userGrowth).toHaveLength(7);
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.activityTimeline).toHaveLength(7);
});
});
});
});