import { expect, tap } from '@git.zone/tstest/tapbundle'; import { DefaultAuthProvider } from '../ts/core/classes.defaultauthprovider.js'; import { AuthManager } from '../ts/core/classes.authmanager.js'; import type { IAuthProvider } from '../ts/core/interfaces.auth.js'; import type { IAuthConfig, IAuthToken } from '../ts/core/interfaces.core.js'; import { createMockAuthProvider } from './helpers/registry.js'; // ============================================================================ // Test State // ============================================================================ let provider: DefaultAuthProvider; let authConfig: IAuthConfig; // ============================================================================ // Setup // ============================================================================ tap.test('setup: should create DefaultAuthProvider', async () => { authConfig = { jwtSecret: 'test-secret-key-for-jwt-signing', tokenStore: 'memory', npmTokens: { enabled: true }, ociTokens: { enabled: true, realm: 'https://auth.example.com/token', service: 'test-registry', }, mavenTokens: { enabled: true }, cargoTokens: { enabled: true }, composerTokens: { enabled: true }, pypiTokens: { enabled: true }, rubygemsTokens: { enabled: true }, }; provider = new DefaultAuthProvider(authConfig); await provider.init(); expect(provider).toBeInstanceOf(DefaultAuthProvider); }); // ============================================================================ // Authentication Tests // ============================================================================ tap.test('authenticate: should authenticate new user (auto-registration)', async () => { const userId = await provider.authenticate({ username: 'newuser', password: 'newpassword', }); expect(userId).toEqual('newuser'); }); tap.test('authenticate: should authenticate existing user with correct password', async () => { // First registration await provider.authenticate({ username: 'existinguser', password: 'correctpass', }); // Second authentication with same credentials const userId = await provider.authenticate({ username: 'existinguser', password: 'correctpass', }); expect(userId).toEqual('existinguser'); }); tap.test('authenticate: should reject authentication with wrong password', async () => { // First registration await provider.authenticate({ username: 'passworduser', password: 'originalpass', }); // Attempt with wrong password const userId = await provider.authenticate({ username: 'passworduser', password: 'wrongpass', }); expect(userId).toBeNull(); }); // ============================================================================ // Token Creation Tests // ============================================================================ tap.test('createToken: should create NPM token with correct scopes', async () => { const token = await provider.createToken('testuser', 'npm', { scopes: ['npm:package:*:*'], }); expect(token).toBeTruthy(); expect(typeof token).toEqual('string'); // Validate the token const validated = await provider.validateToken(token, 'npm'); expect(validated).toBeTruthy(); expect(validated!.type).toEqual('npm'); expect(validated!.userId).toEqual('testuser'); expect(validated!.scopes).toContain('npm:package:*:*'); }); tap.test('createToken: should create Maven token', async () => { const token = await provider.createToken('mavenuser', 'maven', { readonly: true, }); expect(token).toBeTruthy(); const validated = await provider.validateToken(token, 'maven'); expect(validated).toBeTruthy(); expect(validated!.type).toEqual('maven'); expect(validated!.readonly).toBeTrue(); }); tap.test('createToken: should create OCI JWT token with correct claims', async () => { const token = await provider.createToken('ociuser', 'oci', { scopes: ['oci:repository:myrepo:push', 'oci:repository:myrepo:pull'], expiresIn: 3600, }); expect(token).toBeTruthy(); // OCI tokens are JWTs (contain dots) expect(token.split('.').length).toEqual(3); const validated = await provider.validateToken(token, 'oci'); expect(validated).toBeTruthy(); expect(validated!.type).toEqual('oci'); expect(validated!.userId).toEqual('ociuser'); expect(validated!.scopes.length).toBeGreaterThan(0); }); tap.test('createToken: should create token with expiration', async () => { const token = await provider.createToken('expiryuser', 'npm', { expiresIn: 60, // 60 seconds }); const validated = await provider.validateToken(token, 'npm'); expect(validated).toBeTruthy(); expect(validated!.expiresAt).toBeTruthy(); expect(validated!.expiresAt!.getTime()).toBeGreaterThan(Date.now()); }); // ============================================================================ // Token Validation Tests // ============================================================================ tap.test('validateToken: should validate UUID token (NPM, Maven, etc.)', async () => { const npmToken = await provider.createToken('validateuser', 'npm'); const validated = await provider.validateToken(npmToken); expect(validated).toBeTruthy(); expect(validated!.type).toEqual('npm'); expect(validated!.userId).toEqual('validateuser'); }); tap.test('validateToken: should validate OCI JWT token', async () => { const ociToken = await provider.createToken('ocivalidate', 'oci', { scopes: ['oci:repository:*:*'], }); const validated = await provider.validateToken(ociToken, 'oci'); expect(validated).toBeTruthy(); expect(validated!.type).toEqual('oci'); expect(validated!.userId).toEqual('ocivalidate'); }); tap.test('validateToken: should reject expired tokens', async () => { const token = await provider.createToken('expireduser', 'npm', { expiresIn: -1, // Already expired (in the past) }); // The token should be created but will fail validation due to expiry const validated = await provider.validateToken(token, 'npm'); // Token should be rejected because it's expired expect(validated).toBeNull(); }); tap.test('validateToken: should reject invalid token', async () => { const validated = await provider.validateToken('invalid-random-token'); expect(validated).toBeNull(); }); tap.test('validateToken: should reject token with wrong protocol', async () => { const npmToken = await provider.createToken('protocoluser', 'npm'); // Try to validate as Maven token const validated = await provider.validateToken(npmToken, 'maven'); expect(validated).toBeNull(); }); // ============================================================================ // Token Revocation Tests // ============================================================================ tap.test('revokeToken: should revoke tokens', async () => { const token = await provider.createToken('revokeuser', 'npm'); // Verify token works before revocation let validated = await provider.validateToken(token); expect(validated).toBeTruthy(); // Revoke the token await provider.revokeToken(token); // Token should no longer be valid validated = await provider.validateToken(token); expect(validated).toBeNull(); }); // ============================================================================ // Authorization Tests // ============================================================================ tap.test('authorize: should authorize read actions for readonly tokens', async () => { const token = await provider.createToken('readonlyuser', 'npm', { readonly: true, scopes: ['npm:package:*:read'], }); const validated = await provider.validateToken(token); const canRead = await provider.authorize(validated, 'npm:package:lodash', 'read'); expect(canRead).toBeTrue(); const canPull = await provider.authorize(validated, 'npm:package:lodash', 'pull'); expect(canPull).toBeTrue(); }); tap.test('authorize: should deny write actions for readonly tokens', async () => { const token = await provider.createToken('readonlyuser2', 'npm', { readonly: true, scopes: ['npm:package:*:*'], }); const validated = await provider.validateToken(token); const canWrite = await provider.authorize(validated, 'npm:package:lodash', 'write'); expect(canWrite).toBeFalse(); const canPush = await provider.authorize(validated, 'npm:package:lodash', 'push'); expect(canPush).toBeFalse(); const canDelete = await provider.authorize(validated, 'npm:package:lodash', 'delete'); expect(canDelete).toBeFalse(); }); tap.test('authorize: should match scopes with wildcards', async () => { // The scope system uses literal * as wildcard, not glob patterns // npm:*:*:* means "all types, all names, all actions under npm" const token = await provider.createToken('wildcarduser', 'npm', { scopes: ['npm:*:*:*'], }); const validated = await provider.validateToken(token); // Should match any npm resource with full wildcard scope const canAccessAnyPackage = await provider.authorize(validated, 'npm:package:lodash', 'read'); expect(canAccessAnyPackage).toBeTrue(); const canAccessScopedPackage = await provider.authorize(validated, 'npm:package:@myorg/foo', 'write'); expect(canAccessScopedPackage).toBeTrue(); }); tap.test('authorize: should deny access with null token', async () => { const canAccess = await provider.authorize(null, 'npm:package:lodash', 'read'); expect(canAccess).toBeFalse(); }); // ============================================================================ // List Tokens Tests // ============================================================================ tap.test('listUserTokens: should list user tokens', async () => { // Create multiple tokens for the same user const userId = 'listtokenuser'; await provider.createToken(userId, 'npm'); await provider.createToken(userId, 'maven', { readonly: true }); await provider.createToken(userId, 'cargo'); const tokens = await provider.listUserTokens!(userId); expect(tokens.length).toBeGreaterThanOrEqual(3); // Check that tokens have expected properties for (const token of tokens) { expect(token.key).toBeTruthy(); expect(typeof token.readonly).toEqual('boolean'); expect(token.created).toBeTruthy(); } // Verify we have different protocols const protocols = tokens.map(t => t.protocol); expect(protocols).toContain('npm'); expect(protocols).toContain('maven'); expect(protocols).toContain('cargo'); }); // ============================================================================ // AuthManager Integration Tests // ============================================================================ tap.test('AuthManager: should accept custom IAuthProvider', async () => { const mockProvider = createMockAuthProvider({ authenticate: async (credentials) => { if (credentials.username === 'custom' && credentials.password === 'pass') { return 'custom-user-id'; } return null; }, }); const manager = new AuthManager(authConfig, mockProvider); await manager.init(); // Use the custom provider const userId = await manager.authenticate({ username: 'custom', password: 'pass', }); expect(userId).toEqual('custom-user-id'); // Wrong credentials should fail const failed = await manager.authenticate({ username: 'custom', password: 'wrong', }); expect(failed).toBeNull(); }); tap.test('AuthManager: should use default provider when none specified', async () => { const manager = new AuthManager(authConfig); await manager.init(); // Should use DefaultAuthProvider internally const userId = await manager.authenticate({ username: 'defaultuser', password: 'defaultpass', }); expect(userId).toEqual('defaultuser'); }); tap.test('AuthManager: should delegate token creation to provider', async () => { let tokenCreated = false; const mockProvider = createMockAuthProvider({ createToken: async (userId, protocol, options) => { tokenCreated = true; return `mock-token-${protocol}-${userId}`; }, }); const manager = new AuthManager(authConfig, mockProvider); await manager.init(); const token = await manager.createNpmToken('delegateuser', false); expect(tokenCreated).toBeTrue(); expect(token).toContain('mock-token-npm'); }); // ============================================================================ // Edge Cases // ============================================================================ tap.test('edge: should handle concurrent token operations', async () => { const promises: Promise[] = []; // Create 10 tokens concurrently for (let i = 0; i < 10; i++) { promises.push(provider.createToken(`concurrent-user-${i}`, 'npm')); } const tokens = await Promise.all(promises); // All tokens should be unique const uniqueTokens = new Set(tokens); expect(uniqueTokens.size).toEqual(10); // All tokens should be valid for (const token of tokens) { const validated = await provider.validateToken(token); expect(validated).toBeTruthy(); } }); tap.test('edge: should handle empty scopes', async () => { const token = await provider.createToken('emptyuser', 'npm', { scopes: [], }); const validated = await provider.validateToken(token); expect(validated).toBeTruthy(); // Even with empty scopes, token should be valid }); // ============================================================================ // Cleanup // ============================================================================ tap.test('cleanup', async () => { // No cleanup needed for in-memory provider }); export default tap.start();