feat(core): Add core registry infrastructure: storage, auth, upstream cache, and protocol handlers
This commit is contained in:
412
test/test.auth.provider.ts
Normal file
412
test/test.auth.provider.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
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<string>[] = [];
|
||||
|
||||
// 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();
|
||||
Reference in New Issue
Block a user