Browse Source

Merge 9cb750e6d5 into bca5ce3f04

pull/6015/merge
Sven Günther 2 days ago
committed by GitHub
parent
commit
d146c99204
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 23
      DEVELOPMENT.md
  3. 254
      apps/api/src/app/auth/auth.service.spec.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Added
- improved test coverage for AuthService
- Introduced data source transformation support in the import functionality for self-hosted environments
- Added an optional 3D hover effect to the membership card component

23
DEVELOPMENT.md

@ -52,7 +52,28 @@ npm run database:push
## Testing
Run `npm test`
### Run Tests
```bash
npm test # All tests
```
### Writing Tests
**Always add tests for:**
- New services and controllers (required)
- Bug fixes (recommended)
- focus on business logic
- test expected behavior - not implementation details
**Test file location:** Place `.spec.ts` file next to the code file
**Key rules:**
- Mock external dependencies (database, APIs, HTTP)
- Test happy path AND error cases
- Use descriptive test names
## Experimental Features

254
apps/api/src/app/auth/auth.service.spec.ts

@ -0,0 +1,254 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let authService: AuthService;
let userService: jest.Mocked<UserService>;
let jwtService: jest.Mocked<JwtService>;
let configurationService: jest.Mocked<ConfigurationService>;
let propertyService: jest.Mocked<PropertyService>;
let module: TestingModule;
const mockUser = {
id: 'user-123',
accessToken: 'hashed-token',
authChallenge: null,
createdAt: new Date('2024-01-01'),
provider: 'GOOGLE' as any,
role: 'USER' as any,
thirdPartyId: 'google-123',
updatedAt: new Date('2024-01-01')
};
const mockJwtToken = 'jwt-token-xyz';
beforeEach(async () => {
const mockUserService = {
createAccessToken: jest.fn(),
users: jest.fn(),
createUser: jest.fn()
};
const mockJwtService = {
sign: jest.fn()
};
const mockConfigurationService = {
get: jest.fn()
};
const mockPropertyService = {
isUserSignupEnabled: jest.fn()
};
module = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UserService,
useValue: mockUserService
},
{
provide: JwtService,
useValue: mockJwtService
},
{
provide: ConfigurationService,
useValue: mockConfigurationService
},
{
provide: PropertyService,
useValue: mockPropertyService
}
]
}).compile();
authService = module.get<AuthService>(AuthService);
userService = module.get(UserService);
jwtService = module.get(JwtService);
configurationService = module.get(ConfigurationService);
propertyService = module.get(PropertyService);
});
afterEach(async () => {
await module.close();
});
it('should be defined', () => {
expect(authService).toBeDefined();
});
describe('validateAnonymousLogin', () => {
const accessToken = 'test-access-token';
const hashedToken = 'hashed-test-token';
const salt = 'test-salt';
beforeEach(() => {
configurationService.get.mockReturnValue(salt);
userService.createAccessToken.mockReturnValue(hashedToken);
jwtService.sign.mockReturnValue(mockJwtToken);
});
it('should return JWT token when user is found with valid access token', async () => {
userService.users.mockResolvedValue([mockUser]);
const result = await authService.validateAnonymousLogin(accessToken);
expect(result).toBe(mockJwtToken);
expect(configurationService.get).toHaveBeenCalledWith(
'ACCESS_TOKEN_SALT'
);
expect(userService.createAccessToken).toHaveBeenCalledWith({
password: accessToken,
salt
});
expect(userService.users).toHaveBeenCalledWith({
where: { accessToken: hashedToken }
});
expect(jwtService.sign).toHaveBeenCalledWith({
id: mockUser.id
});
});
it('should reject when user is not found', async () => {
userService.users.mockResolvedValue([]);
await expect(
authService.validateAnonymousLogin(accessToken)
).rejects.toBeUndefined();
});
it('should reject when users query throws error', async () => {
userService.users.mockRejectedValue(new Error('Database error'));
await expect(
authService.validateAnonymousLogin(accessToken)
).rejects.toBeUndefined();
});
it('should use correct salt from configuration', async () => {
const customSalt = 'custom-salt-value';
configurationService.get.mockReturnValue(customSalt);
userService.users.mockResolvedValue([mockUser]);
await authService.validateAnonymousLogin(accessToken);
expect(userService.createAccessToken).toHaveBeenCalledWith({
password: accessToken,
salt: customSalt
});
});
});
describe('validateOAuthLogin', () => {
const oAuthParams = {
provider: 'GOOGLE' as any,
thirdPartyId: 'google-user-123'
};
beforeEach(() => {
jwtService.sign.mockReturnValue(mockJwtToken);
});
it('should return JWT token when existing user is found', async () => {
userService.users.mockResolvedValue([mockUser]);
const result = await authService.validateOAuthLogin(oAuthParams);
expect(result).toBe(mockJwtToken);
expect(userService.users).toHaveBeenCalledWith({
where: {
provider: oAuthParams.provider,
thirdPartyId: oAuthParams.thirdPartyId
}
});
expect(jwtService.sign).toHaveBeenCalledWith({
id: mockUser.id
});
expect(userService.createUser).not.toHaveBeenCalled();
});
it('should create new user and return JWT when user not found and signup is enabled', async () => {
const newUser = { id: 'new-user-123', ...oAuthParams };
userService.users.mockResolvedValue([]);
propertyService.isUserSignupEnabled.mockResolvedValue(true);
userService.createUser.mockResolvedValue(newUser as any);
const result = await authService.validateOAuthLogin(oAuthParams);
expect(result).toBe(mockJwtToken);
expect(propertyService.isUserSignupEnabled).toHaveBeenCalled();
expect(userService.createUser).toHaveBeenCalledWith({
data: {
provider: oAuthParams.provider,
thirdPartyId: oAuthParams.thirdPartyId
}
});
expect(jwtService.sign).toHaveBeenCalledWith({
id: newUser.id
});
});
it('should throw InternalServerErrorException when user not found and signup is disabled', async () => {
userService.users.mockResolvedValue([]);
propertyService.isUserSignupEnabled.mockResolvedValue(false);
await expect(authService.validateOAuthLogin(oAuthParams)).rejects.toThrow(
InternalServerErrorException
);
expect(userService.createUser).not.toHaveBeenCalled();
});
it('should throw InternalServerErrorException with error message on failure', async () => {
const errorMessage = 'Database connection failed';
userService.users.mockRejectedValue(new Error(errorMessage));
await expect(authService.validateOAuthLogin(oAuthParams)).rejects.toThrow(
InternalServerErrorException
);
});
it('should handle different OAuth providers', async () => {
const githubParams = {
provider: 'GITHUB' as any,
thirdPartyId: 'github-user-456'
};
const githubUser = { id: 'github-user-id', ...githubParams };
userService.users.mockResolvedValue([githubUser as any]);
const result = await authService.validateOAuthLogin(githubParams);
expect(result).toBe(mockJwtToken);
expect(userService.users).toHaveBeenCalledWith({
where: {
provider: githubParams.provider,
thirdPartyId: githubParams.thirdPartyId
}
});
});
});
describe('JWT token generation', () => {
it('should generate token with correct user id', async () => {
const userId = 'test-user-id-123';
const user = { ...mockUser, id: userId };
userService.users.mockResolvedValue([user]);
configurationService.get.mockReturnValue('salt');
userService.createAccessToken.mockReturnValue('hash');
jwtService.sign.mockReturnValue(mockJwtToken);
await authService.validateAnonymousLogin('token');
expect(jwtService.sign).toHaveBeenCalledWith({
id: userId
});
});
});
});
Loading…
Cancel
Save