UNPKG

dnsweeper

Version:

Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.

421 lines (331 loc) 13.1 kB
/** * import コマンドのユニットテスト */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { readFile, writeFile, unlink } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; // コマンド実行のテスト用モック const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); describe('import command', () => { let tempFiles: string[] = []; beforeEach(() => { vi.clearAllMocks(); mockExit.mockClear(); mockConsoleLog.mockClear(); mockConsoleError.mockClear(); }); afterEach(async () => { // テスト用一時ファイルをクリーンアップ for (const file of tempFiles) { try { if (existsSync(file)) { await unlink(file); } } catch { // ファイルが存在しない場合は無視 } } tempFiles = []; vi.restoreAllMocks(); }); const createTempCsvFile = async (content: string): Promise<string> => { const filePath = join(tmpdir(), `test-import-${Date.now()}-${Math.random().toString(36).substring(2)}.csv`); tempFiles.push(filePath); await writeFile(filePath, content, 'utf-8'); return filePath; }; describe('CSV ファイル検証', () => { it('有効なCloudflare形式のCSVファイルを処理', async () => { const csvContent = `Name,Type,Content,TTL,Priority www.example.com,A,192.0.2.1,300, mail.example.com,MX,mail.example.com,300,10 example.com,TXT,"v=spf1 include:_spf.google.com ~all",300,`; const filePath = await createTempCsvFile(csvContent); // import コマンドの動的インポート const { importCommand } = await import('../../src/commands/import.js'); // コマンド実行(実際のAPIコールは行わない) const mockProgram = { name: () => mockProgram, description: () => mockProgram, argument: () => mockProgram, option: () => mockProgram, action: (fn: Function) => { // アクション関数をテスト用に実行 return fn(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); } }; await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); // エラーなく処理されることを確認 expect(mockExit).not.toHaveBeenCalled(); }); it('無効なCSVファイルでエラー', async () => { const invalidCsvContent = `Invalid,CSV,Format missing,required,columns`; const filePath = await createTempCsvFile(invalidCsvContent); const { importCommand } = await import('../../src/commands/import.js'); await expect(async () => { await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); }).rejects.toThrow(); }); it('存在しないファイルでエラー', async () => { const nonExistentFile = '/non/existent/file.csv'; const { importCommand } = await import('../../src/commands/import.js'); await expect(async () => { await importCommand.action(nonExistentFile, { format: 'cloudflare', dryRun: true, verbose: false }); }).rejects.toThrow(); }); }); describe('Route53形式の処理', () => { it('有効なRoute53形式のCSVファイルを処理', async () => { const csvContent = `Name,Type,Value,TTL,Weight,SetIdentifier www.example.com,A,192.0.2.1,300,, mail.example.com,MX,"10 mail.example.com",300,, example.com,TXT,"v=spf1 include:_spf.google.com ~all",300,,`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'route53', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); }); describe('汎用形式の処理', () => { it('有効な汎用形式のCSVファイルを処理', async () => { const csvContent = `domain,record_type,value,ttl,priority,weight,port www.example.com,A,192.0.2.1,300,,, mail.example.com,MX,mail.example.com,300,10,, _sip._tcp.example.com,SRV,sip.example.com,300,10,20,5060`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'generic', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); }); describe('オプション処理', () => { it('dry-runモードで実際のインポートは実行しない', async () => { const csvContent = `Name,Type,Content,TTL,Priority test.example.com,A,192.0.2.1,300,`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: true }); // dry-runではAPIコールが実行されないことを確認 expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Dry run mode') ); }); it('verboseモードで詳細情報を出力', async () => { const csvContent = `Name,Type,Content,TTL,Priority verbose.example.com,A,192.0.2.1,300,`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: true }); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Processing') ); }); it('フォーマット自動検出', async () => { const csvContent = `Name,Type,Content,TTL,Priority auto.example.com,A,192.0.2.1,300,`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'auto', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); }); describe('大容量ファイル処理', () => { it('大容量CSVファイルをストリーミング処理', async () => { // 1000行のCSVファイルを生成 const headers = 'Name,Type,Content,TTL,Priority'; const rows = Array.from({ length: 1000 }, (_, i) => `test${i}.example.com,A,192.0.2.${Math.floor(i / 254) + 1},300,` ); const csvContent = [headers, ...rows].join('\n'); const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); const start = Date.now(); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); const duration = Date.now() - start; expect(duration).toBeLessThan(5000); // 5秒以内 expect(mockExit).not.toHaveBeenCalled(); }, 10000); }); describe('エラーハンドリング', () => { it('不正なレコードを含むCSVファイルのエラーハンドリング', async () => { const csvContent = `Name,Type,Content,TTL,Priority valid.example.com,A,192.0.2.1,300, invalid.example.com,A,invalid-ip-address,300, another.example.com,A,192.0.2.2,300,`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: true }); // 有効なレコードは処理され、無効なレコードはスキップされることを確認 expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Processing') ); }); it('空のCSVファイル', async () => { const csvContent = `Name,Type,Content,TTL,Priority`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); it('権限がないファイル', async () => { // 実際の権限テストは環境依存のため、ファイル名のみテスト const restrictedFile = '/root/restricted.csv'; const { importCommand } = await import('../../src/commands/import.js'); await expect(async () => { await importCommand.action(restrictedFile, { format: 'cloudflare', dryRun: true, verbose: false }); }).rejects.toThrow(); }); }); describe('進捗表示', () => { it('進捗情報を適切に表示', async () => { const headers = 'Name,Type,Content,TTL,Priority'; const rows = Array.from({ length: 100 }, (_, i) => `progress${i}.example.com,A,192.0.2.1,300,` ); const csvContent = [headers, ...rows].join('\n'); const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: true }); // 進捗情報が出力されることを確認 expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('records processed') ); }); }); describe('エンコーディング対応', () => { it('UTF-8 BOM付きファイルを処理', async () => { const csvContent = `Name,Type,Content,TTL,Priority utf8bom.example.com,A,192.0.2.1,300,`; // UTF-8 BOMを追加 const bomBuffer = Buffer.from([0xEF, 0xBB, 0xBF]); const contentBuffer = Buffer.from(csvContent, 'utf-8'); const finalBuffer = Buffer.concat([bomBuffer, contentBuffer]); const filePath = join(tmpdir(), `test-bom-${Date.now()}.csv`); tempFiles.push(filePath); await writeFile(filePath, finalBuffer); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); it('区切り文字の自動検出', async () => { // セミコロン区切りのCSV const csvContent = `Name;Type;Content;TTL;Priority semicolon.example.com;A;192.0.2.1;300;`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: false }); expect(mockExit).not.toHaveBeenCalled(); }); }); describe('設定ファイル統合', () => { it('設定ファイルからデフォルト値を読み込み', async () => { const csvContent = `Name,Type,Content,TTL,Priority config.example.com,A,192.0.2.1,300,`; const filePath = await createTempCsvFile(csvContent); // 設定ファイルをモック const configPath = join(tmpdir(), '.dnsweeper.json'); tempFiles.push(configPath); await writeFile(configPath, JSON.stringify({ import: { defaultFormat: 'cloudflare', batchSize: 50 } })); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { dryRun: true, verbose: false // format は設定ファイルから読み込まれる }); expect(mockExit).not.toHaveBeenCalled(); }); }); describe('統計情報表示', () => { it('インポート完了後に統計情報を表示', async () => { const csvContent = `Name,Type,Content,TTL,Priority stats1.example.com,A,192.0.2.1,300, stats2.example.com,A,192.0.2.2,300, stats3.example.com,MX,mail.example.com,300,10`; const filePath = await createTempCsvFile(csvContent); const { importCommand } = await import('../../src/commands/import.js'); await importCommand.action(filePath, { format: 'cloudflare', dryRun: true, verbose: true }); // 統計情報が表示されることを確認 expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Total records') ); }); }); });