/** * 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); }); }); });