Add unit tests for models and services
- Implemented unit tests for the Package model, covering methods such as generateId, findById, findByName, and version management. - Created unit tests for the Repository model, including repository creation, name validation, and retrieval methods. - Added tests for the Session model, focusing on session creation, validation, and invalidation. - Developed unit tests for the User model, ensuring user creation, password hashing, and retrieval methods function correctly. - Implemented AuthService tests, validating login, token refresh, and session management. - Added TokenService tests, covering token creation, validation, and revocation processes.
This commit is contained in:
224
test/unit/services/auth.service.test.ts
Normal file
224
test/unit/services/auth.service.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* AuthService unit tests
|
||||
*/
|
||||
|
||||
import { assertEquals, assertExists } from 'jsr:@std/assert';
|
||||
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
|
||||
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
|
||||
import { AuthService } from '../../../ts/services/auth.service.ts';
|
||||
import { Session } from '../../../ts/models/session.ts';
|
||||
import { testConfig } from '../../test.config.ts';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let authService: AuthService;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestDb();
|
||||
authService = new AuthService({
|
||||
jwtSecret: testConfig.jwt.secret,
|
||||
accessTokenExpiresIn: 60, // 1 minute for tests
|
||||
refreshTokenExpiresIn: 300, // 5 minutes for tests
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanupTestDb();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should successfully login with valid credentials', async () => {
|
||||
const { user, password } = await createTestUser({
|
||||
email: 'login@example.com',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const result = await authService.login(user.email, password, {
|
||||
userAgent: 'TestAgent/1.0',
|
||||
ipAddress: '127.0.0.1',
|
||||
});
|
||||
|
||||
assertEquals(result.success, true);
|
||||
assertExists(result.user);
|
||||
assertEquals(result.user.id, user.id);
|
||||
assertExists(result.accessToken);
|
||||
assertExists(result.refreshToken);
|
||||
assertExists(result.sessionId);
|
||||
});
|
||||
|
||||
it('should fail with invalid email', async () => {
|
||||
const result = await authService.login('nonexistent@example.com', 'password');
|
||||
|
||||
assertEquals(result.success, false);
|
||||
assertEquals(result.errorCode, 'INVALID_CREDENTIALS');
|
||||
});
|
||||
|
||||
it('should fail with invalid password', async () => {
|
||||
const { user } = await createTestUser({ email: 'wrongpass@example.com' });
|
||||
|
||||
const result = await authService.login(user.email, 'wrongpassword');
|
||||
|
||||
assertEquals(result.success, false);
|
||||
assertEquals(result.errorCode, 'INVALID_CREDENTIALS');
|
||||
});
|
||||
|
||||
it('should fail for inactive user', async () => {
|
||||
const { user, password } = await createTestUser({
|
||||
email: 'inactive@example.com',
|
||||
status: 'suspended',
|
||||
});
|
||||
|
||||
const result = await authService.login(user.email, password);
|
||||
|
||||
assertEquals(result.success, false);
|
||||
assertEquals(result.errorCode, 'ACCOUNT_INACTIVE');
|
||||
});
|
||||
|
||||
it('should create a session on successful login', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'session@example.com' });
|
||||
|
||||
const result = await authService.login(user.email, password);
|
||||
|
||||
assertEquals(result.success, true);
|
||||
assertExists(result.sessionId);
|
||||
|
||||
const session = await Session.findValidSession(result.sessionId!);
|
||||
assertExists(session);
|
||||
assertEquals(session.userId, user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should refresh access token with valid refresh token', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'refresh@example.com' });
|
||||
const loginResult = await authService.login(user.email, password);
|
||||
|
||||
assertEquals(loginResult.success, true);
|
||||
|
||||
const refreshResult = await authService.refresh(loginResult.refreshToken!);
|
||||
|
||||
assertEquals(refreshResult.success, true);
|
||||
assertExists(refreshResult.accessToken);
|
||||
assertEquals(refreshResult.sessionId, loginResult.sessionId);
|
||||
});
|
||||
|
||||
it('should fail with invalid refresh token', async () => {
|
||||
const result = await authService.refresh('invalid-token');
|
||||
|
||||
assertEquals(result.success, false);
|
||||
assertEquals(result.errorCode, 'INVALID_TOKEN');
|
||||
});
|
||||
|
||||
it('should fail when session is invalidated', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'invalidsession@example.com' });
|
||||
const loginResult = await authService.login(user.email, password);
|
||||
|
||||
// Invalidate session
|
||||
const session = await Session.findValidSession(loginResult.sessionId!);
|
||||
await session!.invalidate('test');
|
||||
|
||||
const refreshResult = await authService.refresh(loginResult.refreshToken!);
|
||||
|
||||
assertEquals(refreshResult.success, false);
|
||||
assertEquals(refreshResult.errorCode, 'SESSION_INVALID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAccessToken', () => {
|
||||
it('should validate valid access token', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'validate@example.com' });
|
||||
const loginResult = await authService.login(user.email, password);
|
||||
|
||||
const validation = await authService.validateAccessToken(loginResult.accessToken!);
|
||||
|
||||
assertExists(validation);
|
||||
assertEquals(validation.user.id, user.id);
|
||||
assertEquals(validation.sessionId, loginResult.sessionId);
|
||||
});
|
||||
|
||||
it('should reject invalid access token', async () => {
|
||||
const validation = await authService.validateAccessToken('invalid-token');
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
|
||||
it('should reject when session is invalidated', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'invalidated@example.com' });
|
||||
const loginResult = await authService.login(user.email, password);
|
||||
|
||||
// Invalidate session
|
||||
const session = await Session.findValidSession(loginResult.sessionId!);
|
||||
await session!.invalidate('test');
|
||||
|
||||
const validation = await authService.validateAccessToken(loginResult.accessToken!);
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should invalidate session', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'logout@example.com' });
|
||||
const loginResult = await authService.login(user.email, password);
|
||||
|
||||
const success = await authService.logout(loginResult.sessionId!);
|
||||
|
||||
assertEquals(success, true);
|
||||
|
||||
const session = await Session.findValidSession(loginResult.sessionId!);
|
||||
assertEquals(session, null);
|
||||
});
|
||||
|
||||
it('should return false for non-existent session', async () => {
|
||||
const success = await authService.logout('non-existent-session-id');
|
||||
|
||||
assertEquals(success, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutAll', () => {
|
||||
it('should invalidate all user sessions', async () => {
|
||||
const { user, password } = await createTestUser({ email: 'logoutall@example.com' });
|
||||
|
||||
// Create multiple sessions
|
||||
await authService.login(user.email, password);
|
||||
await authService.login(user.email, password);
|
||||
await authService.login(user.email, password);
|
||||
|
||||
const count = await authService.logoutAll(user.id);
|
||||
|
||||
assertEquals(count, 3);
|
||||
|
||||
const sessions = await Session.getUserSessions(user.id);
|
||||
assertEquals(sessions.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('static password methods', () => {
|
||||
it('should hash and verify password', async () => {
|
||||
const password = 'MySecurePassword123!';
|
||||
const hash = await AuthService.hashPassword(password);
|
||||
|
||||
const isValid = await AuthService.verifyPassword(password, hash);
|
||||
assertEquals(isValid, true);
|
||||
|
||||
const isInvalid = await AuthService.verifyPassword('WrongPassword', hash);
|
||||
assertEquals(isInvalid, false);
|
||||
});
|
||||
|
||||
it('should generate different hashes for same password', async () => {
|
||||
const password = 'SamePassword';
|
||||
const hash1 = await AuthService.hashPassword(password);
|
||||
const hash2 = await AuthService.hashPassword(password);
|
||||
|
||||
assertEquals(hash1 !== hash2, true);
|
||||
|
||||
// But both should verify
|
||||
assertEquals(await AuthService.verifyPassword(password, hash1), true);
|
||||
assertEquals(await AuthService.verifyPassword(password, hash2), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
260
test/unit/services/token.service.test.ts
Normal file
260
test/unit/services/token.service.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* TokenService unit tests
|
||||
*/
|
||||
|
||||
import { assertEquals, assertExists, assertMatch } from 'jsr:@std/assert';
|
||||
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
|
||||
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
|
||||
import { TokenService } from '../../../ts/services/token.service.ts';
|
||||
import { ApiToken } from '../../../ts/models/apitoken.ts';
|
||||
|
||||
describe('TokenService', () => {
|
||||
let tokenService: TokenService;
|
||||
let testUserId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestDb();
|
||||
tokenService = new TokenService();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanupTestDb();
|
||||
const { user } = await createTestUser();
|
||||
testUserId = user.id;
|
||||
});
|
||||
|
||||
describe('createToken', () => {
|
||||
it('should create token with correct format', async () => {
|
||||
const result = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'test-token',
|
||||
protocols: ['npm', 'oci'],
|
||||
scopes: [{ protocol: '*', actions: ['read', 'write'] }],
|
||||
});
|
||||
|
||||
assertExists(result.rawToken);
|
||||
assertExists(result.token);
|
||||
|
||||
// Check token format: srg_{prefix}_{random}
|
||||
assertMatch(result.rawToken, /^srg_[a-z0-9]+_[a-z0-9]+$/);
|
||||
assertEquals(result.token.name, 'test-token');
|
||||
assertEquals(result.token.protocols.includes('npm'), true);
|
||||
assertEquals(result.token.protocols.includes('oci'), true);
|
||||
});
|
||||
|
||||
it('should store hashed token', async () => {
|
||||
const result = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'hashed-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
// The stored token should be hashed
|
||||
assertEquals(result.token.tokenHash !== result.rawToken, true);
|
||||
assertEquals(result.token.tokenHash.length, 64); // SHA-256 hex
|
||||
});
|
||||
|
||||
it('should set expiration when provided', async () => {
|
||||
const result = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'expiring-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
expiresInDays: 30,
|
||||
});
|
||||
|
||||
assertExists(result.token.expiresAt);
|
||||
|
||||
const expectedExpiry = new Date();
|
||||
expectedExpiry.setDate(expectedExpiry.getDate() + 30);
|
||||
|
||||
// Should be within a few seconds of expected
|
||||
const diff = Math.abs(result.token.expiresAt.getTime() - expectedExpiry.getTime());
|
||||
assertEquals(diff < 5000, true);
|
||||
});
|
||||
|
||||
it('should create org-owned token', async () => {
|
||||
const orgId = 'test-org-123';
|
||||
const result = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
organizationId: orgId,
|
||||
name: 'org-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', organizationId: orgId, actions: ['read', 'write'] }],
|
||||
});
|
||||
|
||||
assertEquals(result.token.organizationId, orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateToken', () => {
|
||||
it('should validate correct token', async () => {
|
||||
const { rawToken } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'valid-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
|
||||
|
||||
assertExists(validation);
|
||||
assertEquals(validation.userId, testUserId);
|
||||
assertEquals(validation.protocols.includes('npm'), true);
|
||||
});
|
||||
|
||||
it('should reject invalid token format', async () => {
|
||||
const validation = await tokenService.validateToken('invalid-format', '127.0.0.1');
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
|
||||
it('should reject non-existent token', async () => {
|
||||
const validation = await tokenService.validateToken('srg_abc123_def456', '127.0.0.1');
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
|
||||
it('should reject revoked token', async () => {
|
||||
const { rawToken, token } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'revoked-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
await token.revoke('Test revocation');
|
||||
|
||||
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
|
||||
it('should reject expired token', async () => {
|
||||
const { rawToken, token } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'expired-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
expiresInDays: 1,
|
||||
});
|
||||
|
||||
// Manually set expiry to past
|
||||
token.expiresAt = new Date(Date.now() - 86400000);
|
||||
await token.save();
|
||||
|
||||
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
|
||||
|
||||
assertEquals(validation, null);
|
||||
});
|
||||
|
||||
it('should record usage on validation', async () => {
|
||||
const { rawToken, token } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'usage-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
await tokenService.validateToken(rawToken, '192.168.1.100');
|
||||
|
||||
// Reload token from DB
|
||||
const updated = await ApiToken.findByHash(token.tokenHash);
|
||||
assertExists(updated);
|
||||
assertExists(updated.lastUsedAt);
|
||||
assertEquals(updated.lastUsedIp, '192.168.1.100');
|
||||
assertEquals(updated.usageCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserTokens', () => {
|
||||
it('should return all user tokens', async () => {
|
||||
await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'token1',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'token2',
|
||||
protocols: ['oci'],
|
||||
scopes: [{ protocol: 'oci', actions: ['read'] }],
|
||||
});
|
||||
|
||||
const tokens = await tokenService.getUserTokens(testUserId);
|
||||
|
||||
assertEquals(tokens.length, 2);
|
||||
});
|
||||
|
||||
it('should not return revoked tokens', async () => {
|
||||
const { token } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'revoked',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'active',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
await token.revoke('test');
|
||||
|
||||
const tokens = await tokenService.getUserTokens(testUserId);
|
||||
|
||||
assertEquals(tokens.length, 1);
|
||||
assertEquals(tokens[0].name, 'active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeToken', () => {
|
||||
it('should revoke token with reason', async () => {
|
||||
const { token } = await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'to-revoke',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
await tokenService.revokeToken(token.id, 'Security concern');
|
||||
|
||||
const updated = await ApiToken.findByPrefix(token.tokenPrefix);
|
||||
assertExists(updated);
|
||||
assertEquals(updated.isRevoked, true);
|
||||
assertEquals(updated.revokedReason, 'Security concern');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrgTokens', () => {
|
||||
it('should return organization tokens', async () => {
|
||||
const orgId = 'org-123';
|
||||
|
||||
await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
organizationId: orgId,
|
||||
name: 'org-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
await tokenService.createToken({
|
||||
userId: testUserId,
|
||||
name: 'personal-token',
|
||||
protocols: ['npm'],
|
||||
scopes: [{ protocol: 'npm', actions: ['read'] }],
|
||||
});
|
||||
|
||||
const tokens = await tokenService.getOrgTokens(orgId);
|
||||
|
||||
assertEquals(tokens.length, 1);
|
||||
assertEquals(tokens[0].organizationId, orgId);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user