native auth
This commit is contained in:
@@ -1,449 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { URLSearchParams } from 'url';
|
||||
import axios from 'axios';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import type { AuthenticatedUser } from './decorators/current-user.decorator';
|
||||
import { User } from '../users/users.entity';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
const mockUser: User = {
|
||||
id: 'uuid-123',
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
lastLoginAt: new Date('2024-01-01'),
|
||||
activeDollId: null,
|
||||
};
|
||||
|
||||
const mockUsersService: jest.Mocked<
|
||||
Pick<UsersService, 'createFromToken' | 'findByKeycloakSub' | 'findOrCreate'>
|
||||
> = {
|
||||
createFromToken: jest.fn().mockResolvedValue(mockUser),
|
||||
findByKeycloakSub: jest.fn().mockResolvedValue(null),
|
||||
findOrCreate: jest.fn().mockResolvedValue(mockUser),
|
||||
};
|
||||
|
||||
const mockAuthUser: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthService,
|
||||
{
|
||||
provide: UsersService,
|
||||
useValue: mockUsersService,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: (key: string) => {
|
||||
if (key === 'JWT_ISSUER') return 'https://auth.example.com';
|
||||
if (key === 'KEYCLOAK_CLIENT_ID') return 'friendolls-client';
|
||||
if (key === 'KEYCLOAK_CLIENT_SECRET') return 'secret';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('revokeToken', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(axios, 'post').mockReset();
|
||||
});
|
||||
|
||||
it('should skip when config missing', async () => {
|
||||
const missingConfigService = new ConfigService({});
|
||||
const localService = new AuthService(
|
||||
mockUsersService as unknown as UsersService,
|
||||
missingConfigService,
|
||||
);
|
||||
|
||||
const warnSpy = jest.spyOn<any, any>(localService['logger'], 'warn');
|
||||
const result = await localService.revokeToken('rt');
|
||||
expect(result).toBe(false);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return true on successful revocation', async () => {
|
||||
jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
|
||||
|
||||
const result = await service.revokeToken('rt-success');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://auth.example.com/protocol/openid-connect/revoke',
|
||||
expect.any(URLSearchParams),
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false on non-2xx response', async () => {
|
||||
jest.spyOn(axios, 'post').mockResolvedValue({ status: 400 });
|
||||
const warnSpy = jest.spyOn<any, any>(service['logger'], 'warn');
|
||||
|
||||
const result = await service.revokeToken('rt-fail');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false on error', async () => {
|
||||
jest.spyOn(axios, 'post').mockRejectedValue({ message: 'boom' });
|
||||
const warnSpy = jest.spyOn<any, any>(service['logger'], 'warn');
|
||||
|
||||
const result = await service.revokeToken('rt-error');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncUserFromToken', () => {
|
||||
it('should create a new user if user does not exist', async () => {
|
||||
mockUsersService.createFromToken.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.syncUserFromToken(mockAuthUser);
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith({
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle existing user via upsert', async () => {
|
||||
const updatedUser = { ...mockUser, lastLoginAt: new Date('2024-02-01') };
|
||||
mockUsersService.createFromToken.mockResolvedValue(updatedUser);
|
||||
|
||||
const result = await service.syncUserFromToken(mockAuthUser);
|
||||
|
||||
expect(result).toEqual(updatedUser);
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith({
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle user with no email by using empty string', async () => {
|
||||
const authUserNoEmail: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:user456',
|
||||
name: 'No Email User',
|
||||
};
|
||||
|
||||
mockUsersService.createFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
email: '',
|
||||
name: 'No Email User',
|
||||
});
|
||||
|
||||
await service.syncUserFromToken(authUserNoEmail);
|
||||
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: '',
|
||||
name: 'No Email User',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle user with no name by using username or fallback', async () => {
|
||||
const authUserNoName: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:user789',
|
||||
username: 'fallbackuser',
|
||||
};
|
||||
|
||||
mockUsersService.createFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
name: 'fallbackuser',
|
||||
});
|
||||
|
||||
await service.syncUserFromToken(authUserNoName);
|
||||
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'fallbackuser',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use "Unknown User" when no name or username is available', async () => {
|
||||
const authUserMinimal: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:minimal',
|
||||
};
|
||||
|
||||
mockUsersService.createFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
name: 'Unknown User',
|
||||
});
|
||||
|
||||
await service.syncUserFromToken(authUserMinimal);
|
||||
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Unknown User',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty keycloakSub gracefully', async () => {
|
||||
const authUserEmptySub: AuthenticatedUser = {
|
||||
keycloakSub: '',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Sub User',
|
||||
};
|
||||
|
||||
mockUsersService.createFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
keycloakSub: '',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Sub User',
|
||||
});
|
||||
|
||||
await service.syncUserFromToken(authUserEmptySub);
|
||||
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keycloakSub: '',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Sub User',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle malformed keycloakSub', async () => {
|
||||
const authUserMalformed: AuthenticatedUser = {
|
||||
keycloakSub: 'invalid-format',
|
||||
email: 'malformed@example.com',
|
||||
name: 'Malformed User',
|
||||
};
|
||||
|
||||
mockUsersService.createFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
keycloakSub: 'invalid-format',
|
||||
email: 'malformed@example.com',
|
||||
name: 'Malformed User',
|
||||
});
|
||||
|
||||
const result = await service.syncUserFromToken(authUserMalformed);
|
||||
|
||||
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keycloakSub: 'invalid-format',
|
||||
email: 'malformed@example.com',
|
||||
name: 'Malformed User',
|
||||
}),
|
||||
);
|
||||
expect(result.keycloakSub).toBe('invalid-format');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureUserExists', () => {
|
||||
it('should call findOrCreate with correct params', async () => {
|
||||
mockUsersService.findOrCreate.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.ensureUserExists(mockAuthUser);
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockUsersService.findOrCreate).toHaveBeenCalledWith({
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing username gracefully', async () => {
|
||||
mockUsersService.findOrCreate.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.ensureUserExists({
|
||||
...mockAuthUser,
|
||||
username: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockUsersService.findOrCreate).toHaveBeenCalledWith({
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: undefined,
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRole', () => {
|
||||
it('should return true if user has the required role', () => {
|
||||
const result = service.hasRole(mockAuthUser, 'user');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if user does not have the required role', () => {
|
||||
const result = service.hasRole(mockAuthUser, 'admin');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user has no roles', () => {
|
||||
const authUserNoRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:noroles',
|
||||
email: 'noroles@example.com',
|
||||
name: 'No Roles User',
|
||||
};
|
||||
|
||||
const result = service.hasRole(authUserNoRoles, 'user');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user roles is empty array', () => {
|
||||
const authUserEmptyRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:emptyroles',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Roles User',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const result = service.hasRole(authUserEmptyRoles, 'user');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAnyRole', () => {
|
||||
it('should return true if user has at least one of the required roles', () => {
|
||||
const result = service.hasAnyRole(mockAuthUser, ['admin', 'premium']);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if user has none of the required roles', () => {
|
||||
const result = service.hasAnyRole(mockAuthUser, ['admin', 'moderator']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user has no roles', () => {
|
||||
const authUserNoRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:noroles',
|
||||
email: 'noroles@example.com',
|
||||
name: 'No Roles User',
|
||||
};
|
||||
|
||||
const result = service.hasAnyRole(authUserNoRoles, ['admin', 'user']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user roles is empty array', () => {
|
||||
const authUserEmptyRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:emptyroles',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Roles User',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const result = service.hasAnyRole(authUserEmptyRoles, ['admin', 'user']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple matching roles', () => {
|
||||
const result = service.hasAnyRole(mockAuthUser, ['user', 'premium']);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAllRoles', () => {
|
||||
it('should return true if user has all of the required roles', () => {
|
||||
const result = service.hasAllRoles(mockAuthUser, ['user', 'premium']);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if user has only some of the required roles', () => {
|
||||
const result = service.hasAllRoles(mockAuthUser, [
|
||||
'user',
|
||||
'premium',
|
||||
'admin',
|
||||
]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user has none of the required roles', () => {
|
||||
const result = service.hasAllRoles(mockAuthUser, ['admin', 'moderator']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user has no roles', () => {
|
||||
const authUserNoRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:noroles',
|
||||
email: 'noroles@example.com',
|
||||
name: 'No Roles User',
|
||||
};
|
||||
|
||||
const result = service.hasAllRoles(authUserNoRoles, ['user']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if user roles is empty array', () => {
|
||||
const authUserEmptyRoles: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:emptyroles',
|
||||
email: 'empty@example.com',
|
||||
name: 'Empty Roles User',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const result = service.hasAllRoles(authUserEmptyRoles, ['user']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for single role check', () => {
|
||||
const result = service.hasAllRoles(mockAuthUser, ['user']);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user