import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AuthManager } from '../../src/auth/index.js';
// モックインスタンスを保持する変数
const mockTokenStorage = {
loadToken: vi.fn().mockResolvedValue(null),
saveToken: vi.fn().mockResolvedValue(undefined),
deleteToken: vi.fn().mockResolvedValue(undefined),
};
const mockBrowserAuth = {
authenticate: vi.fn().mockResolvedValue({
token: 'browser-token',
email: 'browser@example.com',
}),
};
const mockVergeApiClient = {
login: vi.fn().mockResolvedValue({
token: 'api-token',
user: { id: 1, email: 'api@example.com', name: 'API User' },
}),
setToken: vi.fn(),
getCurrentUser: vi.fn().mockResolvedValue({
id: 1,
email: 'user@example.com',
name: 'User',
}),
};
// TokenStorageをモック
vi.mock('../../src/auth/token-storage.js', () => ({
TokenStorage: vi.fn().mockImplementation(() => mockTokenStorage),
}));
// BrowserAuthをモック
vi.mock('../../src/auth/browser-auth.js', () => ({
BrowserAuth: vi.fn().mockImplementation(() => mockBrowserAuth),
}));
// VergeApiClientをモック
vi.mock('../../src/client/api.js', () => ({
VergeApiClient: vi.fn().mockImplementation(() => mockVergeApiClient),
}));
describe('AuthManager', () => {
let authManager: AuthManager;
const baseUrl = 'https://api.example.com';
beforeEach(async () => {
// 環境変数をクリア
delete process.env.VERGE_EMAIL;
delete process.env.VERGE_PASSWORD;
// コンソール出力をモック
vi.spyOn(console, 'error').mockImplementation(() => {});
// モック関数の呼び出し履歴をクリア
mockTokenStorage.loadToken.mockClear();
mockTokenStorage.saveToken.mockClear();
mockTokenStorage.deleteToken.mockClear();
mockBrowserAuth.authenticate.mockClear();
mockVergeApiClient.login.mockClear();
mockVergeApiClient.setToken.mockClear();
mockVergeApiClient.getCurrentUser.mockClear();
// AuthManagerインスタンスを作成(モックされたインスタンスを渡す)
authManager = new AuthManager(
baseUrl,
mockTokenStorage as any,
mockBrowserAuth as any,
mockVergeApiClient as any
);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('authenticate', () => {
it('保存されたトークンが有効な場合はそれを使用する', async () => {
mockTokenStorage.loadToken.mockResolvedValue({
token: 'saved-token-123',
baseUrl,
email: 'saved@example.com',
});
const result = await authManager.authenticate();
expect(mockTokenStorage.loadToken).toHaveBeenCalled();
expect(result).toEqual({
token: 'saved-token-123',
email: 'saved@example.com',
baseUrl,
});
// BrowserAuthは呼ばれない
expect(mockBrowserAuth.authenticate).not.toHaveBeenCalled();
});
it('baseUrlが異なる場合は保存トークンを使用しない', async () => {
mockTokenStorage.loadToken.mockResolvedValue({
token: 'saved-token-123',
baseUrl: 'https://different-api.example.com', // 異なるbaseUrl
email: 'saved@example.com',
});
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'browser-token-456',
email: 'browser@example.com',
});
const result = await authManager.authenticate();
// ブラウザ認証が呼ばれる
expect(mockBrowserAuth.authenticate).toHaveBeenCalled();
expect(result).toEqual({
token: 'browser-token-456',
email: 'browser@example.com',
baseUrl,
});
});
it('環境変数が設定されている場合はそれを使用してログインする', async () => {
mockTokenStorage.loadToken.mockResolvedValue(null);
process.env.VERGE_EMAIL = 'env@example.com';
process.env.VERGE_PASSWORD = 'env-password';
mockVergeApiClient.login.mockResolvedValue({
token: 'env-token-789',
user: { id: 1, email: 'env@example.com', name: 'Env User' },
});
const result = await authManager.authenticate();
expect(mockVergeApiClient.login).toHaveBeenCalledWith(
'env@example.com',
'env-password'
);
expect(mockTokenStorage.saveToken).toHaveBeenCalledWith(
expect.objectContaining({
token: 'env-token-789',
baseUrl,
email: 'env@example.com',
})
);
// expiresAtが設定されていることを確認
const savedTokenInfo = mockTokenStorage.saveToken.mock.calls[0][0];
expect(savedTokenInfo.expiresAt).toBeDefined();
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;
const expectedMinExpiry = Date.now() + sevenDaysInMs - 1000;
const expectedMaxExpiry = Date.now() + sevenDaysInMs + 1000;
expect(savedTokenInfo.expiresAt).toBeGreaterThanOrEqual(expectedMinExpiry);
expect(savedTokenInfo.expiresAt).toBeLessThanOrEqual(expectedMaxExpiry);
expect(result).toEqual({
token: 'env-token-789',
email: 'env@example.com',
baseUrl,
});
});
it('環境変数でのログインに失敗した場合はブラウザ認証にフォールバックする', async () => {
mockTokenStorage.loadToken.mockResolvedValue(null);
process.env.VERGE_EMAIL = 'env@example.com';
process.env.VERGE_PASSWORD = 'wrong-password';
mockVergeApiClient.login.mockRejectedValue(new Error('Invalid credentials'));
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'browser-token-fallback',
email: 'browser@example.com',
});
const result = await authManager.authenticate();
// 環境変数でのログインが試みられる
expect(mockVergeApiClient.login).toHaveBeenCalled();
// ブラウザ認証にフォールバック
expect(mockBrowserAuth.authenticate).toHaveBeenCalled();
expect(result).toEqual({
token: 'browser-token-fallback',
email: 'browser@example.com',
baseUrl,
});
});
it('保存トークンがなく環境変数もない場合はブラウザ認証を使用する', async () => {
mockTokenStorage.loadToken.mockResolvedValue(null);
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'browser-token-only',
email: 'browser@example.com',
});
const result = await authManager.authenticate();
expect(mockBrowserAuth.authenticate).toHaveBeenCalled();
expect(result).toEqual({
token: 'browser-token-only',
email: 'browser@example.com',
baseUrl,
});
});
it('環境変数のメールアドレスのみが設定されている場合はブラウザ認証を使用する', async () => {
mockTokenStorage.loadToken.mockResolvedValue(null);
process.env.VERGE_EMAIL = 'env@example.com';
// VERGE_PASSWORDは設定しない
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'browser-token',
email: 'browser@example.com',
});
const result = await authManager.authenticate();
// 環境変数ログインはスキップされる
expect(mockVergeApiClient.login).not.toHaveBeenCalled();
// ブラウザ認証が使用される
expect(mockBrowserAuth.authenticate).toHaveBeenCalled();
expect(result).toEqual({
token: 'browser-token',
email: 'browser@example.com',
baseUrl,
});
});
});
describe('logout', () => {
it('保存されたトークンを削除する', async () => {
mockTokenStorage.deleteToken.mockResolvedValue(undefined);
await authManager.logout();
expect(mockTokenStorage.deleteToken).toHaveBeenCalled();
});
it('ログアウト成功メッセージを表示する', async () => {
mockTokenStorage.deleteToken.mockResolvedValue(undefined);
const consoleErrorSpy = vi.spyOn(console, 'error');
await authManager.logout();
expect(consoleErrorSpy).toHaveBeenCalledWith('✅ ログアウトしました');
});
});
describe('validateToken', () => {
it('有効なトークンの場合はtrueを返す', async () => {
mockVergeApiClient.getCurrentUser.mockResolvedValue({
id: 1,
email: 'valid@example.com',
name: 'Valid User',
});
const result = await authManager.validateToken('valid-token-123');
expect(mockVergeApiClient.setToken).toHaveBeenCalledWith('valid-token-123');
expect(mockVergeApiClient.getCurrentUser).toHaveBeenCalled();
expect(result).toBe(true);
});
it('無効なトークンの場合はfalseを返す', async () => {
mockVergeApiClient.getCurrentUser.mockRejectedValue(new Error('Unauthorized'));
const result = await authManager.validateToken('invalid-token');
expect(mockVergeApiClient.setToken).toHaveBeenCalledWith('invalid-token');
expect(mockVergeApiClient.getCurrentUser).toHaveBeenCalled();
expect(result).toBe(false);
});
it('ネットワークエラーの場合はfalseを返す', async () => {
mockVergeApiClient.getCurrentUser.mockRejectedValue(new Error('Network error'));
const result = await authManager.validateToken('some-token');
expect(result).toBe(false);
});
});
describe('認証フローの優先順位', () => {
it('優先順位: 保存トークン > 環境変数 > ブラウザ認証', async () => {
// 保存トークンが有効な場合
mockTokenStorage.loadToken.mockResolvedValue({
token: 'saved-token',
baseUrl,
email: 'saved@example.com',
});
process.env.VERGE_EMAIL = 'env@example.com';
process.env.VERGE_PASSWORD = 'env-password';
const result = await authManager.authenticate();
// 保存トークンが最優先
expect(result.token).toBe('saved-token');
// 環境変数ログインは試みられない
expect(mockVergeApiClient.login).not.toHaveBeenCalled();
// ブラウザ認証は試みられない
expect(mockBrowserAuth.authenticate).not.toHaveBeenCalled();
});
});
describe('エラーハンドリング', () => {
it('TokenStorageのloadTokenエラーはキャッチして次の認証方法に進む', async () => {
mockTokenStorage.loadToken.mockRejectedValue(new Error('Storage error'));
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'browser-token',
email: 'browser@example.com',
});
// エラーがスローされずにブラウザ認証にフォールバックする
const result = await authManager.authenticate();
expect(result.token).toBe('browser-token');
});
});
describe('トークン検証と自動再認証', () => {
it('保存トークンが無効な場合は削除してブラウザ認証にフォールバックする', async () => {
mockTokenStorage.loadToken.mockResolvedValue({
token: 'invalid-saved-token',
baseUrl,
email: 'saved@example.com',
});
// トークン検証が失敗
mockVergeApiClient.getCurrentUser.mockRejectedValue(new Error('Unauthorized'));
mockBrowserAuth.authenticate.mockResolvedValue({
token: 'new-browser-token',
email: 'browser@example.com',
});
const result = await authManager.authenticate();
// トークン検証が試みられる
expect(mockVergeApiClient.setToken).toHaveBeenCalledWith('invalid-saved-token');
expect(mockVergeApiClient.getCurrentUser).toHaveBeenCalled();
// 無効なトークンは削除される
expect(mockTokenStorage.deleteToken).toHaveBeenCalled();
// ブラウザ認証にフォールバック
expect(mockBrowserAuth.authenticate).toHaveBeenCalled();
expect(result).toEqual({
token: 'new-browser-token',
email: 'browser@example.com',
baseUrl,
});
});
it('保存トークンがAPI検証で有効な場合はそのまま使用する', async () => {
mockTokenStorage.loadToken.mockResolvedValue({
token: 'valid-saved-token',
baseUrl,
email: 'saved@example.com',
});
// トークン検証が成功
mockVergeApiClient.getCurrentUser.mockResolvedValue({
id: 1,
email: 'saved@example.com',
name: 'Saved User',
});
const result = await authManager.authenticate();
// トークン検証が試みられる
expect(mockVergeApiClient.setToken).toHaveBeenCalledWith('valid-saved-token');
expect(mockVergeApiClient.getCurrentUser).toHaveBeenCalled();
// ブラウザ認証は呼ばれない
expect(mockBrowserAuth.authenticate).not.toHaveBeenCalled();
expect(result).toEqual({
token: 'valid-saved-token',
email: 'saved@example.com',
baseUrl,
});
});
it('保存トークンが無効で環境変数がある場合は環境変数でログインする', async () => {
mockTokenStorage.loadToken.mockResolvedValue({
token: 'invalid-saved-token',
baseUrl,
email: 'saved@example.com',
});
// トークン検証が失敗
mockVergeApiClient.getCurrentUser.mockRejectedValueOnce(new Error('Unauthorized'));
process.env.VERGE_EMAIL = 'env@example.com';
process.env.VERGE_PASSWORD = 'env-password';
mockVergeApiClient.login.mockResolvedValue({
token: 'env-token',
user: { id: 1, email: 'env@example.com', name: 'Env User' },
});
const result = await authManager.authenticate();
// 無効なトークンは削除される
expect(mockTokenStorage.deleteToken).toHaveBeenCalled();
// 環境変数でログインが試みられる
expect(mockVergeApiClient.login).toHaveBeenCalledWith(
'env@example.com',
'env-password'
);
expect(result).toEqual({
token: 'env-token',
email: 'env@example.com',
baseUrl,
});
});
});
});