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:
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