chore: resolve conflicts

This commit is contained in:
Ralph Khreish
2025-09-04 21:00:14 +02:00
parent 91b5f8186e
commit 70ef1298db
3 changed files with 71 additions and 43 deletions

View File

@@ -3,16 +3,18 @@
*/ */
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AuthManager } from './auth-manager'; import { AuthManager } from './auth-manager.js';
// Mock the logger to verify warnings // Mock the logger to verify warnings
vi.mock('../logger', () => ({ const mockLogger = {
getLogger: () => ({ warn: vi.fn(),
warn: vi.fn(), info: vi.fn(),
info: vi.fn(), debug: vi.fn(),
debug: vi.fn(), error: vi.fn()
error: vi.fn() };
})
vi.mock('../logger/index.js', () => ({
getLogger: () => mockLogger
})); }));
describe('AuthManager Singleton', () => { describe('AuthManager Singleton', () => {
@@ -24,59 +26,61 @@ describe('AuthManager Singleton', () => {
it('should return the same instance on multiple calls', () => { it('should return the same instance on multiple calls', () => {
const instance1 = AuthManager.getInstance(); const instance1 = AuthManager.getInstance();
const instance2 = AuthManager.getInstance(); const instance2 = AuthManager.getInstance();
expect(instance1).toBe(instance2); expect(instance1).toBe(instance2);
}); });
it('should use config on first call', () => { it('should use config on first call', () => {
const config = { const config = {
baseUrl: 'https://test.auth.com', baseUrl: 'https://test.auth.com',
configDir: '/test/config', configDir: '/test/config',
configFile: '/test/config/auth.json' configFile: '/test/config/auth.json'
}; };
const instance = AuthManager.getInstance(config); const instance = AuthManager.getInstance(config);
expect(instance).toBeDefined(); expect(instance).toBeDefined();
}); });
it('should warn when config is provided after initialization', () => { it('should warn when config is provided after initialization', () => {
const logger = vi.mocked(require('../logger').getLogger()); // Clear previous calls
mockLogger.warn.mockClear();
// First call with config // First call with config
AuthManager.getInstance({ baseUrl: 'https://first.auth.com' }); AuthManager.getInstance({ baseUrl: 'https://first.auth.com' });
// Second call with different config // Second call with different config
AuthManager.getInstance({ baseUrl: 'https://second.auth.com' }); AuthManager.getInstance({ baseUrl: 'https://second.auth.com' });
// Verify warning was logged // Verify warning was logged
expect(logger.warn).toHaveBeenCalledWith( expect(mockLogger.warn).toHaveBeenCalledWith(
'getInstance called with config after initialization; config is ignored.' 'getInstance called with config after initialization; config is ignored.'
); );
}); });
it('should not warn when no config is provided after initialization', () => { it('should not warn when no config is provided after initialization', () => {
const logger = vi.mocked(require('../logger').getLogger()); // Clear previous calls
mockLogger.warn.mockClear();
// First call with config // First call with config
AuthManager.getInstance({ configDir: '/test/config' }); AuthManager.getInstance({ configDir: '/test/config' });
// Second call without config // Second call without config
AuthManager.getInstance(); AuthManager.getInstance();
// Verify no warning was logged // Verify no warning was logged
expect(logger.warn).not.toHaveBeenCalled(); expect(mockLogger.warn).not.toHaveBeenCalled();
}); });
it('should allow resetting the instance', () => { it('should allow resetting the instance', () => {
const instance1 = AuthManager.getInstance(); const instance1 = AuthManager.getInstance();
// Reset the instance // Reset the instance
AuthManager.resetInstance(); AuthManager.resetInstance();
// Get new instance // Get new instance
const instance2 = AuthManager.getInstance(); const instance2 = AuthManager.getInstance();
// They should be different instances // They should be different instances
expect(instance1).not.toBe(instance2); expect(instance1).not.toBe(instance2);
}); });
}); });

View File

@@ -7,10 +7,11 @@ import {
OAuthFlowOptions, OAuthFlowOptions,
AuthenticationError, AuthenticationError,
AuthConfig AuthConfig
} from './types'; } from './types.js';
import { CredentialStore } from './credential-store'; import { CredentialStore } from './credential-store.js';
import { OAuthService } from './oauth-service'; import { OAuthService } from './oauth-service.js';
import { SupabaseAuthClient } from '../clients/supabase-client'; import { SupabaseAuthClient } from '../clients/supabase-client.js';
import { getLogger } from '../logger/index.js';
/** /**
* Authentication manager class * Authentication manager class
@@ -33,10 +34,23 @@ export class AuthManager {
static getInstance(config?: Partial<AuthConfig>): AuthManager { static getInstance(config?: Partial<AuthConfig>): AuthManager {
if (!AuthManager.instance) { if (!AuthManager.instance) {
AuthManager.instance = new AuthManager(config); AuthManager.instance = new AuthManager(config);
} else if (config) {
// Warn if config is provided after initialization
const logger = getLogger('AuthManager');
logger.warn(
'getInstance called with config after initialization; config is ignored.'
);
} }
return AuthManager.instance; return AuthManager.instance;
} }
/**
* Reset the singleton instance (useful for testing)
*/
static resetInstance(): void {
AuthManager.instance = null as any;
}
/** /**
* Get stored authentication credentials * Get stored authentication credentials
*/ */

View File

@@ -4,9 +4,9 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { AuthCredentials, AuthenticationError, AuthConfig } from './types'; import { AuthCredentials, AuthenticationError, AuthConfig } from './types.js';
import { getAuthConfig } from './config'; import { getAuthConfig } from './config.js';
import { getLogger } from '../logger'; import { getLogger } from '../logger/index.js';
export class CredentialStore { export class CredentialStore {
private logger = getLogger('CredentialStore'); private logger = getLogger('CredentialStore');
@@ -29,17 +29,16 @@ export class CredentialStore {
fs.readFileSync(this.config.configFile, 'utf-8') fs.readFileSync(this.config.configFile, 'utf-8')
) as AuthCredentials; ) as AuthCredentials;
// Normalize/migrate timestamps to numeric (handles both string ISO dates and numbers) // Parse expiration time for validation (expects ISO string format)
const expiresAtMs = let expiresAtMs: number | undefined;
typeof authData.expiresAt === 'number'
? authData.expiresAt if (authData.expiresAt) {
: authData.expiresAt expiresAtMs = Date.parse(authData.expiresAt);
? Date.parse(authData.expiresAt as unknown as string) if (isNaN(expiresAtMs)) {
: undefined; // Invalid date string - treat as expired
this.logger.error(`Invalid expiresAt format: ${authData.expiresAt}`);
// Update the authData with normalized timestamp return null;
if (expiresAtMs !== undefined) { }
authData.expiresAt = expiresAtMs;
} }
// Check if token is expired (API keys never expire) // Check if token is expired (API keys never expire)
@@ -90,6 +89,17 @@ export class CredentialStore {
// Add timestamp // Add timestamp
authData.savedAt = new Date().toISOString(); authData.savedAt = new Date().toISOString();
// Validate expiresAt is a valid ISO string if present
if (authData.expiresAt) {
const ms = Date.parse(authData.expiresAt);
if (isNaN(ms)) {
throw new AuthenticationError(
`Invalid expiresAt format: ${authData.expiresAt}`,
'SAVE_FAILED'
);
}
}
// Save credentials atomically with secure permissions // Save credentials atomically with secure permissions
const tempFile = `${this.config.configFile}.tmp`; const tempFile = `${this.config.configFile}.tmp`;