diff --git a/libs/domains/framework/backend/feature-agent-controller/src/index.ts b/libs/domains/framework/backend/feature-agent-controller/src/index.ts index c44ca5c4..09643bbc 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/index.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/index.ts @@ -1,5 +1,5 @@ -export * from './lib/clients.controller'; -export * from './lib/clients.module'; +export * from './lib/decorators/keycloak-roles.decorator'; +export * from './lib/decorators/users-roles.decorator'; export * from './lib/dto/client-response.dto'; export * from './lib/dto/create-client-response.dto'; export * from './lib/dto/create-client.dto'; @@ -8,7 +8,6 @@ export * from './lib/entities/client-agent-credential.entity'; export * from './lib/entities/client-user.entity'; export * from './lib/entities/client.entity'; export * from './lib/entities/provisioning-reference.entity'; -export * from './lib/entities/user.entity'; export * from './lib/entities/statistics-agent.entity'; export * from './lib/entities/statistics-chat-filter-drop.entity'; export * from './lib/entities/statistics-chat-filter-flag.entity'; @@ -18,16 +17,16 @@ export * from './lib/entities/statistics-client.entity'; export * from './lib/entities/statistics-entity-event.entity'; export * from './lib/entities/statistics-provisioning-reference.entity'; export * from './lib/entities/statistics-user.entity'; -export * from './lib/decorators/keycloak-roles.decorator'; -export * from './lib/decorators/users-roles.decorator'; -export * from './lib/keycloak-user-sync.module'; +export * from './lib/entities/user.entity'; +export * from './lib/modules/clients.module'; +export * from './lib/modules/keycloak-user-sync.module'; +export * from './lib/modules/statistics.module'; +export * from './lib/modules/users-auth.module'; export * from './lib/repositories/client-agent-credentials.repository'; export * from './lib/repositories/clients.repository'; +export * from './lib/repositories/statistics.repository'; export * from './lib/services/client-agent-credentials.service'; export * from './lib/services/client-agent-proxy.service'; export * from './lib/services/clients.service'; export * from './lib/services/keycloak-token.service'; export * from './lib/services/statistics.service'; -export * from './lib/repositories/statistics.repository'; -export * from './lib/statistics.module'; -export * from './lib/users-auth.module'; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/auth.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/auth.controller.spec.ts new file mode 100644 index 00000000..3657766b --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/auth.controller.spec.ts @@ -0,0 +1,709 @@ +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { AuthController } from './auth.controller'; +import { AuthService } from '../services/auth.service'; +import { LoginDto } from '../dto/auth/login.dto'; +import { RegisterDto } from '../dto/auth/register.dto'; +import { ConfirmEmailDto } from '../dto/auth/confirm-email.dto'; +import { RequestPasswordResetDto } from '../dto/auth/request-password-reset.dto'; +import { ResetPasswordDto } from '../dto/auth/reset-password.dto'; +import { ChangePasswordDto } from '../dto/auth/change-password.dto'; +import { UsersAuthGuard } from '../guards/users-auth.guard'; +import { UserRole } from '../entities/user.entity'; + +describe('AuthController', () => { + let controller: AuthController; + let service: jest.Mocked; + + const mockLoginResponse = { + access_token: 'jwt-token-123', + user: { + id: 'user-uuid', + email: 'test@example.com', + role: UserRole.USER, + }, + }; + + const mockRegisterResponse = { + user: { + id: 'user-uuid', + email: 'newuser@example.com', + role: UserRole.USER, + }, + message: + 'Account created. Please confirm your email before logging in. Check your inbox for the confirmation code.', + }; + + const mockFirstUserRegisterResponse = { + user: { + id: 'admin-uuid', + email: 'admin@example.com', + role: UserRole.ADMIN, + }, + message: 'Account created successfully. You can log in immediately.', + }; + + const mockService = { + login: jest.fn(), + register: jest.fn(), + confirmEmail: jest.fn(), + requestPasswordReset: jest.fn(), + resetPassword: jest.fn(), + changePassword: jest.fn(), + }; + + const mockJwtService = { + verifyAsync: jest.fn(), + sign: jest.fn(), + }; + + const mockReflector = { + getAllAndOverride: jest.fn().mockReturnValue(true), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthService, + useValue: mockService, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: Reflector, + useValue: mockReflector, + }, + UsersAuthGuard, + ], + }).compile(); + + controller = module.get(AuthController); + service = module.get(AuthService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('login', () => { + it('should successfully login with valid credentials', async () => { + const loginDto: LoginDto = { + email: 'test@example.com', + password: 'password123', + }; + + service.login.mockResolvedValue(mockLoginResponse); + + const result = await controller.login(loginDto); + + expect(result).toEqual(mockLoginResponse); + expect(result.access_token).toBe('jwt-token-123'); + expect(result.user.email).toBe('test@example.com'); + expect(service.login).toHaveBeenCalledWith('test@example.com', 'password123'); + }); + + it('should login and return user with USER role', async () => { + const loginDto: LoginDto = { + email: 'user@example.com', + password: 'userpass123', + }; + + service.login.mockResolvedValue(mockLoginResponse); + + const result = await controller.login(loginDto); + + expect(result.user.role).toBe(UserRole.USER); + expect(service.login).toHaveBeenCalledWith('user@example.com', 'userpass123'); + }); + + it('should login and return user with ADMIN role', async () => { + const loginDto: LoginDto = { + email: 'admin@example.com', + password: 'adminpass123', + }; + + const adminLoginResponse = { + access_token: 'admin-jwt-token', + user: { + id: 'admin-uuid', + email: 'admin@example.com', + role: UserRole.ADMIN, + }, + }; + + service.login.mockResolvedValue(adminLoginResponse); + + const result = await controller.login(loginDto); + + expect(result.user.role).toBe(UserRole.ADMIN); + expect(service.login).toHaveBeenCalledWith('admin@example.com', 'adminpass123'); + }); + + it('should handle login with special characters in email', async () => { + const loginDto: LoginDto = { + email: 'user+test@example.com', + password: 'password123', + }; + + service.login.mockResolvedValue({ + ...mockLoginResponse, + user: { ...mockLoginResponse.user, email: 'user+test@example.com' }, + }); + + const result = await controller.login(loginDto); + + expect(result.user.email).toBe('user+test@example.com'); + expect(service.login).toHaveBeenCalledWith('user+test@example.com', 'password123'); + }); + + it('should handle login with long password', async () => { + const loginDto: LoginDto = { + email: 'test@example.com', + password: 'VeryLongPasswordWithManyCharacters123', + }; + + service.login.mockResolvedValue(mockLoginResponse); + + const result = await controller.login(loginDto); + + expect(result).toEqual(mockLoginResponse); + expect(service.login).toHaveBeenCalledWith('test@example.com', 'VeryLongPasswordWithManyCharacters123'); + }); + }); + + describe('register', () => { + it('should successfully register a new user', async () => { + const registerDto: RegisterDto = { + email: 'newuser@example.com', + password: 'newpassword123', + }; + + service.register.mockResolvedValue(mockRegisterResponse); + + const result = await controller.register(registerDto); + + expect(result).toEqual(mockRegisterResponse); + expect(result.user.email).toBe('newuser@example.com'); + expect(result.user.role).toBe(UserRole.USER); + expect(result.message).toContain('confirm your email'); + expect(service.register).toHaveBeenCalledWith('newuser@example.com', 'newpassword123'); + }); + + it('should register first user as admin', async () => { + const registerDto: RegisterDto = { + email: 'admin@example.com', + password: 'adminpass123', + }; + + service.register.mockResolvedValue(mockFirstUserRegisterResponse); + + const result = await controller.register(registerDto); + + expect(result.user.role).toBe(UserRole.ADMIN); + expect(result.message).toContain('You can log in immediately'); + expect(service.register).toHaveBeenCalledWith('admin@example.com', 'adminpass123'); + }); + + it('should register user with minimum password length', async () => { + const registerDto: RegisterDto = { + email: 'test@example.com', + password: '12345678', + }; + + service.register.mockResolvedValue(mockRegisterResponse); + + const result = await controller.register(registerDto); + + expect(result).toEqual(mockRegisterResponse); + expect(service.register).toHaveBeenCalledWith('test@example.com', '12345678'); + }); + + it('should register user with complex password', async () => { + const registerDto: RegisterDto = { + email: 'secure@example.com', + password: 'ComplexPassword123', + }; + + service.register.mockResolvedValue(mockRegisterResponse); + + const result = await controller.register(registerDto); + + expect(result).toEqual(mockRegisterResponse); + expect(service.register).toHaveBeenCalledWith('secure@example.com', 'ComplexPassword123'); + }); + + it('should register user with special characters in email', async () => { + const registerDto: RegisterDto = { + email: 'user+tag@sub.example.com', + password: 'password123', + }; + + service.register.mockResolvedValue({ + ...mockRegisterResponse, + user: { ...mockRegisterResponse.user, email: 'user+tag@sub.example.com' }, + }); + + const result = await controller.register(registerDto); + + expect(result.user.email).toBe('user+tag@sub.example.com'); + expect(service.register).toHaveBeenCalledWith('user+tag@sub.example.com', 'password123'); + }); + }); + + describe('confirmEmail', () => { + it('should successfully confirm email with valid code', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: 'ABC123', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result.message).toContain('Email confirmed successfully'); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', 'ABC123'); + }); + + it('should confirm email with uppercase code', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: 'XYZ789', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result.message).toBe('Email confirmed successfully. You can now log in.'); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', 'XYZ789'); + }); + + it('should confirm email with numeric code', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: '123456', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result).toEqual({ message: 'Email confirmed successfully. You can now log in.' }); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', '123456'); + }); + + it('should confirm email with alphanumeric code', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: 'A1B2C3', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result).toEqual({ message: 'Email confirmed successfully. You can now log in.' }); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', 'A1B2C3'); + }); + }); + + describe('requestPasswordReset', () => { + it('should request password reset for existing user', async () => { + const requestDto: RequestPasswordResetDto = { + email: 'test@example.com', + }; + + service.requestPasswordReset.mockResolvedValue({ + message: 'If an account exists with this email, you will receive a password reset code.', + }); + + const result = await controller.requestPasswordReset(requestDto); + + expect(result.message).toContain('password reset code'); + expect(service.requestPasswordReset).toHaveBeenCalledWith('test@example.com'); + }); + + it('should request password reset for non-existing user with same response', async () => { + const requestDto: RequestPasswordResetDto = { + email: 'nonexistent@example.com', + }; + + service.requestPasswordReset.mockResolvedValue({ + message: 'If an account exists with this email, you will receive a password reset code.', + }); + + const result = await controller.requestPasswordReset(requestDto); + + expect(result.message).toBe('If an account exists with this email, you will receive a password reset code.'); + expect(service.requestPasswordReset).toHaveBeenCalledWith('nonexistent@example.com'); + }); + + it('should handle request with special characters in email', async () => { + const requestDto: RequestPasswordResetDto = { + email: 'user+test@example.com', + }; + + service.requestPasswordReset.mockResolvedValue({ + message: 'If an account exists with this email, you will receive a password reset code.', + }); + + const result = await controller.requestPasswordReset(requestDto); + + expect(result).toEqual({ + message: 'If an account exists with this email, you will receive a password reset code.', + }); + expect(service.requestPasswordReset).toHaveBeenCalledWith('user+test@example.com'); + }); + + it('should handle multiple requests for same email', async () => { + const requestDto: RequestPasswordResetDto = { + email: 'test@example.com', + }; + + service.requestPasswordReset.mockResolvedValue({ + message: 'If an account exists with this email, you will receive a password reset code.', + }); + + const result1 = await controller.requestPasswordReset(requestDto); + const result2 = await controller.requestPasswordReset(requestDto); + + expect(result1).toEqual(result2); + expect(service.requestPasswordReset).toHaveBeenCalledTimes(2); + }); + }); + + describe('resetPassword', () => { + it('should successfully reset password with valid code', async () => { + const resetDto: ResetPasswordDto = { + email: 'test@example.com', + code: 'ABC123', + newPassword: 'newpassword123', + }; + + service.resetPassword.mockResolvedValue({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + + const result = await controller.resetPassword(resetDto); + + expect(result.message).toContain('Password reset successfully'); + expect(service.resetPassword).toHaveBeenCalledWith('test@example.com', 'ABC123', 'newpassword123'); + }); + + it('should reset password with minimum length password', async () => { + const resetDto: ResetPasswordDto = { + email: 'test@example.com', + code: 'XYZ789', + newPassword: '12345678', + }; + + service.resetPassword.mockResolvedValue({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + + const result = await controller.resetPassword(resetDto); + + expect(result).toEqual({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + expect(service.resetPassword).toHaveBeenCalledWith('test@example.com', 'XYZ789', '12345678'); + }); + + it('should reset password with complex password', async () => { + const resetDto: ResetPasswordDto = { + email: 'test@example.com', + code: 'A1B2C3', + newPassword: 'ComplexPassword456', + }; + + service.resetPassword.mockResolvedValue({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + + const result = await controller.resetPassword(resetDto); + + expect(result.message).toBe('Password reset successfully. You can now log in with your new password.'); + expect(service.resetPassword).toHaveBeenCalledWith('test@example.com', 'A1B2C3', 'ComplexPassword456'); + }); + + it('should reset password with numeric code', async () => { + const resetDto: ResetPasswordDto = { + email: 'test@example.com', + code: '123456', + newPassword: 'newpassword789', + }; + + service.resetPassword.mockResolvedValue({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + + const result = await controller.resetPassword(resetDto); + + expect(result).toEqual({ + message: 'Password reset successfully. You can now log in with your new password.', + }); + expect(service.resetPassword).toHaveBeenCalledWith('test@example.com', '123456', 'newpassword789'); + }); + }); + + describe('changePassword', () => { + it('should successfully change password with valid current password', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'newpassword456', + newPasswordConfirmation: 'newpassword456', + }; + + const mockReq = { + user: { id: 'user-uuid' }, + } as any; + + service.changePassword.mockResolvedValue({ + message: 'Password changed successfully.', + }); + + const result = await controller.changePassword(changeDto, mockReq); + + expect(result.message).toBe('Password changed successfully.'); + expect(service.changePassword).toHaveBeenCalledWith( + 'user-uuid', + 'oldpassword123', + 'newpassword456', + 'newpassword456', + ); + }); + + it('should throw error when user is not authenticated', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'newpassword456', + newPasswordConfirmation: 'newpassword456', + }; + + const mockReq = {} as any; + + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow(BadRequestException); + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow('User not authenticated'); + }); + + it('should throw error when user.id is undefined', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'newpassword456', + newPasswordConfirmation: 'newpassword456', + }; + + const mockReq = { + user: {}, + } as any; + + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow(BadRequestException); + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow('User not authenticated'); + }); + + it('should throw error when passwords do not match', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'newpassword456', + newPasswordConfirmation: 'differentpassword789', + }; + + const mockReq = { + user: { id: 'user-uuid' }, + } as any; + + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow(BadRequestException); + await expect(controller.changePassword(changeDto, mockReq)).rejects.toThrow( + 'New password and confirmation do not match', + ); + }); + + it('should change password with minimum length password', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: '12345678', + newPasswordConfirmation: '12345678', + }; + + const mockReq = { + user: { id: 'user-uuid' }, + } as any; + + service.changePassword.mockResolvedValue({ + message: 'Password changed successfully.', + }); + + const result = await controller.changePassword(changeDto, mockReq); + + expect(result).toEqual({ message: 'Password changed successfully.' }); + expect(service.changePassword).toHaveBeenCalledWith('user-uuid', 'oldpassword123', '12345678', '12345678'); + }); + + it('should change password with complex password', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'ComplexPassword789', + newPasswordConfirmation: 'ComplexPassword789', + }; + + const mockReq = { + user: { id: 'user-uuid' }, + } as any; + + service.changePassword.mockResolvedValue({ + message: 'Password changed successfully.', + }); + + const result = await controller.changePassword(changeDto, mockReq); + + expect(result.message).toBe('Password changed successfully.'); + expect(service.changePassword).toHaveBeenCalledWith( + 'user-uuid', + 'oldpassword123', + 'ComplexPassword789', + 'ComplexPassword789', + ); + }); + + it('should handle request with different user id', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'oldpassword123', + newPassword: 'newpassword456', + newPasswordConfirmation: 'newpassword456', + }; + + const mockReq = { + user: { id: 'different-user-uuid' }, + } as any; + + service.changePassword.mockResolvedValue({ + message: 'Password changed successfully.', + }); + + const result = await controller.changePassword(changeDto, mockReq); + + expect(result).toEqual({ message: 'Password changed successfully.' }); + expect(service.changePassword).toHaveBeenCalledWith( + 'different-user-uuid', + 'oldpassword123', + 'newpassword456', + 'newpassword456', + ); + }); + }); + + describe('edge cases', () => { + it('should handle login with empty JWT token response', async () => { + const loginDto: LoginDto = { + email: 'test@example.com', + password: 'password123', + }; + + service.login.mockResolvedValue({ + access_token: '', + user: { + id: 'user-uuid', + email: 'test@example.com', + role: UserRole.USER, + }, + }); + + const result = await controller.login(loginDto); + + expect(result.access_token).toBe(''); + expect(service.login).toHaveBeenCalledWith('test@example.com', 'password123'); + }); + + it('should handle change password when new password same as current', async () => { + const changeDto: ChangePasswordDto = { + currentPassword: 'samepassword123', + newPassword: 'samepassword123', + newPasswordConfirmation: 'samepassword123', + }; + + const mockReq = { + user: { id: 'user-uuid' }, + } as any; + + service.changePassword.mockResolvedValue({ + message: 'Password changed successfully.', + }); + + const result = await controller.changePassword(changeDto, mockReq); + + expect(result).toEqual({ message: 'Password changed successfully.' }); + expect(service.changePassword).toHaveBeenCalledWith( + 'user-uuid', + 'samepassword123', + 'samepassword123', + 'samepassword123', + ); + }); + }); + + describe('validation scenarios', () => { + it('should handle login with mixed case email', async () => { + const loginDto: LoginDto = { + email: 'TeSt@ExAmPlE.CoM', + password: 'password123', + }; + + service.login.mockResolvedValue({ + ...mockLoginResponse, + user: { ...mockLoginResponse.user, email: 'TeSt@ExAmPlE.CoM' }, + }); + + const result = await controller.login(loginDto); + + expect(result.user.email).toBe('TeSt@ExAmPlE.CoM'); + expect(service.login).toHaveBeenCalledWith('TeSt@ExAmPlE.CoM', 'password123'); + }); + + it('should handle confirm email code with all numbers', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: '000000', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result.message).toBe('Email confirmed successfully. You can now log in.'); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', '000000'); + }); + + it('should handle confirm email code with all letters', async () => { + const confirmDto: ConfirmEmailDto = { + email: 'test@example.com', + code: 'ABCDEF', + }; + + service.confirmEmail.mockResolvedValue({ + message: 'Email confirmed successfully. You can now log in.', + }); + + const result = await controller.confirmEmail(confirmDto); + + expect(result).toEqual({ message: 'Email confirmed successfully. You can now log in.' }); + expect(service.confirmEmail).toHaveBeenCalledWith('test@example.com', 'ABCDEF'); + }); + }); +}); diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.spec.ts similarity index 94% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.spec.ts index a93fcb7e..52f8c8dc 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.spec.ts @@ -1,12 +1,12 @@ import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { StatisticsQueryService } from '../services/statistics-query.service'; +import * as clientAccessUtils from '../utils/client-access.utils'; import { ClientStatisticsController } from './client-statistics.controller'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { StatisticsQueryService } from './services/statistics-query.service'; -import * as clientAccessUtils from './utils/client-access.utils'; -jest.mock('./utils/client-access.utils', () => ({ +jest.mock('../utils/client-access.utils', () => ({ ensureClientAccess: jest.fn(), })); diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.ts similarity index 91% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.ts index 561fd31f..a1a184bb 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/client-statistics.controller.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/client-statistics.controller.ts @@ -1,10 +1,10 @@ import { Controller, Get, Param, ParseIntPipe, ParseUUIDPipe, Query, Req } from '@nestjs/common'; -import { ChatDirection } from './entities/statistics-chat-io.entity'; -import { StatisticsEntityEventType, StatisticsEntityType } from './entities/statistics-entity-event.entity'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { StatisticsQueryService } from './services/statistics-query.service'; -import { ensureClientAccess, type RequestWithUser } from './utils/client-access.utils'; +import { ChatDirection } from '../entities/statistics-chat-io.entity'; +import { StatisticsEntityEventType, StatisticsEntityType } from '../entities/statistics-entity-event.entity'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { StatisticsQueryService } from '../services/statistics-query.service'; +import { ensureClientAccess, type RequestWithUser } from '../utils/client-access.utils'; /** * Controller for client-scoped statistics endpoints. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.spec.ts similarity index 97% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.spec.ts index 36d25340..e3989ec2 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.spec.ts @@ -1,9 +1,9 @@ import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentDeploymentsProxyService } from '../services/client-agent-deployments-proxy.service'; import { ClientsDeploymentsController } from './clients-deployments.controller'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentDeploymentsProxyService } from './services/client-agent-deployments-proxy.service'; describe('ClientsDeploymentsController', () => { let controller: ClientsDeploymentsController; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.ts similarity index 95% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.ts index 73a46581..9e26657e 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-deployments.controller.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-deployments.controller.ts @@ -12,10 +12,10 @@ import { Query, Req, } from '@nestjs/common'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentDeploymentsProxyService } from './services/client-agent-deployments-proxy.service'; -import { ensureClientAccess, type RequestWithUser } from './utils/client-access.utils'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentDeploymentsProxyService } from '../services/client-agent-deployments-proxy.service'; +import { ensureClientAccess, type RequestWithUser } from '../utils/client-access.utils'; /** * Controller for proxied deployment and CI/CD pipeline endpoints. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.spec.ts similarity index 97% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.spec.ts index fb8de496..96604d22 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.spec.ts @@ -11,10 +11,10 @@ import { } from '@forepath/framework/backend/feature-agent-manager'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentVcsProxyService } from '../services/client-agent-vcs-proxy.service'; import { ClientsVcsController } from './clients-vcs.controller'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentVcsProxyService } from './services/client-agent-vcs-proxy.service'; describe('ClientsVcsController', () => { let controller: ClientsVcsController; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.ts similarity index 96% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.ts index e6647e77..8d3f5dd3 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients-vcs.controller.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients-vcs.controller.ts @@ -24,10 +24,10 @@ import { Query, Req, } from '@nestjs/common'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentVcsProxyService } from './services/client-agent-vcs-proxy.service'; -import { ensureClientAccess, type RequestWithUser } from './utils/client-access.utils'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentVcsProxyService } from '../services/client-agent-vcs-proxy.service'; +import { ensureClientAccess, type RequestWithUser } from '../utils/client-access.utils'; /** * Controller for proxied agent VCS (Version Control System) operations. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.spec.ts similarity index 96% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.spec.ts index 22eb2401..b0e079b6 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.spec.ts @@ -15,28 +15,28 @@ import { } from '@forepath/framework/backend/feature-agent-manager'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { AddClientUserDto } from '../dto/add-client-user.dto'; +import { ClientResponseDto } from '../dto/client-response.dto'; +import { ClientUserResponseDto } from '../dto/client-user-response.dto'; +import { CreateClientResponseDto } from '../dto/create-client-response.dto'; +import { CreateClientDto } from '../dto/create-client.dto'; +import { ProvisionServerDto } from '../dto/provision-server.dto'; +import { ProvisionedServerResponseDto } from '../dto/provisioned-server-response.dto'; +import { UpdateClientDto } from '../dto/update-client.dto'; +import { ClientUserRole } from '../entities/client-user.entity'; +import { AuthenticationType } from '../entities/client.entity'; +import { UserRole } from '../entities/user.entity'; +import { ProvisioningProviderFactory } from '../providers/provisioning-provider.factory'; +import { ProvisioningProvider } from '../providers/provisioning-provider.interface'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentEnvironmentVariablesProxyService } from '../services/client-agent-environment-variables-proxy.service'; +import { ClientAgentFileSystemProxyService } from '../services/client-agent-file-system-proxy.service'; +import { ClientAgentProxyService } from '../services/client-agent-proxy.service'; +import { ClientUsersService } from '../services/client-users.service'; +import { ClientsService } from '../services/clients.service'; +import { ProvisioningService } from '../services/provisioning.service'; import { ClientsController } from './clients.controller'; -import { AddClientUserDto } from './dto/add-client-user.dto'; -import { ClientResponseDto } from './dto/client-response.dto'; -import { ClientUserResponseDto } from './dto/client-user-response.dto'; -import { CreateClientResponseDto } from './dto/create-client-response.dto'; -import { CreateClientDto } from './dto/create-client.dto'; -import { ProvisionServerDto } from './dto/provision-server.dto'; -import { ProvisionedServerResponseDto } from './dto/provisioned-server-response.dto'; -import { UpdateClientDto } from './dto/update-client.dto'; -import { ClientUserRole } from './entities/client-user.entity'; -import { AuthenticationType } from './entities/client.entity'; -import { UserRole } from './entities/user.entity'; -import { ProvisioningProviderFactory } from './providers/provisioning-provider.factory'; -import { ProvisioningProvider } from './providers/provisioning-provider.interface'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentEnvironmentVariablesProxyService } from './services/client-agent-environment-variables-proxy.service'; -import { ClientAgentFileSystemProxyService } from './services/client-agent-file-system-proxy.service'; -import { ClientAgentProxyService } from './services/client-agent-proxy.service'; -import { ClientUsersService } from './services/client-users.service'; -import { ClientsService } from './services/clients.service'; -import { ProvisioningService } from './services/provisioning.service'; describe('ClientsController', () => { let controller: ClientsController; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.ts similarity index 96% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.ts index 43f522d2..b2f3f40e 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.controller.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/clients.controller.ts @@ -30,30 +30,30 @@ import { Query, Req, } from '@nestjs/common'; -import { AddClientUserDto } from './dto/add-client-user.dto'; -import { ClientResponseDto } from './dto/client-response.dto'; -import { ClientUserResponseDto } from './dto/client-user-response.dto'; -import { CreateClientResponseDto } from './dto/create-client-response.dto'; -import { CreateClientDto } from './dto/create-client.dto'; -import { ProvisionServerDto } from './dto/provision-server.dto'; -import { ProvisionedServerResponseDto } from './dto/provisioned-server-response.dto'; -import { UpdateClientDto } from './dto/update-client.dto'; -import { UserRole } from './entities/user.entity'; -import { ProvisioningProviderFactory } from './providers/provisioning-provider.factory'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentEnvironmentVariablesProxyService } from './services/client-agent-environment-variables-proxy.service'; -import { ClientAgentFileSystemProxyService } from './services/client-agent-file-system-proxy.service'; -import { ClientAgentProxyService } from './services/client-agent-proxy.service'; -import { ClientUsersService } from './services/client-users.service'; -import { ClientsService } from './services/clients.service'; -import { ProvisioningService } from './services/provisioning.service'; +import { AddClientUserDto } from '../dto/add-client-user.dto'; +import { ClientResponseDto } from '../dto/client-response.dto'; +import { ClientUserResponseDto } from '../dto/client-user-response.dto'; +import { CreateClientResponseDto } from '../dto/create-client-response.dto'; +import { CreateClientDto } from '../dto/create-client.dto'; +import { ProvisionServerDto } from '../dto/provision-server.dto'; +import { ProvisionedServerResponseDto } from '../dto/provisioned-server-response.dto'; +import { UpdateClientDto } from '../dto/update-client.dto'; +import { UserRole } from '../entities/user.entity'; +import { ProvisioningProviderFactory } from '../providers/provisioning-provider.factory'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientAgentEnvironmentVariablesProxyService } from '../services/client-agent-environment-variables-proxy.service'; +import { ClientAgentFileSystemProxyService } from '../services/client-agent-file-system-proxy.service'; +import { ClientAgentProxyService } from '../services/client-agent-proxy.service'; +import { ClientUsersService } from '../services/client-users.service'; +import { ClientsService } from '../services/clients.service'; +import { ProvisioningService } from '../services/provisioning.service'; import { checkClientAccess, ensureClientAccess, getUserFromRequest, type RequestWithUser, -} from './utils/client-access.utils'; +} from '../utils/client-access.utils'; /** * Controller for client management endpoints. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.spec.ts similarity index 97% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.spec.ts index 8fa9c82e..c54b35d7 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.spec.ts @@ -1,8 +1,8 @@ import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { ClientsService } from '../services/clients.service'; +import { StatisticsQueryService } from '../services/statistics-query.service'; import { StatisticsController } from './statistics.controller'; -import { ClientsService } from './services/clients.service'; -import { StatisticsQueryService } from './services/statistics-query.service'; describe('StatisticsController', () => { let controller: StatisticsController; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.ts similarity index 93% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.ts index 77c9a86e..c1a6ffc8 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.controller.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/statistics.controller.ts @@ -1,9 +1,9 @@ import { Controller, ForbiddenException, Get, ParseIntPipe, Query, Req } from '@nestjs/common'; -import { ChatDirection } from './entities/statistics-chat-io.entity'; -import { StatisticsEntityEventType, StatisticsEntityType } from './entities/statistics-entity-event.entity'; -import { ClientsService } from './services/clients.service'; -import { StatisticsQueryService } from './services/statistics-query.service'; -import { getUserFromRequest, type RequestWithUser } from './utils/client-access.utils'; +import { ChatDirection } from '../entities/statistics-chat-io.entity'; +import { StatisticsEntityEventType, StatisticsEntityType } from '../entities/statistics-entity-event.entity'; +import { ClientsService } from '../services/clients.service'; +import { StatisticsQueryService } from '../services/statistics-query.service'; +import { getUserFromRequest, type RequestWithUser } from '../utils/client-access.utils'; /** * Controller for aggregate statistics endpoints. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/users.controller.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/users.controller.spec.ts new file mode 100644 index 00000000..759b599c --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/controllers/users.controller.spec.ts @@ -0,0 +1,575 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; +import { CreateUserDto } from '../dto/auth/create-user.dto'; +import { UpdateUserDto } from '../dto/auth/update-user.dto'; +import { UserRole } from '../entities/user.entity'; +import { UsersAuthGuard } from '../guards/users-auth.guard'; +import { UsersService } from '../services/users.service'; +import { UsersController } from './users.controller'; + +describe('UsersController', () => { + let controller: UsersController; + let service: jest.Mocked; + + const mockUserResponse = { + id: 'test-user-uuid', + email: 'test@example.com', + role: UserRole.USER, + emailConfirmedAt: undefined, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }; + + const mockAdminUserResponse = { + id: 'admin-user-uuid', + email: 'admin@example.com', + role: UserRole.ADMIN, + emailConfirmedAt: '2024-01-01T00:00:00.000Z', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }; + + const mockService = { + findAll: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + getUsersCount: jest.fn(), + }; + + const mockJwtService = { + verifyAsync: jest.fn(), + sign: jest.fn(), + }; + + const mockReflector = { + getAllAndOverride: jest.fn().mockReturnValue(false), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: mockService, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: Reflector, + useValue: mockReflector, + }, + UsersAuthGuard, + ], + }).compile(); + + controller = module.get(UsersController); + service = module.get(UsersService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('findAll', () => { + it('should return array of users with default pagination', async () => { + const users = [mockUserResponse, mockAdminUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(10, 0); + }); + + it('should return array of users with custom limit', async () => { + const users = [mockUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(5); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(5, 0); + }); + + it('should return array of users with custom offset', async () => { + const users = [mockAdminUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(undefined, 10); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(10, 10); + }); + + it('should return array of users with custom limit and offset', async () => { + const users = [mockUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(20, 5); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(20, 5); + }); + + it('should return empty array when no users exist', async () => { + service.findAll.mockResolvedValue([]); + + const result = await controller.findAll(); + + expect(result).toEqual([]); + expect(service.findAll).toHaveBeenCalledWith(10, 0); + }); + + it('should handle large limit values', async () => { + const users = [mockUserResponse, mockAdminUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(1000); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(1000, 0); + }); + + it('should handle large offset values', async () => { + service.findAll.mockResolvedValue([]); + + const result = await controller.findAll(10, 500); + + expect(result).toEqual([]); + expect(service.findAll).toHaveBeenCalledWith(10, 500); + }); + }); + + describe('findOne', () => { + it('should return single user by id', async () => { + service.findOne.mockResolvedValue(mockUserResponse); + + const result = await controller.findOne('test-user-uuid'); + + expect(result).toEqual(mockUserResponse); + expect(service.findOne).toHaveBeenCalledWith('test-user-uuid'); + }); + + it('should return admin user by id', async () => { + service.findOne.mockResolvedValue(mockAdminUserResponse); + + const result = await controller.findOne('admin-user-uuid'); + + expect(result).toEqual(mockAdminUserResponse); + expect(service.findOne).toHaveBeenCalledWith('admin-user-uuid'); + }); + + it('should handle user with confirmed email', async () => { + const confirmedUser = { + ...mockUserResponse, + emailConfirmedAt: '2024-01-01T12:00:00.000Z', + }; + service.findOne.mockResolvedValue(confirmedUser); + + const result = await controller.findOne('test-user-uuid'); + + expect(result).toEqual(confirmedUser); + expect(result.emailConfirmedAt).toBe('2024-01-01T12:00:00.000Z'); + expect(service.findOne).toHaveBeenCalledWith('test-user-uuid'); + }); + }); + + describe('create', () => { + it('should create new user when users exist (not first user)', async () => { + const createDto: CreateUserDto = { + email: 'newuser@example.com', + password: 'SecurePassword123!', + role: UserRole.USER, + }; + + const newUser = { + ...mockUserResponse, + email: 'newuser@example.com', + }; + + service.getUsersCount.mockResolvedValue(5); + service.create.mockResolvedValue(newUser); + + const result = await controller.create(createDto); + + expect(result).toEqual(newUser); + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + + it('should create first user as admin', async () => { + const createDto: CreateUserDto = { + email: 'firstuser@example.com', + password: 'SecurePassword123!', + }; + + const firstUser = { + ...mockAdminUserResponse, + email: 'firstuser@example.com', + }; + + service.getUsersCount.mockResolvedValue(0); + service.create.mockResolvedValue(firstUser); + + const result = await controller.create(createDto); + + expect(result).toEqual(firstUser); + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, true); + }); + + it('should create user with explicitly specified role', async () => { + const createDto: CreateUserDto = { + email: 'admin@example.com', + password: 'SecurePassword123!', + role: UserRole.ADMIN, + }; + + service.getUsersCount.mockResolvedValue(1); + service.create.mockResolvedValue(mockAdminUserResponse); + + const result = await controller.create(createDto); + + expect(result).toEqual(mockAdminUserResponse); + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + + it('should create user without role (uses default)', async () => { + const createDto: CreateUserDto = { + email: 'defaultuser@example.com', + password: 'SecurePassword123!', + }; + + const newUser = { + ...mockUserResponse, + email: 'defaultuser@example.com', + role: UserRole.USER, + }; + + service.getUsersCount.mockResolvedValue(3); + service.create.mockResolvedValue(newUser); + + const result = await controller.create(createDto); + + expect(result).toEqual(newUser); + expect(result.role).toBe(UserRole.USER); + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + + it('should handle creating user with long password', async () => { + const createDto: CreateUserDto = { + email: 'longpass@example.com', + password: 'AVeryLongAndSecurePasswordThatMeetsAllRequirements123!@#', + }; + + const newUser = { + ...mockUserResponse, + email: 'longpass@example.com', + }; + + service.getUsersCount.mockResolvedValue(2); + service.create.mockResolvedValue(newUser); + + const result = await controller.create(createDto); + + expect(result).toEqual(newUser); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + }); + + describe('update', () => { + it('should update user email', async () => { + const updateDto: UpdateUserDto = { + email: 'updated@example.com', + }; + + const updatedUser = { + ...mockUserResponse, + email: 'updated@example.com', + updatedAt: '2024-01-02T00:00:00.000Z', + }; + + service.update.mockResolvedValue(updatedUser); + + const result = await controller.update('test-user-uuid', updateDto); + + expect(result).toEqual(updatedUser); + expect(result.email).toBe('updated@example.com'); + expect(service.update).toHaveBeenCalledWith('test-user-uuid', updateDto); + }); + + it('should update user password', async () => { + const updateDto: UpdateUserDto = { + password: 'NewSecurePassword456!', + }; + + const updatedUser = { + ...mockUserResponse, + updatedAt: '2024-01-02T00:00:00.000Z', + }; + + service.update.mockResolvedValue(updatedUser); + + const result = await controller.update('test-user-uuid', updateDto); + + expect(result).toEqual(updatedUser); + expect(service.update).toHaveBeenCalledWith('test-user-uuid', updateDto); + }); + + it('should update user role', async () => { + const updateDto: UpdateUserDto = { + role: UserRole.ADMIN, + }; + + const updatedUser = { + ...mockUserResponse, + role: UserRole.ADMIN, + updatedAt: '2024-01-02T00:00:00.000Z', + }; + + service.update.mockResolvedValue(updatedUser); + + const result = await controller.update('test-user-uuid', updateDto); + + expect(result).toEqual(updatedUser); + expect(result.role).toBe(UserRole.ADMIN); + expect(service.update).toHaveBeenCalledWith('test-user-uuid', updateDto); + }); + + it('should update multiple user fields', async () => { + const updateDto: UpdateUserDto = { + email: 'newemail@example.com', + password: 'NewPassword789!', + role: UserRole.ADMIN, + }; + + const updatedUser = { + ...mockUserResponse, + email: 'newemail@example.com', + role: UserRole.ADMIN, + updatedAt: '2024-01-02T00:00:00.000Z', + }; + + service.update.mockResolvedValue(updatedUser); + + const result = await controller.update('test-user-uuid', updateDto); + + expect(result).toEqual(updatedUser); + expect(result.email).toBe('newemail@example.com'); + expect(result.role).toBe(UserRole.ADMIN); + expect(service.update).toHaveBeenCalledWith('test-user-uuid', updateDto); + }); + + it('should update user with empty dto (no changes)', async () => { + const updateDto: UpdateUserDto = {}; + + service.update.mockResolvedValue(mockUserResponse); + + const result = await controller.update('test-user-uuid', updateDto); + + expect(result).toEqual(mockUserResponse); + expect(service.update).toHaveBeenCalledWith('test-user-uuid', updateDto); + }); + + it('should downgrade admin to user role', async () => { + const updateDto: UpdateUserDto = { + role: UserRole.USER, + }; + + const updatedUser = { + ...mockAdminUserResponse, + role: UserRole.USER, + updatedAt: '2024-01-02T00:00:00.000Z', + }; + + service.update.mockResolvedValue(updatedUser); + + const result = await controller.update('admin-user-uuid', updateDto); + + expect(result).toEqual(updatedUser); + expect(result.role).toBe(UserRole.USER); + expect(service.update).toHaveBeenCalledWith('admin-user-uuid', updateDto); + }); + }); + + describe('remove', () => { + it('should delete user without request context', async () => { + service.remove.mockResolvedValue(undefined); + + await controller.remove('test-user-uuid'); + + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', undefined); + }); + + it('should delete user with request context but no user', async () => { + const mockReq = {} as Request & { user?: { id?: string } }; + service.remove.mockResolvedValue(undefined); + + await controller.remove('test-user-uuid', mockReq); + + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', undefined); + }); + + it('should delete user with authenticated user context', async () => { + const mockReq = { + user: { id: 'admin-user-uuid' }, + } as Request & { user?: { id?: string } }; + service.remove.mockResolvedValue(undefined); + + await controller.remove('test-user-uuid', mockReq); + + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', 'admin-user-uuid'); + }); + + it('should prevent user from deleting themselves', async () => { + const mockReq = { + user: { id: 'test-user-uuid' }, + } as Request & { user?: { id?: string } }; + service.remove.mockResolvedValue(undefined); + + await controller.remove('test-user-uuid', mockReq); + + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', 'test-user-uuid'); + }); + + it('should handle deletion by another admin user', async () => { + const mockReq = { + user: { id: 'admin-user-uuid' }, + } as Request & { user?: { id?: string } }; + service.remove.mockResolvedValue(undefined); + + await controller.remove('other-user-uuid', mockReq); + + expect(service.remove).toHaveBeenCalledWith('other-user-uuid', 'admin-user-uuid'); + }); + + it('should handle request with user object but no id', async () => { + const mockReq = { + user: {}, + } as Request & { user?: { id?: string } }; + service.remove.mockResolvedValue(undefined); + + await controller.remove('test-user-uuid', mockReq); + + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', undefined); + }); + + it('should return undefined on successful deletion', async () => { + service.remove.mockResolvedValue(undefined); + + const result = await controller.remove('test-user-uuid'); + + expect(result).toBeUndefined(); + expect(service.remove).toHaveBeenCalledWith('test-user-uuid', undefined); + }); + }); + + describe('edge cases', () => { + it('should handle UUID v4 format for findOne', async () => { + const validUUID = '550e8400-e29b-41d4-a716-446655440000'; + service.findOne.mockResolvedValue(mockUserResponse); + + const result = await controller.findOne(validUUID); + + expect(result).toEqual(mockUserResponse); + expect(service.findOne).toHaveBeenCalledWith(validUUID); + }); + + it('should handle UUID v4 format for update', async () => { + const validUUID = '550e8400-e29b-41d4-a716-446655440000'; + const updateDto: UpdateUserDto = { + email: 'updated@example.com', + }; + service.update.mockResolvedValue(mockUserResponse); + + const result = await controller.update(validUUID, updateDto); + + expect(result).toEqual(mockUserResponse); + expect(service.update).toHaveBeenCalledWith(validUUID, updateDto); + }); + + it('should handle UUID v4 format for remove', async () => { + const validUUID = '550e8400-e29b-41d4-a716-446655440000'; + service.remove.mockResolvedValue(undefined); + + await controller.remove(validUUID); + + expect(service.remove).toHaveBeenCalledWith(validUUID, undefined); + }); + + it('should handle zero limit with default fallback', async () => { + service.findAll.mockResolvedValue([]); + + const result = await controller.findAll(undefined); + + expect(result).toEqual([]); + expect(service.findAll).toHaveBeenCalledWith(10, 0); + }); + + it('should handle zero offset', async () => { + const users = [mockUserResponse, mockAdminUserResponse]; + service.findAll.mockResolvedValue(users); + + const result = await controller.findAll(10, 0); + + expect(result).toEqual(users); + expect(service.findAll).toHaveBeenCalledWith(10, 0); + }); + }); + + describe('first user scenario', () => { + it('should correctly identify first user when count is 0', async () => { + const createDto: CreateUserDto = { + email: 'first@example.com', + password: 'FirstPassword123!', + }; + + service.getUsersCount.mockResolvedValue(0); + service.create.mockResolvedValue(mockAdminUserResponse); + + await controller.create(createDto); + + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, true); + }); + + it('should correctly identify non-first user when count is 1', async () => { + const createDto: CreateUserDto = { + email: 'second@example.com', + password: 'SecondPassword123!', + }; + + service.getUsersCount.mockResolvedValue(1); + service.create.mockResolvedValue(mockUserResponse); + + await controller.create(createDto); + + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + + it('should correctly identify non-first user when count is greater than 1', async () => { + const createDto: CreateUserDto = { + email: 'nth@example.com', + password: 'NthPassword123!', + }; + + service.getUsersCount.mockResolvedValue(100); + service.create.mockResolvedValue(mockUserResponse); + + await controller.create(createDto); + + expect(service.getUsersCount).toHaveBeenCalled(); + expect(service.create).toHaveBeenCalledWith(createDto, false); + }); + }); +}); diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.spec.ts similarity index 98% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.spec.ts index 92be4e50..62ac88e0 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.spec.ts @@ -1,12 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { UserRole } from './entities/user.entity'; +import { UserRole } from '../entities/user.entity'; +import { ClientAgentCredentialsRepository } from '../repositories/client-agent-credentials.repository'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientsService } from '../services/clients.service'; +import { SocketAuthService } from '../services/socket-auth.service'; +import { StatisticsService } from '../services/statistics.service'; import { ClientsGateway } from './clients.gateway'; -import { ClientAgentCredentialsRepository } from './repositories/client-agent-credentials.repository'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientsService } from './services/clients.service'; -import { SocketAuthService } from './services/socket-auth.service'; -import { StatisticsService } from './services/statistics.service'; jest.mock( 'socket.io-client', diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.ts similarity index 98% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.ts index 87e2f8d4..c0644cbd 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.gateway.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/gateways/clients.gateway.ts @@ -1,26 +1,26 @@ import { BadRequestException, Logger } from '@nestjs/common'; -import { FilterDropDirection } from './entities/statistics-chat-filter-drop.entity'; -import { FilterFlagDirection } from './entities/statistics-chat-filter-flag.entity'; import { ConnectedSocket, MessageBody, - OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, + OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import type { Socket as ClientSocket } from 'socket.io-client'; -import { AuthenticationType } from './entities/client.entity'; -import { buildRequestFromSocketUser, ensureClientAccess } from './utils/client-access.utils'; -import { ClientAgentCredentialsRepository } from './repositories/client-agent-credentials.repository'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientsService } from './services/clients.service'; -import { SocketAuthService } from './services/socket-auth.service'; -import { StatisticsService } from './services/statistics.service'; +import { AuthenticationType } from '../entities/client.entity'; +import { FilterDropDirection } from '../entities/statistics-chat-filter-drop.entity'; +import { FilterFlagDirection } from '../entities/statistics-chat-filter-flag.entity'; +import { ClientAgentCredentialsRepository } from '../repositories/client-agent-credentials.repository'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ClientsService } from '../services/clients.service'; +import { SocketAuthService } from '../services/socket-auth.service'; +import { StatisticsService } from '../services/statistics.service'; +import { buildRequestFromSocketUser, ensureClientAccess } from '../utils/client-access.utils'; // socket.io-client is required at runtime when forwarding; avoid static import to keep optional dependency for tests // Using type-only import for ClientSocket to avoid runtime dependency diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.spec.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.spec.ts similarity index 79% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.spec.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.spec.ts index f8e39131..e7c13d62 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.spec.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.spec.ts @@ -2,32 +2,32 @@ import { getAuthenticationMethod } from '@forepath/identity/backend'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { KEYCLOAK_CONNECT_OPTIONS, KEYCLOAK_INSTANCE } from 'nest-keycloak-connect'; -import { ClientsController } from './clients.controller'; -import { ClientEntity } from './entities/client.entity'; -import { ClientUserEntity } from './entities/client-user.entity'; -import { ProvisioningReferenceEntity } from './entities/provisioning-reference.entity'; -import { StatisticsAgentEntity } from './entities/statistics-agent.entity'; -import { StatisticsChatFilterDropEntity } from './entities/statistics-chat-filter-drop.entity'; -import { StatisticsChatFilterFlagEntity } from './entities/statistics-chat-filter-flag.entity'; -import { StatisticsChatIoEntity } from './entities/statistics-chat-io.entity'; -import { StatisticsClientEntity } from './entities/statistics-client.entity'; -import { StatisticsClientUserEntity } from './entities/statistics-client-user.entity'; -import { StatisticsEntityEventEntity } from './entities/statistics-entity-event.entity'; -import { StatisticsProvisioningReferenceEntity } from './entities/statistics-provisioning-reference.entity'; -import { StatisticsUserEntity } from './entities/statistics-user.entity'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ClientAgentFileSystemProxyService } from './services/client-agent-file-system-proxy.service'; -import { ClientAgentProxyService } from './services/client-agent-proxy.service'; -import { ClientsService } from './services/clients.service'; -import { KeycloakTokenService } from './services/keycloak-token.service'; +import { ClientsController } from '../controllers/clients.controller'; +import { ClientAgentCredentialEntity } from '../entities/client-agent-credential.entity'; +import { ClientUserEntity } from '../entities/client-user.entity'; +import { ClientEntity } from '../entities/client.entity'; +import { ProvisioningReferenceEntity } from '../entities/provisioning-reference.entity'; +import { StatisticsAgentEntity } from '../entities/statistics-agent.entity'; +import { StatisticsChatFilterDropEntity } from '../entities/statistics-chat-filter-drop.entity'; +import { StatisticsChatFilterFlagEntity } from '../entities/statistics-chat-filter-flag.entity'; +import { StatisticsChatIoEntity } from '../entities/statistics-chat-io.entity'; +import { StatisticsClientUserEntity } from '../entities/statistics-client-user.entity'; +import { StatisticsClientEntity } from '../entities/statistics-client.entity'; +import { StatisticsEntityEventEntity } from '../entities/statistics-entity-event.entity'; +import { StatisticsProvisioningReferenceEntity } from '../entities/statistics-provisioning-reference.entity'; +import { StatisticsUserEntity } from '../entities/statistics-user.entity'; +import { UserEntity } from '../entities/user.entity'; +import { ClientsGateway } from '../gateways/clients.gateway'; +import { ClientAgentCredentialsRepository } from '../repositories/client-agent-credentials.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { UsersRepository } from '../repositories/users.repository'; +import { ClientAgentCredentialsService } from '../services/client-agent-credentials.service'; +import { ClientAgentFileSystemProxyService } from '../services/client-agent-file-system-proxy.service'; +import { ClientAgentProxyService } from '../services/client-agent-proxy.service'; +import { ClientsService } from '../services/clients.service'; +import { KeycloakTokenService } from '../services/keycloak-token.service'; +import { SocketAuthService } from '../services/socket-auth.service'; import { ClientsModule } from './clients.module'; -import { ClientAgentCredentialEntity } from './entities/client-agent-credential.entity'; -import { ClientAgentCredentialsService } from './services/client-agent-credentials.service'; -import { ClientAgentCredentialsRepository } from './repositories/client-agent-credentials.repository'; -import { ClientsGateway } from './clients.gateway'; -import { SocketAuthService } from './services/socket-auth.service'; -import { UserEntity } from './entities/user.entity'; -import { UsersRepository } from './repositories/users.repository'; describe('ClientsModule', () => { let module: TestingModule; diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.ts similarity index 51% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.ts index 598dd5eb..cb5a40a7 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/clients.module.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/clients.module.ts @@ -2,38 +2,38 @@ import { getAuthenticationMethod, KeycloakService } from '@forepath/identity/bac import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; -import { ClientStatisticsController } from './client-statistics.controller'; -import { ClientsDeploymentsController } from './clients-deployments.controller'; -import { ClientsVcsController } from './clients-vcs.controller'; -import { ClientsController } from './clients.controller'; -import { StatisticsController } from './statistics.controller'; -import { ClientsGateway } from './clients.gateway'; -import { ClientAgentCredentialEntity } from './entities/client-agent-credential.entity'; -import { ClientEntity } from './entities/client.entity'; -import { ClientUserEntity } from './entities/client-user.entity'; -import { ProvisioningReferenceEntity } from './entities/provisioning-reference.entity'; -import { UserEntity } from './entities/user.entity'; -import { DigitalOceanProvider } from './providers/digital-ocean.provider'; -import { HetznerProvider } from './providers/hetzner.provider'; -import { ProvisioningProviderFactory } from './providers/provisioning-provider.factory'; +import { ClientStatisticsController } from '../controllers/client-statistics.controller'; +import { ClientsDeploymentsController } from '../controllers/clients-deployments.controller'; +import { ClientsVcsController } from '../controllers/clients-vcs.controller'; +import { ClientsController } from '../controllers/clients.controller'; +import { StatisticsController } from '../controllers/statistics.controller'; +import { ClientAgentCredentialEntity } from '../entities/client-agent-credential.entity'; +import { ClientUserEntity } from '../entities/client-user.entity'; +import { ClientEntity } from '../entities/client.entity'; +import { ProvisioningReferenceEntity } from '../entities/provisioning-reference.entity'; +import { UserEntity } from '../entities/user.entity'; +import { ClientsGateway } from '../gateways/clients.gateway'; +import { DigitalOceanProvider } from '../providers/digital-ocean.provider'; +import { HetznerProvider } from '../providers/hetzner.provider'; +import { ProvisioningProviderFactory } from '../providers/provisioning-provider.factory'; +import { ClientAgentCredentialsRepository } from '../repositories/client-agent-credentials.repository'; +import { ClientUsersRepository } from '../repositories/client-users.repository'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { ProvisioningReferencesRepository } from '../repositories/provisioning-references.repository'; +import { UsersRepository } from '../repositories/users.repository'; +import { ClientAgentCredentialsService } from '../services/client-agent-credentials.service'; +import { ClientAgentDeploymentsProxyService } from '../services/client-agent-deployments-proxy.service'; +import { ClientAgentEnvironmentVariablesProxyService } from '../services/client-agent-environment-variables-proxy.service'; +import { ClientAgentFileSystemProxyService } from '../services/client-agent-file-system-proxy.service'; +import { ClientAgentProxyService } from '../services/client-agent-proxy.service'; +import { ClientAgentVcsProxyService } from '../services/client-agent-vcs-proxy.service'; +import { ClientUsersService } from '../services/client-users.service'; +import { ClientsService } from '../services/clients.service'; +import { KeycloakTokenService } from '../services/keycloak-token.service'; +import { ProvisioningService } from '../services/provisioning.service'; +import { SocketAuthService } from '../services/socket-auth.service'; +import { StatisticsAgentSyncService } from '../services/statistics-agent-sync.service'; import { StatisticsModule } from './statistics.module'; -import { ClientAgentCredentialsRepository } from './repositories/client-agent-credentials.repository'; -import { ClientUsersRepository } from './repositories/client-users.repository'; -import { ClientsRepository } from './repositories/clients.repository'; -import { ProvisioningReferencesRepository } from './repositories/provisioning-references.repository'; -import { UsersRepository } from './repositories/users.repository'; -import { ClientAgentCredentialsService } from './services/client-agent-credentials.service'; -import { ClientAgentDeploymentsProxyService } from './services/client-agent-deployments-proxy.service'; -import { ClientAgentEnvironmentVariablesProxyService } from './services/client-agent-environment-variables-proxy.service'; -import { ClientAgentFileSystemProxyService } from './services/client-agent-file-system-proxy.service'; -import { ClientAgentProxyService } from './services/client-agent-proxy.service'; -import { ClientAgentVcsProxyService } from './services/client-agent-vcs-proxy.service'; -import { ClientUsersService } from './services/client-users.service'; -import { ClientsService } from './services/clients.service'; -import { KeycloakTokenService } from './services/keycloak-token.service'; -import { ProvisioningService } from './services/provisioning.service'; -import { SocketAuthService } from './services/socket-auth.service'; -import { StatisticsAgentSyncService } from './services/statistics-agent-sync.service'; const authMethod = getAuthenticationMethod(); diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/keycloak-user-sync.module.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/keycloak-user-sync.module.ts similarity index 64% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/keycloak-user-sync.module.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/modules/keycloak-user-sync.module.ts index 9a40a405..637ce255 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/keycloak-user-sync.module.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/keycloak-user-sync.module.ts @@ -2,14 +2,14 @@ import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UsersController } from './controllers/users.controller'; -import { UserEntity } from './entities/user.entity'; -import { UsersAuthGuard } from './guards/users-auth.guard'; -import { KeycloakRolesGuard } from './guards/keycloak-roles.guard'; -import { KeycloakAuthGuard } from './guards/keycloak-auth.guard'; -import { UsersRepository } from './repositories/users.repository'; -import { EmailService } from './services/email.service'; -import { UsersService } from './services/users.service'; +import { UsersController } from '../controllers/users.controller'; +import { UserEntity } from '../entities/user.entity'; +import { KeycloakAuthGuard } from '../guards/keycloak-auth.guard'; +import { KeycloakRolesGuard } from '../guards/keycloak-roles.guard'; +import { UsersAuthGuard } from '../guards/users-auth.guard'; +import { UsersRepository } from '../repositories/users.repository'; +import { EmailService } from '../services/email.service'; +import { UsersService } from '../services/users.service'; /** * Module that syncs Keycloak-authenticated users to the users table. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/statistics.module.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/statistics.module.ts new file mode 100644 index 00000000..dbf3e6df --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/statistics.module.ts @@ -0,0 +1,54 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientEntity } from '../entities/client.entity'; +import { StatisticsAgentEntity } from '../entities/statistics-agent.entity'; +import { StatisticsChatFilterDropEntity } from '../entities/statistics-chat-filter-drop.entity'; +import { StatisticsChatFilterFlagEntity } from '../entities/statistics-chat-filter-flag.entity'; +import { StatisticsChatIoEntity } from '../entities/statistics-chat-io.entity'; +import { StatisticsClientUserEntity } from '../entities/statistics-client-user.entity'; +import { StatisticsClientEntity } from '../entities/statistics-client.entity'; +import { StatisticsEntityEventEntity } from '../entities/statistics-entity-event.entity'; +import { StatisticsProvisioningReferenceEntity } from '../entities/statistics-provisioning-reference.entity'; +import { StatisticsUserEntity } from '../entities/statistics-user.entity'; +import { UserEntity } from '../entities/user.entity'; +import { ClientsRepository } from '../repositories/clients.repository'; +import { StatisticsRepository } from '../repositories/statistics.repository'; +import { UsersRepository } from '../repositories/users.repository'; +import { StatisticsClientSyncService } from '../services/statistics-client-sync.service'; +import { StatisticsQueryService } from '../services/statistics-query.service'; +import { StatisticsUserSyncService } from '../services/statistics-user-sync.service'; +import { StatisticsService } from '../services/statistics.service'; + +/** + * Module for persistent statistics. Provides StatisticsService for recording + * chat I/O, filter drops, and entity lifecycle events, and StatisticsQueryService + * for REST API queries. Syncs users and clients to statistics mirror tables on startup. + */ +@Module({ + imports: [ + TypeOrmModule.forFeature([ + ClientEntity, + UserEntity, + StatisticsUserEntity, + StatisticsClientEntity, + StatisticsAgentEntity, + StatisticsProvisioningReferenceEntity, + StatisticsClientUserEntity, + StatisticsChatIoEntity, + StatisticsChatFilterDropEntity, + StatisticsChatFilterFlagEntity, + StatisticsEntityEventEntity, + ]), + ], + providers: [ + StatisticsRepository, + StatisticsService, + StatisticsQueryService, + StatisticsUserSyncService, + StatisticsClientSyncService, + ClientsRepository, + UsersRepository, + ], + exports: [StatisticsService, StatisticsQueryService, StatisticsRepository], +}) +export class StatisticsModule {} diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/users-auth.module.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/users-auth.module.ts similarity index 63% rename from libs/domains/framework/backend/feature-agent-controller/src/lib/users-auth.module.ts rename to libs/domains/framework/backend/feature-agent-controller/src/lib/modules/users-auth.module.ts index 34b6e023..32de3d42 100644 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/users-auth.module.ts +++ b/libs/domains/framework/backend/feature-agent-controller/src/lib/modules/users-auth.module.ts @@ -2,17 +2,17 @@ import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AuthController } from './controllers/auth.controller'; +import { AuthController } from '../controllers/auth.controller'; +import { UsersController } from '../controllers/users.controller'; +import { UserEntity } from '../entities/user.entity'; +import { KeycloakRolesGuard } from '../guards/keycloak-roles.guard'; +import { UsersAuthGuard } from '../guards/users-auth.guard'; +import { UsersRolesGuard } from '../guards/users-roles.guard'; +import { UsersRepository } from '../repositories/users.repository'; +import { AuthService } from '../services/auth.service'; +import { EmailService } from '../services/email.service'; +import { UsersService } from '../services/users.service'; import { StatisticsModule } from './statistics.module'; -import { UsersController } from './controllers/users.controller'; -import { UserEntity } from './entities/user.entity'; -import { UsersAuthGuard } from './guards/users-auth.guard'; -import { KeycloakRolesGuard } from './guards/keycloak-roles.guard'; -import { UsersRolesGuard } from './guards/users-roles.guard'; -import { UsersRepository } from './repositories/users.repository'; -import { AuthService } from './services/auth.service'; -import { EmailService } from './services/email.service'; -import { UsersService } from './services/users.service'; /** * Module for "users" authentication method. * Provides JWT-based auth with user registration, email confirmation, password reset. diff --git a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.module.ts b/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.module.ts deleted file mode 100644 index 321285b6..00000000 --- a/libs/domains/framework/backend/feature-agent-controller/src/lib/statistics.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ClientEntity } from './entities/client.entity'; -import { StatisticsAgentEntity } from './entities/statistics-agent.entity'; -import { StatisticsChatFilterDropEntity } from './entities/statistics-chat-filter-drop.entity'; -import { StatisticsChatFilterFlagEntity } from './entities/statistics-chat-filter-flag.entity'; -import { StatisticsChatIoEntity } from './entities/statistics-chat-io.entity'; -import { StatisticsClientEntity } from './entities/statistics-client.entity'; -import { StatisticsClientUserEntity } from './entities/statistics-client-user.entity'; -import { StatisticsEntityEventEntity } from './entities/statistics-entity-event.entity'; -import { StatisticsProvisioningReferenceEntity } from './entities/statistics-provisioning-reference.entity'; -import { StatisticsUserEntity } from './entities/statistics-user.entity'; -import { UserEntity } from './entities/user.entity'; -import { ClientsRepository } from './repositories/clients.repository'; -import { StatisticsRepository } from './repositories/statistics.repository'; -import { UsersRepository } from './repositories/users.repository'; -import { StatisticsQueryService } from './services/statistics-query.service'; -import { StatisticsService } from './services/statistics.service'; -import { StatisticsClientSyncService } from './services/statistics-client-sync.service'; -import { StatisticsUserSyncService } from './services/statistics-user-sync.service'; - -/** - * Module for persistent statistics. Provides StatisticsService for recording - * chat I/O, filter drops, and entity lifecycle events, and StatisticsQueryService - * for REST API queries. Syncs users and clients to statistics mirror tables on startup. - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([ - ClientEntity, - UserEntity, - StatisticsUserEntity, - StatisticsClientEntity, - StatisticsAgentEntity, - StatisticsProvisioningReferenceEntity, - StatisticsClientUserEntity, - StatisticsChatIoEntity, - StatisticsChatFilterDropEntity, - StatisticsChatFilterFlagEntity, - StatisticsEntityEventEntity, - ]), - ], - providers: [ - StatisticsRepository, - StatisticsService, - StatisticsQueryService, - StatisticsUserSyncService, - StatisticsClientSyncService, - ClientsRepository, - UsersRepository, - ], - exports: [StatisticsService, StatisticsQueryService, StatisticsRepository], -}) -export class StatisticsModule {} diff --git a/libs/domains/framework/backend/feature-agent-manager/src/index.ts b/libs/domains/framework/backend/feature-agent-manager/src/index.ts index 14248675..62616c3a 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/index.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/index.ts @@ -1,10 +1,3 @@ -export * from './lib/agents-environment-variables.controller'; -export * from './lib/agents-files.controller'; -export * from './lib/agents-vcs.controller'; -export * from './lib/agents.controller'; -export * from './lib/agents.gateway'; -export * from './lib/agents.module'; -export * from './lib/config.controller'; export * from './lib/dto/agent-response.dto'; export * from './lib/dto/commit.dto'; export * from './lib/dto/config-response.dto'; @@ -35,6 +28,8 @@ export * from './lib/entities/agent-message.entity'; export * from './lib/entities/agent.entity'; export * from './lib/entities/deployment-configuration.entity'; export * from './lib/entities/deployment-run.entity'; +export * from './lib/gateways/agents.gateway'; +export * from './lib/modules/agents.module'; export * from './lib/repositories/agent-messages.repository'; export * from './lib/repositories/agents.repository'; export * from './lib/services/agent-messages.service'; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.spec.ts similarity index 98% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.spec.ts index cf9cf37b..d5f5ab81 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { DeploymentConfigurationResponseDto } from './dto/deployment-configuration.dto'; -import { DeploymentsService } from './services/deployments.service'; +import { DeploymentConfigurationResponseDto } from '../dto/deployment-configuration.dto'; +import { DeploymentsService } from '../services/deployments.service'; import { AgentsDeploymentsController } from './agents-deployments.controller'; describe('AgentsDeploymentsController', () => { diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.ts similarity index 97% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.ts index 5e864dc9..cb0a19b2 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-deployments.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-deployments.controller.ts @@ -15,7 +15,7 @@ import { CreateDeploymentConfigurationDto, DeploymentConfigurationResponseDto, UpdateDeploymentConfigurationDto, -} from './dto/deployment-configuration.dto'; +} from '../dto/deployment-configuration.dto'; import { BranchResponseDto, DeploymentRunResponseDto, @@ -23,8 +23,8 @@ import { RepositoryResponseDto, TriggerWorkflowDto, WorkflowResponseDto, -} from './dto/deployment-run.dto'; -import { DeploymentsService } from './services/deployments.service'; +} from '../dto/deployment-run.dto'; +import { DeploymentsService } from '../services/deployments.service'; /** * Controller for deployment and CI/CD pipeline endpoints. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.spec.ts similarity index 93% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.spec.ts index 49c80f65..b1af4bd6 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { CreateEnvironmentVariableDto } from '../dto/create-environment-variable.dto'; +import { EnvironmentVariableResponseDto } from '../dto/environment-variable-response.dto'; +import { UpdateEnvironmentVariableDto } from '../dto/update-environment-variable.dto'; +import { AgentEnvironmentVariableEntity } from '../entities/agent-environment-variable.entity'; +import { AgentEnvironmentVariablesService } from '../services/agent-environment-variables.service'; import { AgentsEnvironmentVariablesController } from './agents-environment-variables.controller'; -import { CreateEnvironmentVariableDto } from './dto/create-environment-variable.dto'; -import { EnvironmentVariableResponseDto } from './dto/environment-variable-response.dto'; -import { UpdateEnvironmentVariableDto } from './dto/update-environment-variable.dto'; -import { AgentEnvironmentVariableEntity } from './entities/agent-environment-variable.entity'; -import { AgentEnvironmentVariablesService } from './services/agent-environment-variables.service'; describe('AgentsEnvironmentVariablesController', () => { let controller: AgentsEnvironmentVariablesController; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.ts similarity index 93% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.ts index e4cc0b44..5b7c8172 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-environment-variables.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-environment-variables.controller.ts @@ -12,10 +12,10 @@ import { Put, Query, } from '@nestjs/common'; -import { CreateEnvironmentVariableDto } from './dto/create-environment-variable.dto'; -import { EnvironmentVariableResponseDto } from './dto/environment-variable-response.dto'; -import { UpdateEnvironmentVariableDto } from './dto/update-environment-variable.dto'; -import { AgentEnvironmentVariablesService } from './services/agent-environment-variables.service'; +import { CreateEnvironmentVariableDto } from '../dto/create-environment-variable.dto'; +import { EnvironmentVariableResponseDto } from '../dto/environment-variable-response.dto'; +import { UpdateEnvironmentVariableDto } from '../dto/update-environment-variable.dto'; +import { AgentEnvironmentVariablesService } from '../services/agent-environment-variables.service'; /** * Controller for agent environment variables management endpoints. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.spec.ts similarity index 95% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.spec.ts index 023c8e97..ce396a86 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { CreateFileDto } from '../dto/create-file.dto'; +import { FileContentDto } from '../dto/file-content.dto'; +import { FileNodeDto } from '../dto/file-node.dto'; +import { MoveFileDto } from '../dto/move-file.dto'; +import { WriteFileDto } from '../dto/write-file.dto'; +import { AgentFileSystemService } from '../services/agent-file-system.service'; import { AgentsFilesController } from './agents-files.controller'; -import { CreateFileDto } from './dto/create-file.dto'; -import { FileContentDto } from './dto/file-content.dto'; -import { FileNodeDto } from './dto/file-node.dto'; -import { MoveFileDto } from './dto/move-file.dto'; -import { WriteFileDto } from './dto/write-file.dto'; -import { AgentFileSystemService } from './services/agent-file-system.service'; describe('AgentsFilesController', () => { let controller: AgentsFilesController; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.ts similarity index 95% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.ts index c6925781..97014b70 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-files.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-files.controller.ts @@ -13,12 +13,12 @@ import { Put, Query, } from '@nestjs/common'; -import { CreateFileDto } from './dto/create-file.dto'; -import { FileContentDto } from './dto/file-content.dto'; -import { FileNodeDto } from './dto/file-node.dto'; -import { MoveFileDto } from './dto/move-file.dto'; -import { WriteFileDto } from './dto/write-file.dto'; -import { AgentFileSystemService } from './services/agent-file-system.service'; +import { CreateFileDto } from '../dto/create-file.dto'; +import { FileContentDto } from '../dto/file-content.dto'; +import { FileNodeDto } from '../dto/file-node.dto'; +import { MoveFileDto } from '../dto/move-file.dto'; +import { WriteFileDto } from '../dto/write-file.dto'; +import { AgentFileSystemService } from '../services/agent-file-system.service'; /** * Controller for agent file system operations. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.spec.ts similarity index 96% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.spec.ts index ec515355..0a0d9514 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { CreateBranchDto } from './dto/create-branch.dto'; -import { GitBranchDto } from './dto/git-branch.dto'; -import { GitDiffDto } from './dto/git-diff.dto'; -import { GitStatusDto } from './dto/git-status.dto'; +import { CreateBranchDto } from '../dto/create-branch.dto'; +import { GitBranchDto } from '../dto/git-branch.dto'; +import { GitDiffDto } from '../dto/git-diff.dto'; +import { GitStatusDto } from '../dto/git-status.dto'; +import { AgentsVcsService } from '../services/agents-vcs.service'; import { AgentsVcsController } from './agents-vcs.controller'; -import { AgentsVcsService } from './services/agents-vcs.service'; describe('AgentsVcsController', () => { let controller: AgentsVcsController; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.ts similarity index 90% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.ts index 1a1213bc..7bf5f2bc 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents-vcs.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents-vcs.controller.ts @@ -1,15 +1,15 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common'; -import { CommitDto } from './dto/commit.dto'; -import { CreateBranchDto } from './dto/create-branch.dto'; -import { GitBranchDto } from './dto/git-branch.dto'; -import { GitDiffDto } from './dto/git-diff.dto'; -import { GitStatusDto } from './dto/git-status.dto'; -import { PushOptionsDto } from './dto/push-options.dto'; -import { RebaseDto } from './dto/rebase.dto'; -import { ResolveConflictDto } from './dto/resolve-conflict.dto'; -import { StageFilesDto } from './dto/stage-files.dto'; -import { UnstageFilesDto } from './dto/unstage-files.dto'; -import { AgentsVcsService } from './services/agents-vcs.service'; +import { CommitDto } from '../dto/commit.dto'; +import { CreateBranchDto } from '../dto/create-branch.dto'; +import { GitBranchDto } from '../dto/git-branch.dto'; +import { GitDiffDto } from '../dto/git-diff.dto'; +import { GitStatusDto } from '../dto/git-status.dto'; +import { PushOptionsDto } from '../dto/push-options.dto'; +import { RebaseDto } from '../dto/rebase.dto'; +import { ResolveConflictDto } from '../dto/resolve-conflict.dto'; +import { StageFilesDto } from '../dto/stage-files.dto'; +import { UnstageFilesDto } from '../dto/unstage-files.dto'; +import { AgentsVcsService } from '../services/agents-vcs.service'; /** * Controller for agent VCS (Version Control System) operations. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.spec.ts similarity index 92% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.spec.ts index 70b5d502..fd044f50 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { AgentResponseDto } from '../dto/agent-response.dto'; +import { CreateAgentResponseDto } from '../dto/create-agent-response.dto'; +import { CreateAgentDto } from '../dto/create-agent.dto'; +import { UpdateAgentDto } from '../dto/update-agent.dto'; +import { ContainerType } from '../entities/agent.entity'; +import { AgentsService } from '../services/agents.service'; import { AgentsController } from './agents.controller'; -import { AgentResponseDto } from './dto/agent-response.dto'; -import { CreateAgentResponseDto } from './dto/create-agent-response.dto'; -import { CreateAgentDto } from './dto/create-agent.dto'; -import { UpdateAgentDto } from './dto/update-agent.dto'; -import { ContainerType } from './entities/agent.entity'; -import { AgentsService } from './services/agents.service'; describe('AgentsController', () => { let controller: AgentsController; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.ts similarity index 91% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.ts index 6e7a5173..95df11f2 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/agents.controller.ts @@ -11,11 +11,11 @@ import { Post, Query, } from '@nestjs/common'; -import { AgentResponseDto } from './dto/agent-response.dto'; -import { CreateAgentResponseDto } from './dto/create-agent-response.dto'; -import { CreateAgentDto } from './dto/create-agent.dto'; -import { UpdateAgentDto } from './dto/update-agent.dto'; -import { AgentsService } from './services/agents.service'; +import { AgentResponseDto } from '../dto/agent-response.dto'; +import { CreateAgentResponseDto } from '../dto/create-agent-response.dto'; +import { CreateAgentDto } from '../dto/create-agent.dto'; +import { UpdateAgentDto } from '../dto/update-agent.dto'; +import { AgentsService } from '../services/agents.service'; /** * Controller for agent management endpoints. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.spec.ts similarity index 98% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.spec.ts index 672f8e34..4c297b92 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '../services/config.service'; import { ConfigController } from './config.controller'; -import { ConfigService } from './services/config.service'; describe('ConfigController', () => { let controller: ConfigController; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.ts similarity index 83% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.ts index 05042004..cfdc052a 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/config.controller.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/controllers/config.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get } from '@nestjs/common'; -import { ConfigResponseDto } from './dto/config-response.dto'; -import { ConfigService } from './services/config.service'; +import { ConfigResponseDto } from '../dto/config-response.dto'; +import { ConfigService } from '../services/config.service'; /** * Controller for configuration endpoints. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-configuration.entity.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-configuration.entity.spec.ts new file mode 100644 index 00000000..d39765c1 --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-configuration.entity.spec.ts @@ -0,0 +1,187 @@ +import { DeploymentConfigurationEntity } from './deployment-configuration.entity'; +import { AgentEntity } from './agent.entity'; + +describe('DeploymentConfigurationEntity', () => { + it('should create an instance', () => { + const config = new DeploymentConfigurationEntity(); + expect(config).toBeDefined(); + }); + + it('should have all required properties', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.id).toBe('test-uuid'); + expect(config.agentId).toBe('agent-uuid-123'); + expect(config.providerType).toBe('github'); + expect(config.repositoryId).toBe('owner/repo'); + expect(config.providerToken).toBe('encrypted-token-value'); + expect(config.createdAt).toBeInstanceOf(Date); + expect(config.updatedAt).toBeInstanceOf(Date); + }); + + it('should support optional defaultBranch property', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.defaultBranch = 'main'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.defaultBranch).toBe('main'); + }); + + it('should support optional workflowId property', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.workflowId = 'workflow-123'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.workflowId).toBe('workflow-123'); + }); + + it('should support optional providerBaseUrl property', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerBaseUrl = 'https://github.enterprise.com'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.providerBaseUrl).toBe('https://github.enterprise.com'); + }); + + it('should allow undefined optional properties', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.defaultBranch).toBeUndefined(); + expect(config.workflowId).toBeUndefined(); + expect(config.providerBaseUrl).toBeUndefined(); + }); + + it('should support relationship to AgentEntity', () => { + const agent = new AgentEntity(); + agent.id = 'agent-uuid-123'; + agent.name = 'Test Agent'; + agent.hashedPassword = 'hashed-password'; + agent.createdAt = new Date(); + agent.updatedAt = new Date(); + + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.agent = agent; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.agent).toBe(agent); + expect(config.agent.id).toBe('agent-uuid-123'); + expect(config.agent.name).toBe('Test Agent'); + }); + + it('should support GitHub provider type', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.providerType).toBe('github'); + }); + + it('should support GitLab provider type', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'gitlab'; + config.repositoryId = 'group/project'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.providerType).toBe('gitlab'); + }); + + it('should store full configuration with all optional fields', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.defaultBranch = 'main'; + config.workflowId = 'workflow-123'; + config.providerToken = 'encrypted-token-value'; + config.providerBaseUrl = 'https://github.enterprise.com'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.id).toBe('test-uuid'); + expect(config.agentId).toBe('agent-uuid-123'); + expect(config.providerType).toBe('github'); + expect(config.repositoryId).toBe('owner/repo'); + expect(config.defaultBranch).toBe('main'); + expect(config.workflowId).toBe('workflow-123'); + expect(config.providerToken).toBe('encrypted-token-value'); + expect(config.providerBaseUrl).toBe('https://github.enterprise.com'); + expect(config.createdAt).toBeInstanceOf(Date); + expect(config.updatedAt).toBeInstanceOf(Date); + }); + + it('should handle long repository ID', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'organization-with-long-name/repository-with-very-long-name-that-could-exist'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.repositoryId).toBe('organization-with-long-name/repository-with-very-long-name-that-could-exist'); + }); + + it('should handle long provider token', () => { + const longToken = 'ghp_' + 'A'.repeat(2000); + const config = new DeploymentConfigurationEntity(); + config.id = 'test-uuid'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = longToken; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + expect(config.providerToken).toBe(longToken); + expect(config.providerToken.length).toBe(2004); + }); +}); diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-run.entity.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-run.entity.spec.ts new file mode 100644 index 00000000..56c32900 --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/deployment-run.entity.spec.ts @@ -0,0 +1,329 @@ +import { DeploymentRunEntity } from './deployment-run.entity'; +import { DeploymentConfigurationEntity } from './deployment-configuration.entity'; + +describe('DeploymentRunEntity', () => { + it('should create an instance', () => { + const run = new DeploymentRunEntity(); + expect(run).toBeDefined(); + }); + + it('should have all required properties', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.id).toBe('test-uuid'); + expect(run.configurationId).toBe('config-uuid-123'); + expect(run.providerRunId).toBe('run-12345'); + expect(run.runName).toBe('CI Build'); + expect(run.status).toBe('in_progress'); + expect(run.ref).toBe('refs/heads/main'); + expect(run.sha).toBe('abc123def456'); + expect(run.createdAt).toBeInstanceOf(Date); + expect(run.updatedAt).toBeInstanceOf(Date); + }); + + it('should support optional conclusion property', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'completed'; + run.conclusion = 'success'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.conclusion).toBe('success'); + }); + + it('should support optional workflowId property', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.workflowId = 'workflow-123'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.workflowId).toBe('workflow-123'); + }); + + it('should support optional workflowName property', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.workflowName = 'Continuous Integration'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.workflowName).toBe('Continuous Integration'); + }); + + it('should support optional startedAt property', () => { + const startTime = new Date('2024-01-01T10:00:00Z'); + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.startedAt = startTime; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.startedAt).toBe(startTime); + expect(run.startedAt).toBeInstanceOf(Date); + }); + + it('should support optional completedAt property', () => { + const completedTime = new Date('2024-01-01T10:15:00Z'); + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'completed'; + run.conclusion = 'success'; + run.completedAt = completedTime; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.completedAt).toBe(completedTime); + expect(run.completedAt).toBeInstanceOf(Date); + }); + + it('should support optional htmlUrl property', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.htmlUrl = 'https://github.com/owner/repo/actions/runs/12345'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.htmlUrl).toBe('https://github.com/owner/repo/actions/runs/12345'); + }); + + it('should allow undefined optional properties', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'queued'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.conclusion).toBeUndefined(); + expect(run.workflowId).toBeUndefined(); + expect(run.workflowName).toBeUndefined(); + expect(run.startedAt).toBeUndefined(); + expect(run.completedAt).toBeUndefined(); + expect(run.htmlUrl).toBeUndefined(); + }); + + it('should support relationship to DeploymentConfigurationEntity', () => { + const config = new DeploymentConfigurationEntity(); + config.id = 'config-uuid-123'; + config.agentId = 'agent-uuid-123'; + config.providerType = 'github'; + config.repositoryId = 'owner/repo'; + config.providerToken = 'encrypted-token-value'; + config.createdAt = new Date(); + config.updatedAt = new Date(); + + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.configuration = config; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.configuration).toBe(config); + expect(run.configuration.id).toBe('config-uuid-123'); + expect(run.configuration.providerType).toBe('github'); + }); + + it('should support queued status', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'queued'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.status).toBe('queued'); + }); + + it('should support in_progress status', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.status).toBe('in_progress'); + }); + + it('should support completed status with success conclusion', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'completed'; + run.conclusion = 'success'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.status).toBe('completed'); + expect(run.conclusion).toBe('success'); + }); + + it('should support completed status with failure conclusion', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'completed'; + run.conclusion = 'failure'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.status).toBe('completed'); + expect(run.conclusion).toBe('failure'); + }); + + it('should store full run with all optional fields', () => { + const startTime = new Date('2024-01-01T10:00:00Z'); + const completedTime = new Date('2024-01-01T10:15:00Z'); + + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'completed'; + run.conclusion = 'success'; + run.ref = 'refs/heads/main'; + run.sha = 'abc123def456'; + run.workflowId = 'workflow-123'; + run.workflowName = 'Continuous Integration'; + run.startedAt = startTime; + run.completedAt = completedTime; + run.htmlUrl = 'https://github.com/owner/repo/actions/runs/12345'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.id).toBe('test-uuid'); + expect(run.configurationId).toBe('config-uuid-123'); + expect(run.providerRunId).toBe('run-12345'); + expect(run.runName).toBe('CI Build'); + expect(run.status).toBe('completed'); + expect(run.conclusion).toBe('success'); + expect(run.ref).toBe('refs/heads/main'); + expect(run.sha).toBe('abc123def456'); + expect(run.workflowId).toBe('workflow-123'); + expect(run.workflowName).toBe('Continuous Integration'); + expect(run.startedAt).toBe(startTime); + expect(run.completedAt).toBe(completedTime); + expect(run.htmlUrl).toBe('https://github.com/owner/repo/actions/runs/12345'); + expect(run.createdAt).toBeInstanceOf(Date); + expect(run.updatedAt).toBeInstanceOf(Date); + }); + + it('should handle SHA with full 40 characters', () => { + const fullSha = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.ref = 'refs/heads/main'; + run.sha = fullSha; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.sha).toBe(fullSha); + expect(run.sha.length).toBe(40); + }); + + it('should handle branch ref', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'CI Build'; + run.status = 'in_progress'; + run.ref = 'refs/heads/feature/new-feature'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.ref).toBe('refs/heads/feature/new-feature'); + }); + + it('should handle tag ref', () => { + const run = new DeploymentRunEntity(); + run.id = 'test-uuid'; + run.configurationId = 'config-uuid-123'; + run.providerRunId = 'run-12345'; + run.runName = 'Release Build'; + run.status = 'in_progress'; + run.ref = 'refs/tags/v1.0.0'; + run.sha = 'abc123def456'; + run.createdAt = new Date(); + run.updatedAt = new Date(); + + expect(run.ref).toBe('refs/tags/v1.0.0'); + }); +}); diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/user.entity.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/user.entity.spec.ts new file mode 100644 index 00000000..a405f1c7 --- /dev/null +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/entities/user.entity.spec.ts @@ -0,0 +1,158 @@ +import { UserRole } from './user.entity'; + +describe('UserRole', () => { + it('should define CONTROLLER role', () => { + expect(UserRole.CONTROLLER).toBe('controller'); + }); + + it('should define ADMIN role', () => { + expect(UserRole.ADMIN).toBe('admin'); + }); + + it('should define USER role', () => { + expect(UserRole.USER).toBe('user'); + }); + + it('should have exactly three roles defined', () => { + const roles = Object.values(UserRole); + expect(roles).toHaveLength(3); + }); + + it('should have all roles as strings', () => { + const roles = Object.values(UserRole); + roles.forEach((role) => { + expect(typeof role).toBe('string'); + }); + }); + + it('should allow assignment to variables', () => { + const controllerRole: UserRole = UserRole.CONTROLLER; + const adminRole: UserRole = UserRole.ADMIN; + const userRole: UserRole = UserRole.USER; + + expect(controllerRole).toBe('controller'); + expect(adminRole).toBe('admin'); + expect(userRole).toBe('user'); + }); + + it('should have distinct values for each role', () => { + const roles = [UserRole.CONTROLLER, UserRole.ADMIN, UserRole.USER]; + const uniqueRoles = new Set(roles); + expect(uniqueRoles.size).toBe(3); + }); + + it('should support string comparison', () => { + expect(UserRole.CONTROLLER === 'controller').toBe(true); + expect(UserRole.ADMIN === 'admin').toBe(true); + expect(UserRole.USER === 'user').toBe(true); + }); + + it('should be usable in switch statements', () => { + const testRoleSwitch = (role: UserRole): string => { + switch (role) { + case UserRole.CONTROLLER: + return 'controller-access'; + case UserRole.ADMIN: + return 'admin-access'; + case UserRole.USER: + return 'user-access'; + } + }; + + expect(testRoleSwitch(UserRole.ADMIN)).toBe('admin-access'); + expect(testRoleSwitch(UserRole.CONTROLLER)).toBe('controller-access'); + expect(testRoleSwitch(UserRole.USER)).toBe('user-access'); + }); + + it('should be usable in conditional statements', () => { + const role = UserRole.CONTROLLER; + + if (role === UserRole.CONTROLLER) { + expect(true).toBe(true); + } else { + fail('Should match CONTROLLER role'); + } + }); + + it('should support Object.keys to get role names', () => { + const roleNames = Object.keys(UserRole); + expect(roleNames).toContain('CONTROLLER'); + expect(roleNames).toContain('ADMIN'); + expect(roleNames).toContain('USER'); + }); + + it('should support Object.values to get role values', () => { + const roleValues = Object.values(UserRole); + expect(roleValues).toContain('controller'); + expect(roleValues).toContain('admin'); + expect(roleValues).toContain('user'); + }); + + it('should maintain role hierarchy conceptually', () => { + // Test that we can determine role hierarchy (controller > admin > user) + const roleHierarchy = { + [UserRole.CONTROLLER]: 3, + [UserRole.ADMIN]: 2, + [UserRole.USER]: 1, + }; + + expect(roleHierarchy[UserRole.CONTROLLER]).toBeGreaterThan(roleHierarchy[UserRole.ADMIN]); + expect(roleHierarchy[UserRole.ADMIN]).toBeGreaterThan(roleHierarchy[UserRole.USER]); + }); + + it('should be usable in arrays for role checking', () => { + const adminRoles = [UserRole.CONTROLLER, UserRole.ADMIN]; + + expect(adminRoles).toContain(UserRole.CONTROLLER); + expect(adminRoles).toContain(UserRole.ADMIN); + expect(adminRoles).not.toContain(UserRole.USER); + }); + + it('should support role validation', () => { + const isValidRole = (role: string): role is UserRole => { + return Object.values(UserRole).includes(role as UserRole); + }; + + expect(isValidRole('controller')).toBe(true); + expect(isValidRole('admin')).toBe(true); + expect(isValidRole('user')).toBe(true); + expect(isValidRole('invalid')).toBe(false); + expect(isValidRole('superuser')).toBe(false); + }); + + it('should support role-based permission checking', () => { + const hasAdminAccess = (role: UserRole): boolean => { + return role === UserRole.CONTROLLER || role === UserRole.ADMIN; + }; + + expect(hasAdminAccess(UserRole.CONTROLLER)).toBe(true); + expect(hasAdminAccess(UserRole.ADMIN)).toBe(true); + expect(hasAdminAccess(UserRole.USER)).toBe(false); + }); + + it('should work with array filtering', () => { + const users = [ + { name: 'Alice', role: UserRole.CONTROLLER }, + { name: 'Bob', role: UserRole.ADMIN }, + { name: 'Charlie', role: UserRole.USER }, + ]; + + const admins = users.filter((user) => user.role === UserRole.CONTROLLER || user.role === UserRole.ADMIN); + + expect(admins).toHaveLength(2); + expect(admins[0].name).toBe('Alice'); + expect(admins[1].name).toBe('Bob'); + }); + + it('should support mapping role to display names', () => { + const roleDisplayNames: Record = { + [UserRole.CONTROLLER]: 'System Controller', + [UserRole.ADMIN]: 'Administrator', + [UserRole.USER]: 'Standard User', + }; + + expect(roleDisplayNames[UserRole.CONTROLLER]).toBe('System Controller'); + expect(roleDisplayNames[UserRole.ADMIN]).toBe('Administrator'); + expect(roleDisplayNames[UserRole.USER]).toBe('Standard User'); + }); +}); diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.spec.ts similarity index 99% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.spec.ts index bd4c2e85..2f88caf1 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Server, Socket } from 'socket.io'; +import { AgentEntity, ContainerType } from '../entities/agent.entity'; +import { AgentProviderFactory } from '../providers/agent-provider.factory'; +import { AgentProvider } from '../providers/agent-provider.interface'; +import { ChatFilterFactory } from '../providers/chat-filter.factory'; +import { ChatFilter, FilterDirection } from '../providers/chat-filter.interface'; +import { AgentsRepository } from '../repositories/agents.repository'; +import { AgentMessagesService } from '../services/agent-messages.service'; +import { AgentsService } from '../services/agents.service'; +import { DockerService } from '../services/docker.service'; import { AgentsGateway } from './agents.gateway'; -import { AgentEntity, ContainerType } from './entities/agent.entity'; -import { AgentProviderFactory } from './providers/agent-provider.factory'; -import { AgentProvider } from './providers/agent-provider.interface'; -import { ChatFilterFactory } from './providers/chat-filter.factory'; -import { ChatFilter, FilterDirection } from './providers/chat-filter.interface'; -import { AgentsRepository } from './repositories/agents.repository'; -import { AgentMessagesService } from './services/agent-messages.service'; -import { AgentsService } from './services/agents.service'; -import { DockerService } from './services/docker.service'; interface ChatPayload { message: string; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.ts similarity index 98% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.ts index 0f85d9dd..1447febd 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.gateway.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/gateways/agents.gateway.ts @@ -9,19 +9,19 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { AgentProviderFactory } from './providers/agent-provider.factory'; -import { AgentResponseObject } from './providers/agent-provider.interface'; -import { ChatFilterFactory } from './providers/chat-filter.factory'; +import { AgentProviderFactory } from '../providers/agent-provider.factory'; +import { AgentResponseObject } from '../providers/agent-provider.interface'; +import { ChatFilterFactory } from '../providers/chat-filter.factory'; import { AppliedFilterInfo, FilterApplicationResult, FilterContext, FilterDirection, -} from './providers/chat-filter.interface'; -import { AgentsRepository } from './repositories/agents.repository'; -import { AgentMessagesService } from './services/agent-messages.service'; -import { AgentsService } from './services/agents.service'; -import { DockerService } from './services/docker.service'; +} from '../providers/chat-filter.interface'; +import { AgentsRepository } from '../repositories/agents.repository'; +import { AgentMessagesService } from '../services/agent-messages.service'; +import { AgentsService } from '../services/agents.service'; +import { DockerService } from '../services/docker.service'; interface LoginPayload { agentId: string; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.spec.ts similarity index 82% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.spec.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.spec.ts index 45008ed1..d596aca4 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.spec.ts @@ -1,37 +1,37 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { AgentsDeploymentsController } from './agents-deployments.controller'; -import { AgentsController } from './agents.controller'; -import { AgentsGateway } from './agents.gateway'; +import { AgentsDeploymentsController } from '../controllers/agents-deployments.controller'; +import { AgentsController } from '../controllers/agents.controller'; +import { AgentEnvironmentVariableEntity } from '../entities/agent-environment-variable.entity'; +import { AgentMessageEntity } from '../entities/agent-message.entity'; +import { AgentEntity } from '../entities/agent.entity'; +import { DeploymentConfigurationEntity } from '../entities/deployment-configuration.entity'; +import { DeploymentRunEntity } from '../entities/deployment-run.entity'; +import { AgentsGateway } from '../gateways/agents.gateway'; +import { AgentProviderFactory } from '../providers/agent-provider.factory'; +import { CursorAgentProvider } from '../providers/agents/cursor-agent.provider'; +import { OpenClawAgentProvider } from '../providers/agents/openclaw-agent.provider'; +import { OpenCodeAgentProvider } from '../providers/agents/opencode-agent.provider'; +import { ChatFilterFactory } from '../providers/chat-filter.factory'; +import { BidirectionalChatFilter } from '../providers/filters/bidirectional-chat-filter'; +import { IncomingChatFilter } from '../providers/filters/incoming-chat-filter'; +import { NoopChatFilter } from '../providers/filters/noop-chat-filter'; +import { OutgoingChatFilter } from '../providers/filters/outgoing-chat-filter'; +import { PipelineProviderFactory } from '../providers/pipeline-provider.factory'; +import { GitHubProvider } from '../providers/pipelines/github.provider'; +import { GitLabProvider } from '../providers/pipelines/gitlab.provider'; +import { AgentEnvironmentVariablesRepository } from '../repositories/agent-environment-variables.repository'; +import { AgentMessagesRepository } from '../repositories/agent-messages.repository'; +import { AgentsRepository } from '../repositories/agents.repository'; +import { DeploymentConfigurationsRepository } from '../repositories/deployment-configurations.repository'; +import { DeploymentRunsRepository } from '../repositories/deployment-runs.repository'; +import { AgentEnvironmentVariablesService } from '../services/agent-environment-variables.service'; +import { AgentMessagesService } from '../services/agent-messages.service'; +import { AgentsService } from '../services/agents.service'; +import { DeploymentsService } from '../services/deployments.service'; +import { DockerService } from '../services/docker.service'; +import { PasswordService } from '../services/password.service'; import { AgentsModule } from './agents.module'; -import { AgentEnvironmentVariableEntity } from './entities/agent-environment-variable.entity'; -import { AgentMessageEntity } from './entities/agent-message.entity'; -import { AgentEntity } from './entities/agent.entity'; -import { DeploymentConfigurationEntity } from './entities/deployment-configuration.entity'; -import { DeploymentRunEntity } from './entities/deployment-run.entity'; -import { AgentProviderFactory } from './providers/agent-provider.factory'; -import { CursorAgentProvider } from './providers/agents/cursor-agent.provider'; -import { OpenClawAgentProvider } from './providers/agents/openclaw-agent.provider'; -import { OpenCodeAgentProvider } from './providers/agents/opencode-agent.provider'; -import { ChatFilterFactory } from './providers/chat-filter.factory'; -import { BidirectionalChatFilter } from './providers/filters/bidirectional-chat-filter'; -import { IncomingChatFilter } from './providers/filters/incoming-chat-filter'; -import { NoopChatFilter } from './providers/filters/noop-chat-filter'; -import { OutgoingChatFilter } from './providers/filters/outgoing-chat-filter'; -import { PipelineProviderFactory } from './providers/pipeline-provider.factory'; -import { GitHubProvider } from './providers/pipelines/github.provider'; -import { GitLabProvider } from './providers/pipelines/gitlab.provider'; -import { AgentEnvironmentVariablesRepository } from './repositories/agent-environment-variables.repository'; -import { AgentMessagesRepository } from './repositories/agent-messages.repository'; -import { AgentsRepository } from './repositories/agents.repository'; -import { DeploymentConfigurationsRepository } from './repositories/deployment-configurations.repository'; -import { DeploymentRunsRepository } from './repositories/deployment-runs.repository'; -import { AgentEnvironmentVariablesService } from './services/agent-environment-variables.service'; -import { AgentMessagesService } from './services/agent-messages.service'; -import { AgentsService } from './services/agents.service'; -import { DeploymentsService } from './services/deployments.service'; -import { DockerService } from './services/docker.service'; -import { PasswordService } from './services/password.service'; describe('AgentsModule', () => { let module: TestingModule; diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.ts similarity index 52% rename from libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.ts rename to libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.ts index 947110d7..046c25a5 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/agents.module.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/modules/agents.module.ts @@ -1,43 +1,43 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AgentsDeploymentsController } from './agents-deployments.controller'; -import { AgentsEnvironmentVariablesController } from './agents-environment-variables.controller'; -import { AgentsFilesController } from './agents-files.controller'; -import { AgentsVcsController } from './agents-vcs.controller'; -import { AgentsController } from './agents.controller'; -import { AgentsGateway } from './agents.gateway'; -import { ConfigController } from './config.controller'; -import { AgentEnvironmentVariableEntity } from './entities/agent-environment-variable.entity'; -import { AgentMessageEntity } from './entities/agent-message.entity'; -import { AgentEntity } from './entities/agent.entity'; -import { DeploymentConfigurationEntity } from './entities/deployment-configuration.entity'; -import { DeploymentRunEntity } from './entities/deployment-run.entity'; -import { AgentProviderFactory } from './providers/agent-provider.factory'; -import { CursorAgentProvider } from './providers/agents/cursor-agent.provider'; -import { OpenClawAgentProvider } from './providers/agents/openclaw-agent.provider'; -import { OpenCodeAgentProvider } from './providers/agents/opencode-agent.provider'; -import { ChatFilterFactory } from './providers/chat-filter.factory'; -import { BidirectionalChatFilter } from './providers/filters/bidirectional-chat-filter'; -import { IncomingChatFilter } from './providers/filters/incoming-chat-filter'; -import { NoopChatFilter } from './providers/filters/noop-chat-filter'; -import { OutgoingChatFilter } from './providers/filters/outgoing-chat-filter'; -import { PipelineProviderFactory } from './providers/pipeline-provider.factory'; -import { GitHubProvider } from './providers/pipelines/github.provider'; -import { GitLabProvider } from './providers/pipelines/gitlab.provider'; -import { AgentEnvironmentVariablesRepository } from './repositories/agent-environment-variables.repository'; -import { AgentMessagesRepository } from './repositories/agent-messages.repository'; -import { AgentsRepository } from './repositories/agents.repository'; -import { DeploymentConfigurationsRepository } from './repositories/deployment-configurations.repository'; -import { DeploymentRunsRepository } from './repositories/deployment-runs.repository'; -import { AgentEnvironmentVariablesService } from './services/agent-environment-variables.service'; -import { AgentFileSystemService } from './services/agent-file-system.service'; -import { AgentMessagesService } from './services/agent-messages.service'; -import { AgentsVcsService } from './services/agents-vcs.service'; -import { AgentsService } from './services/agents.service'; -import { ConfigService } from './services/config.service'; -import { DeploymentsService } from './services/deployments.service'; -import { DockerService } from './services/docker.service'; -import { PasswordService } from './services/password.service'; +import { AgentsDeploymentsController } from '../controllers/agents-deployments.controller'; +import { AgentsEnvironmentVariablesController } from '../controllers/agents-environment-variables.controller'; +import { AgentsFilesController } from '../controllers/agents-files.controller'; +import { AgentsVcsController } from '../controllers/agents-vcs.controller'; +import { AgentsController } from '../controllers/agents.controller'; +import { ConfigController } from '../controllers/config.controller'; +import { AgentEnvironmentVariableEntity } from '../entities/agent-environment-variable.entity'; +import { AgentMessageEntity } from '../entities/agent-message.entity'; +import { AgentEntity } from '../entities/agent.entity'; +import { DeploymentConfigurationEntity } from '../entities/deployment-configuration.entity'; +import { DeploymentRunEntity } from '../entities/deployment-run.entity'; +import { AgentsGateway } from '../gateways/agents.gateway'; +import { AgentProviderFactory } from '../providers/agent-provider.factory'; +import { CursorAgentProvider } from '../providers/agents/cursor-agent.provider'; +import { OpenClawAgentProvider } from '../providers/agents/openclaw-agent.provider'; +import { OpenCodeAgentProvider } from '../providers/agents/opencode-agent.provider'; +import { ChatFilterFactory } from '../providers/chat-filter.factory'; +import { BidirectionalChatFilter } from '../providers/filters/bidirectional-chat-filter'; +import { IncomingChatFilter } from '../providers/filters/incoming-chat-filter'; +import { NoopChatFilter } from '../providers/filters/noop-chat-filter'; +import { OutgoingChatFilter } from '../providers/filters/outgoing-chat-filter'; +import { PipelineProviderFactory } from '../providers/pipeline-provider.factory'; +import { GitHubProvider } from '../providers/pipelines/github.provider'; +import { GitLabProvider } from '../providers/pipelines/gitlab.provider'; +import { AgentEnvironmentVariablesRepository } from '../repositories/agent-environment-variables.repository'; +import { AgentMessagesRepository } from '../repositories/agent-messages.repository'; +import { AgentsRepository } from '../repositories/agents.repository'; +import { DeploymentConfigurationsRepository } from '../repositories/deployment-configurations.repository'; +import { DeploymentRunsRepository } from '../repositories/deployment-runs.repository'; +import { AgentEnvironmentVariablesService } from '../services/agent-environment-variables.service'; +import { AgentFileSystemService } from '../services/agent-file-system.service'; +import { AgentMessagesService } from '../services/agent-messages.service'; +import { AgentsVcsService } from '../services/agents-vcs.service'; +import { AgentsService } from '../services/agents.service'; +import { ConfigService } from '../services/config.service'; +import { DeploymentsService } from '../services/deployments.service'; +import { DockerService } from '../services/docker.service'; +import { PasswordService } from '../services/password.service'; /** * Module for agent management feature. diff --git a/libs/domains/framework/backend/feature-agent-manager/src/lib/services/config.service.spec.ts b/libs/domains/framework/backend/feature-agent-manager/src/lib/services/config.service.spec.ts index 7094b31b..c2534f8e 100644 --- a/libs/domains/framework/backend/feature-agent-manager/src/lib/services/config.service.spec.ts +++ b/libs/domains/framework/backend/feature-agent-manager/src/lib/services/config.service.spec.ts @@ -63,7 +63,7 @@ describe('ConfigService', () => { describe('getAvailableAgentTypes', () => { it('should return array of agent types with display names', () => { - const agentTypes = ['cursor', 'opencode', 'openclaw']; + const agentTypes = ['cursor']; agentProviderFactory.getRegisteredTypes.mockReturnValue(agentTypes); const result = service.getAvailableAgentTypes();