import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TokenStorage, TokenInfo } from '../../src/auth/token-storage.js';
import os from 'os';
import fs from 'fs/promises';
// fsモジュールをモック
vi.mock('fs/promises', () => ({
default: {
mkdir: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
unlink: vi.fn(),
access: vi.fn(),
},
}));
// osモジュールのhomedirをスパイ
vi.spyOn(os, 'homedir').mockReturnValue('/mock/home');
describe('TokenStorage', () => {
let tokenStorage: TokenStorage;
const mockFs = fs as any;
beforeEach(() => {
tokenStorage = new TokenStorage();
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('saveToken', () => {
it('トークン情報を正しくファイルに保存する', async () => {
const tokenInfo: TokenInfo = {
token: 'test-token-123',
baseUrl: 'https://api.example.com',
email: 'test@example.com',
};
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
await tokenStorage.saveToken(tokenInfo);
// ディレクトリ作成を確認
expect(mockFs.mkdir).toHaveBeenCalledWith(
'/mock/home/.verge-mcp',
{ recursive: true, mode: 0o700 }
);
// ファイル書き込みを確認
expect(mockFs.writeFile).toHaveBeenCalledWith(
'/mock/home/.verge-mcp/token.json',
JSON.stringify(tokenInfo, null, 2),
{ mode: 0o600 }
);
});
it('有効期限付きトークンを正しく保存する', async () => {
const expiresAt = Date.now() + 86400000; // 24時間後
const tokenInfo: TokenInfo = {
token: 'test-token-456',
baseUrl: 'https://api.example.com',
email: 'test@example.com',
expiresAt,
};
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
await tokenStorage.saveToken(tokenInfo);
const savedData = JSON.parse(
mockFs.writeFile.mock.calls[0][1]
);
expect(savedData).toEqual(tokenInfo);
expect(savedData.expiresAt).toBe(expiresAt);
});
it('ディレクトリ作成に失敗した場合はエラーをスローする', async () => {
const tokenInfo: TokenInfo = {
token: 'test-token',
baseUrl: 'https://api.example.com',
email: 'test@example.com',
};
mockFs.mkdir.mockRejectedValue(new Error('Permission denied'));
await expect(tokenStorage.saveToken(tokenInfo)).rejects.toThrow(
'Failed to save token: Permission denied'
);
});
it('ファイル書き込みに失敗した場合はエラーをスローする', async () => {
const tokenInfo: TokenInfo = {
token: 'test-token',
baseUrl: 'https://api.example.com',
email: 'test@example.com',
};
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockRejectedValue(new Error('Disk full'));
await expect(tokenStorage.saveToken(tokenInfo)).rejects.toThrow(
'Failed to save token: Disk full'
);
});
});
describe('loadToken', () => {
it('保存されたトークンを正しく読み込む', async () => {
const tokenInfo: TokenInfo = {
token: 'saved-token-789',
baseUrl: 'https://api.example.com',
email: 'saved@example.com',
};
mockFs.readFile.mockResolvedValue(JSON.stringify(tokenInfo));
const result = await tokenStorage.loadToken();
expect(mockFs.readFile).toHaveBeenCalledWith(
'/mock/home/.verge-mcp/token.json',
'utf-8'
);
expect(result).toEqual(tokenInfo);
});
it('有効期限内のトークンを正しく読み込む', async () => {
const tokenInfo: TokenInfo = {
token: 'valid-token',
baseUrl: 'https://api.example.com',
email: 'valid@example.com',
expiresAt: Date.now() + 86400000, // 24時間後
};
mockFs.readFile.mockResolvedValue(JSON.stringify(tokenInfo));
const result = await tokenStorage.loadToken();
expect(result).toEqual(tokenInfo);
});
it('有効期限切れのトークンは削除してnullを返す', async () => {
const expiredTokenInfo: TokenInfo = {
token: 'expired-token',
baseUrl: 'https://api.example.com',
email: 'expired@example.com',
expiresAt: Date.now() - 1000, // 1秒前(期限切れ)
};
mockFs.readFile.mockResolvedValue(JSON.stringify(expiredTokenInfo));
mockFs.unlink.mockResolvedValue(undefined);
const result = await tokenStorage.loadToken();
expect(result).toBeNull();
expect(mockFs.unlink).toHaveBeenCalledWith(
'/mock/home/.verge-mcp/token.json'
);
});
it('ファイルが存在しない場合はnullを返す', async () => {
const error: any = new Error('File not found');
error.code = 'ENOENT';
mockFs.readFile.mockRejectedValue(error);
const result = await tokenStorage.loadToken();
expect(result).toBeNull();
});
it('ファイル読み込みエラー(ENOENT以外)はエラーをスローする', async () => {
mockFs.readFile.mockRejectedValue(new Error('Permission denied'));
await expect(tokenStorage.loadToken()).rejects.toThrow(
'Failed to load token: Permission denied'
);
});
it('不正なJSONフォーマットの場合はエラーをスローする', async () => {
mockFs.readFile.mockResolvedValue('invalid json {]');
await expect(tokenStorage.loadToken()).rejects.toThrow();
});
});
describe('deleteToken', () => {
it('トークンファイルを正しく削除する', async () => {
mockFs.unlink.mockResolvedValue(undefined);
await tokenStorage.deleteToken();
expect(mockFs.unlink).toHaveBeenCalledWith(
'/mock/home/.verge-mcp/token.json'
);
});
it('ファイルが存在しない場合はエラーをスローしない', async () => {
const error: any = new Error('File not found');
error.code = 'ENOENT';
mockFs.unlink.mockRejectedValue(error);
await expect(tokenStorage.deleteToken()).resolves.toBeUndefined();
});
it('削除エラー(ENOENT以外)はエラーをスローする', async () => {
mockFs.unlink.mockRejectedValue(new Error('Permission denied'));
await expect(tokenStorage.deleteToken()).rejects.toThrow(
'Failed to delete token: Permission denied'
);
});
});
describe('hasToken', () => {
it('トークンファイルが存在する場合はtrueを返す', async () => {
mockFs.access.mockResolvedValue(undefined);
const result = await tokenStorage.hasToken();
expect(mockFs.access).toHaveBeenCalledWith(
'/mock/home/.verge-mcp/token.json'
);
expect(result).toBe(true);
});
it('トークンファイルが存在しない場合はfalseを返す', async () => {
mockFs.access.mockRejectedValue(new Error('File not found'));
const result = await tokenStorage.hasToken();
expect(result).toBe(false);
});
it('アクセス権限がない場合はfalseを返す', async () => {
const error: any = new Error('Permission denied');
error.code = 'EACCES';
mockFs.access.mockRejectedValue(error);
const result = await tokenStorage.hasToken();
expect(result).toBe(false);
});
});
describe('パスの構築', () => {
it('正しいトークンディレクトリパスを使用する', () => {
// TokenStorageのprivateフィールドにアクセスするため、anyにキャスト
const storage = tokenStorage as any;
expect(storage.tokenDir).toBe('/mock/home/.verge-mcp');
expect(storage.tokenFile).toBe('/mock/home/.verge-mcp/token.json');
});
});
});