Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('AuthController', () => {
refresh: jest.fn(),
logout: jest.fn(),
verifyEmail: jest.fn(),
resendEmailVerification: jest.fn(),
verifyPhone: jest.fn(),
resendPhoneVerification: jest.fn(),
forgotPassword: jest.fn(),
};

Expand Down Expand Up @@ -43,6 +46,7 @@ describe('AuthController', () => {
firstName: 'Test',
lastName: 'User',
password: 'Password123!',
phone: '+15551234567',
};

const expectedUser = {
Expand Down Expand Up @@ -173,12 +177,76 @@ describe('AuthController', () => {
token: 'verification-token',
};

mockAuthService.verifyEmail.mockResolvedValue(undefined);
mockAuthService.verifyEmail.mockResolvedValue({
message: 'Email verified successfully',
emailVerified: true,
phoneVerified: false,
isVerified: false,
email: 'test@example.com',
});

const result = await controller.verifyEmail(verifyEmailDto);

expect(authService.verifyEmail).toHaveBeenCalledWith(verifyEmailDto);
expect(result).toEqual({ message: 'Email verified successfully' });
expect(result).toEqual({
message: 'Email verified successfully',
emailVerified: true,
phoneVerified: false,
isVerified: false,
email: 'test@example.com',
});
});
});

describe('verifyPhone', () => {
it('should verify phone successfully', async () => {
const verifyPhoneDto = {
email: 'test@example.com',
code: '123456',
};

mockAuthService.verifyPhone.mockResolvedValue({
message: 'Phone number verified successfully',
emailVerified: true,
phoneVerified: true,
isVerified: true,
});

const result = await controller.verifyPhone(verifyPhoneDto);

expect(authService.verifyPhone).toHaveBeenCalledWith(verifyPhoneDto);
expect(result).toEqual({
message: 'Phone number verified successfully',
emailVerified: true,
phoneVerified: true,
isVerified: true,
});
});
});

describe('resend verification', () => {
it('should resend email verification successfully', async () => {
const dto = { email: 'test@example.com' };
mockAuthService.resendEmailVerification.mockResolvedValue(undefined);

const result = await controller.resendEmailVerification(dto);

expect(authService.resendEmailVerification).toHaveBeenCalledWith(dto);
expect(result).toEqual({
message: 'If the account exists, a new verification email was sent',
});
});

it('should resend phone verification successfully', async () => {
const dto = { email: 'test@example.com' };
mockAuthService.resendPhoneVerification.mockResolvedValue(undefined);

const result = await controller.resendPhoneVerification(dto);

expect(authService.resendPhoneVerification).toHaveBeenCalledWith(dto);
expect(result).toEqual({
message: 'If the account exists, a new verification code was sent',
});
});
});

