Files
registry/test/unit/services/token.service.test.ts

261 lines
7.9 KiB
TypeScript
Raw Normal View History

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