chore: resolve conflicts
This commit is contained in:
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
Reference in New Issue
Block a user