Expand Down
29 changes: 27 additions & 2 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
RefreshDto,
LogoutDto,
VerifyEmailDto,
ResendVerificationDto,
VerifyPhoneDto,
ForgotPasswordDto,
ResetPasswordDto,
} from './dto/auth.dto';
Expand Down Expand Up @@ -58,8 +60,31 @@ export class AuthController {
@Post('verify-email')
@HttpCode(HttpStatus.OK)
async verifyEmail(@Body() verifyEmailDto: VerifyEmailDto) {
await this.authService.verifyEmail(verifyEmailDto);
return { message: 'Email verified successfully' };
return this.authService.verifyEmail(verifyEmailDto);
}

@Post('resend-email-verification')
@HttpCode(HttpStatus.OK)
async resendEmailVerification(
@Body() resendVerificationDto: ResendVerificationDto,
) {
await this.authService.resendEmailVerification(resendVerificationDto);
return { message: 'If the account exists, a new verification email was sent' };
}

@Post('verify-phone')
@HttpCode(HttpStatus.OK)
async verifyPhone(@Body() verifyPhoneDto: VerifyPhoneDto) {
return this.authService.verifyPhone(verifyPhoneDto);
}

@Post('resend-phone-verification')
@HttpCode(HttpStatus.OK)
async resendPhoneVerification(
@Body() resendVerificationDto: ResendVerificationDto,
) {
await this.authService.resendPhoneVerification(resendVerificationDto);
return { message: 'If the account exists, a new verification code was sent' };
}

@Post('forgot-password')
Expand Down
8 changes: 6 additions & 2 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { UserRole } from './entities/user-role.entity';
import { RolePermission } from './entities/role-permission.entity';
import { RoleAuditLog } from './entities/role-audit-log.entity';
import { FailedLoginAttempt } from './entities/failed-login-attempt.entity';
import { EmailServiceImpl } from './services/email.service';
import { EMAIL_SERVICE } from './interfaces/email-service.interface';
import { RolesService } from './services/roles.service';
import { RolesController } from './controllers/roles.controller';
import { PermissionsService } from './services/permissions.service';
import { RolesPermissionsSeeder } from './seeds/roles-permissions.seed';
import { SmsModule } from '../modules/sms/sms.module';
import { EmailModule } from '../modules/email/email.module';
import { EmailService as AppEmailService } from '../modules/email/email.service';

@Module({
imports: [
Expand Down Expand Up @@ -77,6 +79,8 @@ import { RolesPermissionsSeeder } from './seeds/roles-permissions.seed';
},
}),
forwardRef(() => UsersModule),
SmsModule,
EmailModule,
],
controllers: [AuthController, RolesController],
providers: [
Expand All @@ -89,7 +93,7 @@ import { RolesPermissionsSeeder } from './seeds/roles-permissions.seed';
RolesPermissionsSeeder,
{
provide: EMAIL_SERVICE,
useClass: EmailServiceImpl,
useExisting: AppEmailService,
},
],
exports: [
Expand Down
72 changes: 67 additions & 5 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import { UsersService } from '../modules/users/users.service';
import { User } from '../modules/users/entities/user.entity';
import { RefreshToken } from './entities/refresh-token.entity';
import { Session } from './entities/session.entity';
import { EmailService } from './interfaces/email-service.interface';
import { EmailServiceImpl } from './services/email.service';
import {
EMAIL_SERVICE,
type EmailService,
} from './interfaces/email-service.interface';
import { PasswordUtil } from './utils/password.util';
import { DeviceFingerprintUtil } from './utils/device-fingerprint.util';
import { TokenUtil } from './utils/token.util';
import { SmsService } from '../modules/sms/sms.service';

describe('AuthService', () => {
let service: AuthService;
Expand All @@ -41,6 +44,7 @@ describe('AuthService', () => {
save: jest.fn(),
findOne: jest.fn(),
remove: jest.fn(),
delete: jest.fn(),
};

const mockSessionRepository = {
Expand Down Expand Up @@ -69,6 +73,10 @@ describe('AuthService', () => {
sendPasswordResetEmail: jest.fn(),
};

const mockSmsService = {
sendSms: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
Expand Down Expand Up @@ -98,9 +106,13 @@ describe('AuthService', () => {
useValue: mockConfigService,
},
{
provide: EmailService,
provide: EMAIL_SERVICE,
useValue: mockEmailService,
},
{
provide: SmsService,
useValue: mockSmsService,
},
],
}).compile();

Expand All @@ -115,7 +127,7 @@ describe('AuthService', () => {
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
configService = module.get<ConfigService>(ConfigService);
emailService = module.get<EmailService>(EmailService);
emailService = module.get<EmailService>(EMAIL_SERVICE);

// Reset all mocks
jest.clearAllMocks();
Expand All @@ -127,6 +139,7 @@ describe('AuthService', () => {
firstName: 'Test',
lastName: 'User',
password: 'Password123!',
phone: '+15551234567',
};

it('should register a new user successfully', async () => {
Expand All @@ -142,6 +155,7 @@ describe('AuthService', () => {
.mockReturnValue('hashed-verification-token');
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'auth.emailVerificationExpiration') return '24h';
if (key === 'auth.phoneVerificationExpiration') return '24h';
return null;
});

Expand All @@ -152,12 +166,16 @@ describe('AuthService', () => {
emailVerified: false,
emailVerificationToken: 'hashed-verification-token',
emailVerificationExpires: new Date(),
phoneVerified: false,
phoneVerificationCode: 'hashed-verification-token',
phoneVerificationExpires: new Date(),
isActive: true,
failedLoginAttempts: 0,
};

mockUserRepository.create.mockReturnValue(mockUser);
mockUserRepository.save.mockResolvedValue(mockUser);
mockSmsService.sendSms.mockResolvedValue({ success: true });

const result = await service.register(registerDto);

Expand Down Expand Up @@ -201,6 +219,7 @@ describe('AuthService', () => {
password: 'hashedPassword',
isActive: true,
emailVerified: true,
phoneVerified: true,
failedLoginAttempts: 0,
lockedUntil: null,
};
Expand Down Expand Up @@ -437,6 +456,7 @@ describe('AuthService', () => {
id: 'user-id',
email: 'test@example.com',
emailVerified: false,
phoneVerified: false,
emailVerificationToken: 'hashed-token',
emailVerificationExpires: new Date(Date.now() + 24 * 60 * 60 * 1000),
};
Expand All @@ -446,11 +466,12 @@ describe('AuthService', () => {
emailVerified: true,
});

await service.verifyEmail(verifyEmailDto);
const result = await service.verifyEmail(verifyEmailDto);

expect(TokenUtil.hashToken).toHaveBeenCalledWith(verifyEmailDto.token);
expect(mockUserRepository.findOne).toHaveBeenCalled();
expect(mockUserRepository.save).toHaveBeenCalled();
expect(result.emailVerified).toBe(true);
});

it('should throw BadRequestException for invalid token', async () => {
Expand All @@ -477,6 +498,47 @@ describe('AuthService', () => {
});
});

describe('verifyPhone', () => {
const verifyPhoneDto = {
email: 'test@example.com',
code: '123456',
};

it('should verify phone successfully', async () => {
jest.spyOn(TokenUtil, 'hashToken').mockReturnValue('hashed-code');
const mockUser = {
id: 'user-id',
email: 'test@example.com',
emailVerified: true,
phoneVerified: false,
phoneVerificationCode: 'hashed-code',
phoneVerificationExpires: new Date(Date.now() + 24 * 60 * 60 * 1000),
};
mockUsersService.findByEmail.mockResolvedValue(mockUser);
mockUserRepository.save.mockResolvedValue({
...mockUser,
phoneVerified: true,
});

const result = await service.verifyPhone(verifyPhoneDto);

expect(result.phoneVerified).toBe(true);
expect(mockUserRepository.save).toHaveBeenCalled();
});

it('should throw when the code is invalid', async () => {
jest.spyOn(TokenUtil, 'hashToken').mockReturnValue('wrong-code');
mockUsersService.findByEmail.mockResolvedValue({
phoneVerificationCode: 'hashed-code',
phoneVerificationExpires: new Date(Date.now() + 1000),
});

await expect(service.verifyPhone(verifyPhoneDto)).rejects.toThrow(
BadRequestException,
);
});
});

describe('forgotPassword', () => {
const forgotPasswordDto = {
email: 'test@example.com',
Expand Down
Loading