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.

528 lines 22.7 kB
/** * route53.ts のユニットテスト */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { Route53Client } from '../../src/lib/route53.js'; // fetch をモック const mockFetch = vi.fn(); global.fetch = mockFetch; describe('Route53Client', () => { let client; let config; beforeEach(() => { config = { accessKeyId: 'test-access-key', secretAccessKey: 'test-secret-key', region: 'us-east-1' }; client = new Route53Client(config); // fetch をリセット mockFetch.mockReset(); }); afterEach(() => { vi.clearAllMocks(); }); describe('constructor', () => { it('デフォルトリージョンを設定', () => { const clientWithoutRegion = new Route53Client({ accessKeyId: 'test', secretAccessKey: 'test' }); // プライベートプロパティなので、リクエストでリージョンが使用されることを間接的にテスト expect(clientWithoutRegion).toBeDefined(); }); it('カスタムリージョンを設定', () => { const customClient = new Route53Client({ accessKeyId: 'test', secretAccessKey: 'test', region: 'eu-west-1' }); expect(customClient).toBeDefined(); }); }); describe('listHostedZones', () => { it('ホステッドゾーン一覧を正常に取得', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <ListHostedZonesResponse> <HostedZones> <HostedZone> <Id>/hostedzone/Z123456789</Id> <Name>example.com.</Name> <CallerReference>test-ref</CallerReference> <Config> <Comment>Test zone</Comment> <PrivateZone>false</PrivateZone> </Config> <ResourceRecordSetCount>10</ResourceRecordSetCount> </HostedZone> </HostedZones> </ListHostedZonesResponse>`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', text: () => Promise.resolve(mockResponse), headers: new Map([['x-amzn-requestid', 'test-request-id']]) }); const result = await client.listHostedZones(); expect(result.data).toBeDefined(); expect(result.data).toHaveLength(1); expect(result.data[0].Name).toBe('example.com.'); expect(result.data[0].Id).toBe('/hostedzone/Z123456789'); expect(result.statusCode).toBe(200); expect(result.requestId).toBe('test-request-id'); }); it('APIエラーを適切に処理', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 403, statusText: 'Forbidden', text: () => Promise.resolve('Access denied'), headers: new Map() }); const result = await client.listHostedZones(); expect(result.error).toContain('Failed to list hosted zones'); expect(result.statusCode).toBe(403); expect(result.data).toBeUndefined(); }); it('ネットワークエラーを適切に処理', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); const result = await client.listHostedZones(); expect(result.error).toBe('Network error'); expect(result.statusCode).toBe(500); }); }); describe('getHostedZone', () => { it('特定のホステッドゾーンを正常に取得', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <GetHostedZoneResponse> <HostedZone> <Id>/hostedzone/Z123456789</Id> <Name>example.com.</Name> <CallerReference>test-ref</CallerReference> <Config> <Comment>Test zone</Comment> <PrivateZone>false</PrivateZone> </Config> <ResourceRecordSetCount>10</ResourceRecordSetCount> </HostedZone> </GetHostedZoneResponse>`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(mockResponse), headers: new Map() }); const result = await client.getHostedZone('Z123456789'); expect(result.data).toBeDefined(); expect(result.data.Name).toBe('example.com.'); expect(result.data.Id).toBe('/hostedzone/Z123456789'); }); it('ゾーンIDのプレフィックスを適切に処理', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve('<GetHostedZoneResponse></GetHostedZoneResponse>'), headers: new Map() }); await client.getHostedZone('/hostedzone/Z123456789'); // URLパスが正しく生成されることを確認 expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/hostedzone/Z123456789'), expect.any(Object)); }); }); describe('listResourceRecordSets', () => { it('レコードセット一覧を正常に取得', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <ListResourceRecordSetsResponse> <ResourceRecordSets> <ResourceRecordSet> <Name>www.example.com.</Name> <Type>A</Type> <TTL>300</TTL> <ResourceRecords> <ResourceRecord> <Value>192.0.2.1</Value> </ResourceRecord> </ResourceRecords> </ResourceRecordSet> </ResourceRecordSets> </ListResourceRecordSetsResponse>`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(mockResponse), headers: new Map() }); const result = await client.listResourceRecordSets('Z123456789'); expect(result.data).toBeDefined(); expect(result.data).toHaveLength(1); expect(result.data[0].Name).toBe('www.example.com.'); expect(result.data[0].Type).toBe('A'); expect(result.data[0].TTL).toBe(300); }); it('フィルターオプションを適切に適用', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve('<ListResourceRecordSetsResponse></ListResourceRecordSetsResponse>'), headers: new Map() }); await client.listResourceRecordSets('Z123456789', { type: 'A', name: 'www.example.com', maxItems: 100 }); // クエリパラメータが正しく設定されることを確認 expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('type=A'), expect.any(Object)); expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('name=www.example.com'), expect.any(Object)); expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('maxitems=100'), expect.any(Object)); }); }); describe('changeResourceRecordSets', () => { it('レコードセット変更を正常に実行', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <ChangeResourceRecordSetsResponse> <ChangeInfo> <Id>/change/C123456789</Id> <Status>PENDING</Status> <SubmittedAt>2023-01-01T00:00:00.000Z</SubmittedAt> <Comment>Test change</Comment> </ChangeInfo> </ChangeResourceRecordSetsResponse>`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(mockResponse), headers: new Map() }); const changeBatch = { Comment: 'Test change', Changes: [{ Action: 'CREATE', ResourceRecordSet: { Name: 'test.example.com.', Type: 'A', TTL: 300, ResourceRecords: [{ Value: '192.0.2.1' }] } }] }; const result = await client.changeResourceRecordSets('Z123456789', changeBatch); expect(result.data).toBeDefined(); expect(result.data.Id).toBe('/change/C123456789'); expect(result.data.Status).toBe('PENDING'); }); it('XMLを正しく生成', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve('<ChangeResourceRecordSetsResponse></ChangeResourceRecordSetsResponse>'), headers: new Map() }); const changeBatch = { Comment: 'Test change', Changes: [{ Action: 'UPSERT', ResourceRecordSet: { Name: 'test.example.com.', Type: 'A', TTL: 300, ResourceRecords: [{ Value: '192.0.2.1' }] } }] }; await client.changeResourceRecordSets('Z123456789', changeBatch); // POSTリクエストのボディにXMLが含まれることを確認 expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'POST', body: expect.stringContaining('<Action>UPSERT</Action>') })); }); }); describe('convertCSVToRoute53Records', () => { it('CSVレコードを正しくRoute53レコードに変換', () => { const csvRecords = [ { domain: 'example.com', type: 'A', value: '192.0.2.1', ttl: 300 }, { domain: 'mail.example.com', type: 'MX', value: 'mail.example.com', ttl: 300, priority: 10 } ]; const route53Records = client.convertCSVToRoute53Records(csvRecords); expect(route53Records).toHaveLength(2); // Aレコードの確認 expect(route53Records[0].Name).toBe('example.com.'); expect(route53Records[0].Type).toBe('A'); expect(route53Records[0].ResourceRecords[0].Value).toBe('192.0.2.1'); // MXレコードの確認(優先度が値に含まれる) expect(route53Records[1].Name).toBe('mail.example.com.'); expect(route53Records[1].Type).toBe('MX'); expect(route53Records[1].ResourceRecords[0].Value).toBe('10 mail.example.com'); }); it('SRVレコードを正しく変換', () => { const csvRecords = [{ domain: '_sip._tcp.example.com', type: 'SRV', value: 'sip.example.com', ttl: 300, priority: 10, weight: 20, port: 5060 }]; const route53Records = client.convertCSVToRoute53Records(csvRecords); expect(route53Records[0].ResourceRecords[0].Value).toBe('10 20 5060 sip.example.com'); }); it('ドメイン名にドットを追加', () => { const csvRecords = [{ domain: 'example.com', type: 'A', value: '192.0.2.1', ttl: 300 }]; const route53Records = client.convertCSVToRoute53Records(csvRecords); expect(route53Records[0].Name).toBe('example.com.'); }); it('既にドットが付いているドメイン名をそのまま保持', () => { const csvRecords = [{ domain: 'example.com.', type: 'A', value: '192.0.2.1', ttl: 300 }]; const route53Records = client.convertCSVToRoute53Records(csvRecords); expect(route53Records[0].Name).toBe('example.com.'); }); it('重みベースルーティングを設定', () => { const csvRecords = [{ domain: 'example.com', type: 'A', value: '192.0.2.1', ttl: 300, weight: 100 }]; const route53Records = client.convertCSVToRoute53Records(csvRecords); expect(route53Records[0].Weight).toBe(100); expect(route53Records[0].SetIdentifier).toBe('example.com-A-100'); }); }); describe('convertRoute53ToCSVRecords', () => { it('Route53レコードを正しくCSVレコードに変換', () => { const route53Records = [ { Name: 'example.com.', Type: 'A', TTL: 300, ResourceRecords: [{ Value: '192.0.2.1' }] }, { Name: 'mail.example.com.', Type: 'MX', TTL: 300, ResourceRecords: [{ Value: '10 mail.example.com' }] } ]; const csvRecords = client.convertRoute53ToCSVRecords(route53Records); expect(csvRecords).toHaveLength(2); // Aレコードの確認 expect(csvRecords[0].domain).toBe('example.com'); expect(csvRecords[0].type).toBe('A'); expect(csvRecords[0].value).toBe('192.0.2.1'); // MXレコードの確認(優先度が分離される) expect(csvRecords[1].domain).toBe('mail.example.com'); expect(csvRecords[1].type).toBe('MX'); expect(csvRecords[1].value).toBe('mail.example.com'); expect(csvRecords[1].priority).toBe(10); }); it('SRVレコードを正しく変換', () => { const route53Records = [{ Name: '_sip._tcp.example.com.', Type: 'SRV', TTL: 300, ResourceRecords: [{ Value: '10 20 5060 sip.example.com' }] }]; const csvRecords = client.convertRoute53ToCSVRecords(route53Records); expect(csvRecords[0].priority).toBe(10); expect(csvRecords[0].weight).toBe(20); expect(csvRecords[0].port).toBe(5060); expect(csvRecords[0].value).toBe('sip.example.com'); }); it('エイリアスレコードを変換', () => { const route53Records = [{ Name: 'example.com.', Type: 'A', AliasTarget: { DNSName: 'alias.example.com', EvaluateTargetHealth: false, HostedZoneId: 'Z123456789' } }]; const csvRecords = client.convertRoute53ToCSVRecords(route53Records); expect(csvRecords[0].value).toBe('alias.example.com'); expect(csvRecords[0].ttl).toBe(300); // デフォルトTTL }); it('ドメイン名のドットを除去', () => { const route53Records = [{ Name: 'example.com.', Type: 'A', TTL: 300, ResourceRecords: [{ Value: '192.0.2.1' }] }]; const csvRecords = client.convertRoute53ToCSVRecords(route53Records); expect(csvRecords[0].domain).toBe('example.com'); }); it('重みベースルーティングの重みを取得', () => { const route53Records = [{ Name: 'example.com.', Type: 'A', TTL: 300, Weight: 100, ResourceRecords: [{ Value: '192.0.2.1' }] }]; const csvRecords = client.convertRoute53ToCSVRecords(route53Records); expect(csvRecords[0].weight).toBe(100); }); }); describe('importRecords', () => { it('レコードを正常にインポート', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <ChangeResourceRecordSetsResponse> <ChangeInfo> <Id>/change/C123456789</Id> <Status>PENDING</Status> <SubmittedAt>2023-01-01T00:00:00.000Z</SubmittedAt> </ChangeInfo> </ChangeResourceRecordSetsResponse>`; mockFetch.mockResolvedValue({ ok: true, status: 200, text: () => Promise.resolve(mockResponse), headers: new Map() }); const csvRecords = [ { domain: 'test1.example.com', type: 'A', value: '192.0.2.1', ttl: 300 }, { domain: 'test2.example.com', type: 'A', value: '192.0.2.2', ttl: 300 } ]; const result = await client.importRecords('Z123456789', csvRecords); expect(result.data).toBeDefined(); expect(result.data).toHaveLength(1); // バッチサイズが100なので1つのバッチ expect(result.statusCode).toBe(200); }); it('大量レコードを適切にバッチ分割', async () => { mockFetch.mockResolvedValue({ ok: true, status: 200, text: () => Promise.resolve('<ChangeResourceRecordSetsResponse><ChangeInfo><Id>test</Id><Status>PENDING</Status><SubmittedAt>2023-01-01T00:00:00.000Z</SubmittedAt></ChangeInfo></ChangeResourceRecordSetsResponse>'), headers: new Map() }); const csvRecords = Array.from({ length: 150 }, (_, i) => ({ domain: `test${i}.example.com`, type: 'A', value: '192.0.2.1', ttl: 300 })); const result = await client.importRecords('Z123456789', csvRecords, { batchSize: 100 }); expect(result.data).toBeDefined(); expect(result.data).toHaveLength(2); // 150レコードが100と50に分割される expect(mockFetch).toHaveBeenCalledTimes(2); }); it('エラー時に適切なレスポンスを返す', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request', text: () => Promise.resolve('Invalid request'), headers: new Map() }); const csvRecords = [{ domain: 'test.example.com', type: 'A', value: '192.0.2.1', ttl: 300 }]; const result = await client.importRecords('Z123456789', csvRecords); expect(result.error).toContain('Failed to change resource record sets'); expect(result.statusCode).toBe(400); }); }); describe('exportRecords', () => { it('レコードを正常にエクスポート', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <ListResourceRecordSetsResponse> <ResourceRecordSets> <ResourceRecordSet> <Name>www.example.com.</Name> <Type>A</Type> <TTL>300</TTL> <ResourceRecords> <ResourceRecord> <Value>192.0.2.1</Value> </ResourceRecord> </ResourceRecords> </ResourceRecordSet> </ResourceRecordSets> </ListResourceRecordSetsResponse>`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(mockResponse), headers: new Map() }); const result = await client.exportRecords('Z123456789'); expect(result.data).toBeDefined(); expect(result.data).toHaveLength(1); expect(result.data[0].domain).toBe('www.example.com'); expect(result.data[0].type).toBe('A'); expect(result.data[0].value).toBe('192.0.2.1'); }); it('特定のレコードタイプでフィルタリング', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve('<ListResourceRecordSetsResponse></ListResourceRecordSetsResponse>'), headers: new Map() }); await client.exportRecords('Z123456789', { type: 'MX' }); // リクエストにタイプフィルタが含まれることを確認 expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('type=MX'), expect.any(Object)); }); }); describe('エラーハンドリング', () => { it('不正な設定でエラーにならない', () => { expect(() => { new Route53Client({ accessKeyId: '', secretAccessKey: '' }); }).not.toThrow(); }); it('空のレコード配列を適切に処理', () => { const result = client.convertCSVToRoute53Records([]); expect(result).toEqual([]); }); it('空のRoute53レコード配列を適切に処理', () => { const result = client.convertRoute53ToCSVRecords([]); expect(result).toEqual([]); }); }); }); //# sourceMappingURL=route53.test.js.map