managarten/services/mana-core-auth/test/integration/auth-flow.integration.spec.ts
Wuesteon 9c47119535 Fix wrong type
import, make auth and chat work
2025-12-04 23:25:25 +01:00

487 lines
14 KiB
TypeScript

/**
* Authentication Flow Integration Tests
*
* Tests complete authentication workflows:
* - Registration → Login → Token Generation
* - Token Refresh → Logout
* - Multi-device sessions
*/
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from '../../src/auth/auth.service';
import { CreditsService } from '../../src/credits/credits.service';
import configuration from '../../src/config/configuration';
describe('Authentication Flow Integration Tests', () => {
let authService: AuthService;
let creditsService: CreditsService;
let module: TestingModule;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true,
}),
],
providers: [AuthService, CreditsService],
}).compile();
authService = module.get<AuthService>(AuthService);
creditsService = module.get<CreditsService>(CreditsService);
});
afterAll(async () => {
await module.close();
});
describe('B2C User Registration → Login → Token Flow', () => {
it('should complete full B2C registration and login flow', async () => {
const uniqueEmail = `test-b2c-${Date.now()}@example.com`;
// Step 1: Register new user
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Test User',
});
expect(registerResult).toMatchObject({
id: expect.any(String),
email: uniqueEmail,
name: 'Test User',
});
const userId = registerResult.id;
// Step 2: Initialize credit balance
const balance = await creditsService.initializeUserBalance(userId);
expect(balance).toMatchObject({
userId,
balance: 0,
freeCreditsRemaining: 150, // Signup bonus
dailyFreeCredits: 5,
});
// Step 3: Login with credentials
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
expect(loginResult).toMatchObject({
user: {
id: userId,
email: uniqueEmail,
},
accessToken: expect.any(String),
refreshToken: expect.any(String),
tokenType: 'Bearer',
expiresIn: 900, // 15 minutes
});
// Step 4: Validate access token
const validationResult = await authService.validateToken(loginResult.accessToken);
expect(validationResult.valid).toBe(true);
expect(validationResult.payload).toMatchObject({
sub: userId,
email: uniqueEmail,
role: 'user',
});
});
it('should support multiple login sessions from different devices', async () => {
const uniqueEmail = `multi-device-${Date.now()}@example.com`;
// Register user
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Multi Device User',
});
// Login from mobile device
const mobileLogin = await authService.login(
{
email: uniqueEmail,
password: 'SecurePassword123!',
deviceId: 'mobile-device-123',
deviceName: 'iPhone 15',
},
'192.168.1.100',
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)'
);
// Login from web device
const webLogin = await authService.login(
{
email: uniqueEmail,
password: 'SecurePassword123!',
deviceId: 'web-device-456',
deviceName: 'Chrome Browser',
},
'192.168.1.101',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
// Both sessions should be valid
expect(mobileLogin.accessToken).toBeDefined();
expect(webLogin.accessToken).toBeDefined();
expect(mobileLogin.accessToken).not.toBe(webLogin.accessToken);
// Validate both tokens
const mobileValidation = await authService.validateToken(mobileLogin.accessToken);
const webValidation = await authService.validateToken(webLogin.accessToken);
expect(mobileValidation.valid).toBe(true);
expect(webValidation.valid).toBe(true);
// Session IDs should be different
expect(mobileValidation.payload.sessionId).not.toBe(webValidation.payload.sessionId);
});
});
describe('Token Refresh Flow', () => {
it('should refresh tokens and rotate refresh token', async () => {
const uniqueEmail = `refresh-test-${Date.now()}@example.com`;
// Register and login
await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Refresh Test User',
});
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
const originalRefreshToken = loginResult.refreshToken;
const originalAccessToken = loginResult.accessToken;
// Wait a moment to ensure different timestamps
await new Promise((resolve) => setTimeout(resolve, 100));
// Refresh tokens
const refreshResult = await authService.refreshToken(originalRefreshToken);
expect(refreshResult).toMatchObject({
user: {
email: uniqueEmail,
},
accessToken: expect.any(String),
refreshToken: expect.any(String),
});
// New tokens should be different
expect(refreshResult.accessToken).not.toBe(originalAccessToken);
expect(refreshResult.refreshToken).not.toBe(originalRefreshToken);
// Old refresh token should be revoked
await expect(authService.refreshToken(originalRefreshToken)).rejects.toThrow(
'Invalid refresh token'
);
// New refresh token should work
const secondRefreshResult = await authService.refreshToken(refreshResult.refreshToken);
expect(secondRefreshResult.accessToken).toBeDefined();
});
it('should not allow refresh with revoked token after logout', async () => {
const uniqueEmail = `logout-test-${Date.now()}@example.com`;
// Register and login
await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Logout Test User',
});
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
const refreshToken = loginResult.refreshToken;
// Extract sessionId from access token
const validation = await authService.validateToken(loginResult.accessToken);
const sessionId = validation.payload.sessionId;
// Logout
await authService.logout(sessionId);
// Attempt to refresh with revoked token
await expect(authService.refreshToken(refreshToken)).rejects.toThrow('Invalid refresh token');
});
});
describe('Logout Flow', () => {
it('should revoke session on logout', async () => {
const uniqueEmail = `logout-flow-${Date.now()}@example.com`;
// Register and login
await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Logout Flow User',
});
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
// Extract sessionId
const validation = await authService.validateToken(loginResult.accessToken);
const sessionId = validation.payload.sessionId;
// Logout
const logoutResult = await authService.logout(sessionId);
expect(logoutResult).toEqual({
message: 'Logged out successfully',
});
// Refresh token should no longer work
await expect(authService.refreshToken(loginResult.refreshToken)).rejects.toThrow();
});
it('should not affect other sessions when logging out one session', async () => {
const uniqueEmail = `multi-session-logout-${Date.now()}@example.com`;
// Register
await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Multi Session User',
});
// Create two sessions
const session1 = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
deviceId: 'device-1',
});
const session2 = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
deviceId: 'device-2',
});
// Logout session 1
const validation1 = await authService.validateToken(session1.accessToken);
await authService.logout(validation1.payload.sessionId);
// Session 1 refresh token should not work
await expect(authService.refreshToken(session1.refreshToken)).rejects.toThrow();
// Session 2 should still work
const session2Refresh = await authService.refreshToken(session2.refreshToken);
expect(session2Refresh.accessToken).toBeDefined();
});
});
describe('Security Validations', () => {
it('should prevent registration with duplicate email', async () => {
const duplicateEmail = `duplicate-${Date.now()}@example.com`;
// First registration
await authService.register({
email: duplicateEmail,
password: 'SecurePassword123!',
name: 'First User',
});
// Second registration with same email should fail
await expect(
authService.register({
email: duplicateEmail,
password: 'AnotherPassword456!',
name: 'Second User',
})
).rejects.toThrow('User with this email already exists');
});
it('should reject login with incorrect password', async () => {
const uniqueEmail = `wrong-password-${Date.now()}@example.com`;
await authService.register({
email: uniqueEmail,
password: 'CorrectPassword123!',
name: 'Password Test User',
});
await expect(
authService.login({
email: uniqueEmail,
password: 'WrongPassword123!',
})
).rejects.toThrow('Invalid credentials');
});
it('should reject login for non-existent user', async () => {
await expect(
authService.login({
email: `nonexistent-${Date.now()}@example.com`,
password: 'SomePassword123!',
})
).rejects.toThrow('Invalid credentials');
});
it('should normalize email to lowercase', async () => {
const mixedCaseEmail = `MixedCase${Date.now()}@EXAMPLE.COM`;
const registerResult = await authService.register({
email: mixedCaseEmail,
password: 'SecurePassword123!',
name: 'Mixed Case User',
});
expect(registerResult.email).toBe(mixedCaseEmail.toLowerCase());
// Should be able to login with different casing
const loginResult = await authService.login({
email: mixedCaseEmail.toUpperCase(),
password: 'SecurePassword123!',
});
expect(loginResult.user.email).toBe(mixedCaseEmail.toLowerCase());
});
});
describe('Credit Balance Integration', () => {
it('should initialize credit balance automatically on registration', async () => {
const uniqueEmail = `credits-init-${Date.now()}@example.com`;
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Credits User',
});
const userId = registerResult.id;
// Initialize balance
const balance = await creditsService.initializeUserBalance(userId);
expect(balance.freeCreditsRemaining).toBe(150); // Signup bonus
expect(balance.dailyFreeCredits).toBe(5);
expect(balance.balance).toBe(0);
});
it('should not create duplicate balances', async () => {
const uniqueEmail = `no-duplicate-balance-${Date.now()}@example.com`;
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'No Duplicate User',
});
const userId = registerResult.id;
// Initialize balance twice
const balance1 = await creditsService.initializeUserBalance(userId);
const balance2 = await creditsService.initializeUserBalance(userId);
// Should return the same balance
expect(balance1.userId).toBe(balance2.userId);
expect(balance1.freeCreditsRemaining).toBe(balance2.freeCreditsRemaining);
});
});
describe('Error Handling', () => {
it('should handle soft-deleted user login attempt', async () => {
const uniqueEmail = `deleted-user-${Date.now()}@example.com`;
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'To Be Deleted',
});
// Note: In a real scenario, you'd soft-delete the user here
// For now, we just test the logic exists
// This test validates the login check for deletedAt field exists
expect(registerResult.id).toBeDefined();
});
it('should handle expired refresh token', async () => {
const uniqueEmail = `expired-token-${Date.now()}@example.com`;
await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Expired Token User',
});
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
// Test with obviously invalid token
await expect(authService.refreshToken('invalid-refresh-token')).rejects.toThrow();
});
});
describe('Password Security', () => {
it('should hash passwords using bcrypt with proper cost factor', async () => {
const uniqueEmail = `password-hash-${Date.now()}@example.com`;
const registerResult = await authService.register({
email: uniqueEmail,
password: 'TestPassword123!',
name: 'Hash Test User',
});
// Login should work with correct password
const loginResult = await authService.login({
email: uniqueEmail,
password: 'TestPassword123!',
});
expect(loginResult.accessToken).toBeDefined();
// Login should fail with incorrect password
await expect(
authService.login({
email: uniqueEmail,
password: 'WrongPassword123!',
})
).rejects.toThrow('Invalid credentials');
});
it('should not expose password in any response', async () => {
const uniqueEmail = `no-password-leak-${Date.now()}@example.com`;
const registerResult = await authService.register({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'No Leak User',
});
// Registration response should not contain password
expect(registerResult).not.toHaveProperty('password');
expect(registerResult).not.toHaveProperty('hashedPassword');
const loginResult = await authService.login({
email: uniqueEmail,
password: 'SecurePassword123!',
});
// Login response should not contain password
expect(loginResult.user).not.toHaveProperty('password');
expect(loginResult.user).not.toHaveProperty('hashedPassword');
});
});
});