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
text/typescript
/**
* 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')
);
});
});
});