UNPKG

amazon-route-53-dns-zone-file

Version:

Makes DNS Zone File easy. Parses and validates BIND zone files and can be extended for custom features. Functionality is modular. Features are made open for extension and closed for runtime mutation. Written in TypeScript.

1,443 lines (1,404 loc) 38.2 kB
import { SUPPORTED_RECORD_TYPES, RecordMinLengthKind, ErrorKeyKind, WarningKeyKind, ParserInvalidArgumentsError, ParsingError, } from '../../src'; import { ExampleDomain } from '../../example/example_types'; import { getErrorText } from '../../example/example_consumer'; import * as complexMock from '../mocks/complex'; import * as validMock from '../mocks/valid'; import * as invalidApiMock from '../mocks/invalid_api'; import * as invalidClientMock from '../mocks/invalid_client'; import * as edgeMock from '../mocks/edge'; import { testImportZoneFileAndCallApi, testImportZoneFile, spyOnCreateApi, MockSupportedRecordType, } from '../utils'; type MockWithoutTypeForKey<Key extends string> = { [K in Key]?: ExampleDomain.ClientRecord[]; }; type MockTypeForKey<Key extends string> = { [K in MockSupportedRecordType]: MockWithoutTypeForKey<Key>; }; type IValidMockKeyType = keyof typeof validMock; type IInvalidApiMockKeyType = keyof typeof invalidApiMock; type IComplexMockKeyType = keyof typeof complexMock; describe('Import zone file (features)', () => { const validTestCases: MockTypeForKey<IValidMockKeyType> = { GENERAL: { ttlWithNameSimilarToTTL: [ { name: '100.canada.igloo.com.', endpoints: ['2.3.4.5'], type: 'A', ttl: 42, }, ], backwardsTTL: [ { name: 'eh.igloo.com.', endpoints: ['2.3.4.5'], type: 'A', ttl: 42, }, ], optionalTTLAndClass: [ { name: 'neither.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 4242, }, { name: 'ttl.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 42, }, { name: 'class.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 4242, }, { name: 'ttl.class.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 42, }, { name: 'class.ttl.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 42, }, ], comments1: [ { name: 'eh.igloo.com.', endpoints: ['2.3.4.5'], type: 'A', ttl: 42, }, { name: 'www.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 42, }, ], nameNotTooLong: [ { name: '1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.igloo.com.', endpoints: ['1.2.3.4'], ttl: 60, type: 'A', }, ], utfCharacters: [ { name: '©.igloo.com.', endpoints: ['1.2.3.4'], ttl: 42, type: 'A', }, { name: '¿.igloo.com.', endpoints: ['1.2.3.4'], ttl: 42, type: 'A', }, ], escapeInTXT: [ { name: 'www.igloo.com.', endpoints: ['"\\101\\"'], ttl: 42, type: 'TXT', }, ], escapeInCNAMEValue: [ { name: 'www.igloo.com.', endpoints: ['\\008\\99.igloo.com.'], ttl: 42, type: 'CNAME', }, ], defaultOrigin: [ { name: 'igloo.com.', endpoints: ['1.2.3.4'], ttl: 42, type: 'A' }, { name: 'www.igloo.com.', endpoints: ['1.2.3.4'], ttl: 42, type: 'A', }, ], specialTTL: [ { name: 'eh.igloo.com.', endpoints: ['2.3.4.5'], ttl: 42, type: 'A', }, ], }, AAAA: { aaaaWithNameAndTopLevelTTL: [ { name: 'example.igloo.com.', endpoints: ['::1'], ttl: 42, type: 'AAAA', }, ], aaaaWithAtNameAndInlineTTL: [ { name: 'igloo.com.', endpoints: ['::1'], ttl: 42, type: 'AAAA', }, ], }, A: { aWithNameAndTopLevelTTL: [ { name: 'example.igloo.com.', endpoints: ['127.0.0.1'], ttl: 42, type: 'A', }, ], aWithAtNameAndInlineTTL: [ { name: 'igloo.com.', endpoints: ['127.0.0.1'], ttl: 42, type: 'A', }, ], }, CAA: { caaExampleJustIssueWithInNoNameTopLevelTTL: [ { name: 'igloo.com.', endpoints: ['0 issue "ca.igloo.com"'], ttl: 42, type: 'CAA', }, ], caaExampleJustIssueNoNameTopLevelTTL: [ { name: 'igloo.com.', endpoints: ['0 issue "ca.igloo.com"'], ttl: 42, type: 'CAA', }, ], caaExampleIssueAndIoDefAndTopLevelTTL: [ { name: 'igloo.com.', endpoints: [ '0 issue "ca.igloo.com"', '0 iodef "mailto:security@igloo.com"', '0 iodef "http://iodef.igloo.com/"', ], ttl: 42, type: 'CAA', }, ], caaExampleIssueAndIoDefAndTopLevelTTLNoQuotes: [ { name: 'igloo.com.', endpoints: [ '0 issue ca.igloo.com', '0 iodef mailto:security@igloo.com', '0 iodef http://iodef.igloo.com/', ], ttl: 42, type: 'CAA', }, ], caaExampleJustIoDef: [ { name: 'igloo.com.', endpoints: ['0 iodef "mailto:security@igloo.com"'], type: 'CAA', ttl: 42, }, ], caaExampleInlineTTL: [ { name: 'igloo.com.', endpoints: [ '0 issue "ca.igloo.com"', '0 iodef "mailto:security@igloo.com"', ], ttl: 42, type: 'CAA', }, ], caaExampleJustIssue: [ { name: 'igloo.com.', endpoints: ['0 issue "ca.igloo.com"'], ttl: 42, type: 'CAA', }, ], }, MX: { mxExampleAt: [ { name: 'igloo.com.', endpoints: ['0 mail1', '10 mail2'], ttl: 42, type: 'MX', }, ], }, CNAME: { cnameWithTopLevelTTL: [ { name: 'mail1.igloo.com.', endpoints: ['mail'], ttl: 42, type: 'CNAME', }, { name: 'mail2.igloo.com.', endpoints: ['mail'], ttl: 42, type: 'CNAME', }, ], cnameWithInlineAndSpacesTTL: [ { name: 'mail1.igloo.com.', endpoints: ['mail'], ttl: 42, type: 'CNAME', }, { name: 'mail2.igloo.com.', endpoints: ['mail'], ttl: 60, type: 'CNAME', }, ], }, NAPTR: { naptrExampleTopLevelTTL: [ { name: 'igloo.com.', endpoints: [ '100 10 "U" "E2U+sip" "!^.*$!sip:customer-service@example.com!" .', '102 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" .', ], ttl: 42, type: 'NAPTR', }, ], naptrExample1: [ { name: 'rfc3403-1.igloo.com.', endpoints: [ '100 10 "u" "sip+E2U" "!^.*$!sip:information@foo.se!i" .', ], ttl: 42, type: 'NAPTR', }, { name: 'rfc3403-2.igloo.com.', endpoints: ['100 50 "a" "z3950+N2L+N2C" "" cidserver.igloo.com.'], ttl: 42, type: 'NAPTR', }, ], naptrExampleInlineTTLAndNested: [ { name: 'rfc3403-1.igloo.com.', endpoints: [ '100 10 "u" "sip+E2U" "!^.*$!sip:information@foo.se!i" .', ], ttl: 42, type: 'NAPTR', }, { name: 'rfc3403-2.igloo.com.', endpoints: ['100 50 "a" "z3950+N2L+N2C" "" cidserver.igloo.com.'], ttl: 42, type: 'NAPTR', }, { name: 'igloo.com.', endpoints: [ '99 98 "B" "E2U+siz" "" .', '100 10 "U" "E2U+sip" "!^.*$!sip:customer-service@example.com!" .', '102 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" .', ], ttl: 42, type: 'NAPTR', }, ], }, TXT: { txtExampleTopLevelTTL: [ { name: 'treefrog.igloo.com.', endpoints: [ '"v=spf1 a mx a:mail.treefrog.ca a:webmail.treefrog.ca ip4:76.75.250.33 ?all"', ], ttl: 42, type: 'TXT', }, { name: 'treemonkey.igloo.com.', endpoints: ['"v=DKIM1; k=rsa; p=MIGf..."'], ttl: 42, type: 'TXT', }, { name: 'treemonkey1.igloo.com.', endpoints: ['"v=DKIM1; k=rsa; p=MIGf..."'], ttl: 42, type: 'TXT', }, ], txtExampleInlineTTL: [ { name: 'treefrog.igloo.com.', endpoints: [ '"v=spf1 a mx a:mail.treefrog.ca a:webmail.treefrog.ca ip4:76.75.250.33 ?all"', ], ttl: 42, type: 'TXT', }, { name: 'treemonkey.igloo.com.', endpoints: ['"v=DKIM1; k=rsa; p=MIGf..."'], ttl: 42, type: 'TXT', }, { name: 'treemonkey1.igloo.com.', endpoints: ['"v=DKIM1; k=rsa; p=MIGf..."'], ttl: 42, type: 'TXT', }, ], /** @idea would we want to join in this case? */ txtExampleNested: [ { name: 'treefrog.igloo.com.', endpoints: ['"eh=true"', '"moose=DKIM1; beaver=canada;"'], ttl: 42, type: 'TXT', }, ], parensInTXT: [ { name: 'eh.igloo.com.', endpoints: ['"Some text (Moose)"', '"spaced ( spaces1 spaces2 )"'], ttl: 42, type: 'TXT', }, ], }, NS: { nsExampleWithAtAndTopLevelTTL: [], nsExampleWithNoNameAndTopLevelTTL: [], nsExampleWithNamesAndTopLevelTTL: [ { name: 'syrup.igloo.com.', endpoints: ['NS1.NAMESERVER.NET.'], ttl: 42, type: 'NS', }, { name: 'beaver.igloo.com.', endpoints: ['NS2.NAMESERVER.NET.'], type: 'NS', ttl: 42, }, ], nsExampleInlineTTL: [ { name: 'eh.igloo.com.', endpoints: ['NS1.NAMESERVER.NET.'], ttl: 42, type: 'NS', }, { name: 'canada.igloo.com.', endpoints: ['NS2.NAMESERVER.NET.'], ttl: 42, type: 'NS', }, ], nsExampleNestedWithTopLevelTTL: [ { name: 'moose.igloo.com.', endpoints: ['NS1.NAMESERVER.NET.', 'NS2.NAMESERVER.NET.'], ttl: 42, type: 'NS', }, ], }, PTR: { ptrExampleOneEntryTopLevelTTL: [ { endpoints: ['HOST1.igloo.com.'], name: '99.11.igloo.com.', type: 'PTR', ttl: 42, }, ], ptrExampleInlineTTL: [ { name: '1.0.168.192.igloo.com.', endpoints: ['HOST1.igloo.com.'], type: 'PTR', ttl: 42, }, { name: '2.0.168.192.igloo.com.', endpoints: ['HOST2.igloo.com.'], type: 'PTR', ttl: 42, }, ], ptrExampleTopLevelTTL: [ { name: '1.0.168.192.igloo.com.', endpoints: ['HOST1.igloo.com.'], type: 'PTR', ttl: 42, }, { name: '2.0.168.192.igloo.com.', endpoints: ['HOST2.igloo.com.'], type: 'PTR', ttl: 42, }, ], ptrExampleNestedTopLevelTTL: [ { name: '3.30.168.192.igloo.com', endpoints: ['HOST3.igloo.com.'], type: 'PTR', ttl: 42, }, { name: '4.30.168.192.igloo.com', endpoints: [ 'HOST4.igloo.com.', 'HOST5.igloo.com.', 'HOST6.igloo.com.', ], type: 'PTR', ttl: 42, }, ], ptrExampleDecimals: [ { endpoints: ['HOST3.igloo.com.'], name: '10.3.168.192.igloo.com', ttl: 42, type: 'PTR', }, { endpoints: ['HOST4.igloo.com.'], name: '10.4.168.192.igloo.com', ttl: 42, type: 'PTR', }, ], ptrMultipleOrigins: [ { endpoints: ['HOST3.igloo.com.'], name: '3.30.168.192.igloo.com', ttl: 41, type: 'PTR', }, { endpoints: [ 'HOST4.igloo.com.', 'HOST5.igloo.com.', 'HOST6.igloo.com.', ], name: '4.30.168.192.igloo.com', ttl: 42, type: 'PTR', }, { endpoints: ['HOST3.igloo.com.'], name: '10.3.168.192.igloo.com', ttl: 45, type: 'PTR', }, { endpoints: ['HOST4.igloo.com.'], name: '10.4.168.192.igloo.com', ttl: 46, type: 'PTR', }, ], }, SPF: { spfMaxValues: [ { name: 'eh.igloo.com.', endpoints: ['v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all'], ttl: 42, type: 'SPF', }, ], spfMinValues: [ { name: 'eh.igloo.com.', endpoints: ['v=spf1 ip:4:192.168.0.1/16-all'], ttl: 42, type: 'SPF', }, ], spfExampleOneEntryInlineTTL: [ { name: 'test.igloo.com.', endpoints: ['"v=spf1" "mx:gcloud-node.com." "-all"'], ttl: 42, type: 'SPF', }, ], spfExampleNestedTopLevelTTL: [ { name: 'test.igloo.com.', endpoints: ['"v=spf1" "mx:gcloud-node.com." "-all"'], ttl: 42, type: 'SPF', }, { name: 'test1.igloo.com.', endpoints: [ '"v=spf2" "mx:gcloud-node.com." "-all"', '"v=spf3" "mx:gcloud-node.com." "-all "', '"v=spf4" "mx:gcloud-node.com." "-all"', ], ttl: 42, type: 'SPF', }, ], spfExampleNestedInlineTTL: [ { name: 'test.igloo.com.', endpoints: ['"v=spf1" "mx:gcloud-node.com." "-all"'], ttl: 42, type: 'SPF', }, { name: 'test1.igloo.com.', endpoints: [ '"v=spf2" "mx:gcloud-node.com." "-all"', '"v=spf3" "mx:gcloud-node.com." "-all "', '"v=spf4" "mx:gcloud-node.com." "-all"', ], ttl: 42, type: 'SPF', }, ], spfExampleAtNameInlineTTL: [ { name: 'igloo.com.', endpoints: ['"v=spf1 a mx ~all"'], ttl: 42, type: 'TXT', }, ], spfExampleOfOneParam: [ { name: 'eh.igloo.com.', endpoints: ['"v=spf1 ip:4:192.168.0.1/16"'], ttl: 42, type: 'SPF', }, ], }, SRV: { srvExampleWithNameAndTopLevelTTL: [ { name: 'server.igloo.com.', endpoints: ['172.30.79.10'], ttl: 42, type: 'A', }, { name: 'old-slow-box.igloo.com.', endpoints: ['172.30.79.11'], ttl: 42, type: 'A', }, { name: 'sysadmins-box.igloo.com.', endpoints: ['172.30.79.12'], ttl: 42, type: 'A', }, { name: 'new-fast-box.igloo.com.', endpoints: ['172.30.79.13'], ttl: 42, type: 'A', }, { name: '_foobar._tcp.igloo.com.', endpoints: [ '0 1 9 old-slow-box.igloo.com.', '0 3 9 new-fast-box.igloo.com.', '1 0 9 sysadmins-box.igloo.com.', '1 0 9 server.igloo.com.', ], ttl: 42, type: 'SRV', }, { name: '*._tcp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 42, type: 'SRV', }, { name: '*._udp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 42, type: 'SRV', }, ], srvExampleWithNameAndInlineTTL: [ { name: '_foobar._tcp.igloo.com.', endpoints: [ '0 1 9 old-slow-box.igloo.com.', '0 3 9 new-fast-box.igloo.com.', '1 0 9 sysadmins-box.igloo.com.', '1 0 9 server.igloo.com.', ], ttl: 42, type: 'SRV', }, { name: '*._tcp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 42, type: 'SRV', }, { name: '*._udp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 42, type: 'SRV', }, ], }, SOA: { soaIsIgnored1: [], soaIsIgnored2: [ { name: 'example.igloo.com.', endpoints: ['127.0.0.1'], /** @default 300 */ ttl: 300, type: 'A', }, ], soaIsIgnored3: [ { name: 'mail1.igloo.com.', endpoints: ['mail'], /** @default 300 */ ttl: 300, type: 'CNAME', }, ], simpleSoa: [], }, }; describe.each([ 'GENERAL', ...SUPPORTED_RECORD_TYPES, 'SOA', ] as MockSupportedRecordType[])( 'Parsing type "%s" (valid)', resourceRecordType => { beforeEach(() => { spyOnCreateApi().mockImplementation(() => Promise.resolve(true)); }); const testCasesIndexed = validTestCases[resourceRecordType]; it.each(Object.keys(testCasesIndexed) as IValidMockKeyType[])( 'Should match expected - %s', key => { const input = validMock[key]; const expected = testCasesIndexed[key]; const { records } = testImportZoneFile(input); expect(records).toEqual(expected); } ); } ); describe('Warnings when there are ignored records in zone file', () => { it('Should return no warnings when there are not SOA and/or NS without name', () => { const input = validMock['nsExampleWithNamesAndTopLevelTTL']; const { warnings } = testImportZoneFile(input); expect(warnings).toEqual([]); }); it('Should return warning when SOA is ignored', () => { const input = validMock['soaIsIgnored2']; const { warnings } = testImportZoneFile(input); expect(warnings).toEqual([WarningKeyKind.SoaIgnored]); }); it('Should return warning when NS is ignored', () => { const input = validMock['nsExampleWithAtAndTopLevelTTL']; const { warnings } = testImportZoneFile(input); expect(warnings).toEqual([ WarningKeyKind.NsIgnored, WarningKeyKind.NsIgnored, ]); }); it('Should return warnings when SOA and NS are ignored', () => { const input = validMock['soaIsIgnored1']; const { warnings } = testImportZoneFile(input); expect(warnings).toEqual([ WarningKeyKind.SoaIgnored, WarningKeyKind.NsIgnored, ]); }); }); describe('Edge cases', () => { const edgeTestCases: MockWithoutTypeForKey<keyof typeof edgeMock> = { atSymbol: [ { name: 'www.igloo.com.', ttl: 300, type: 'CNAME', endpoints: ['igloo.com.'], }, ], atSymbolEscaped: [ { name: 'www.igloo.com.', ttl: 300, type: 'CNAME', endpoints: ['@'], }, ], }; it.each(Object.keys(edgeTestCases) as Array<keyof typeof edgeTestCases>)( 'Should handle "%s"', key => { const input = edgeMock[key]; const expected = edgeTestCases[key]; const edgeResults = testImportZoneFile(input); expect(edgeResults.records).toEqual(expected); } ); }); describe('Complex (valid)', () => { const complexTestCases: MockWithoutTypeForKey<IComplexMockKeyType> = { /** @note the ttls here are defaulted to 300 */ complicated1: [ { name: 'server.igloo.com.', endpoints: ['172.30.79.10'], ttl: 300, type: 'A', }, { name: 'old-slow-box.igloo.com.', endpoints: ['172.30.79.11'], ttl: 300, type: 'A', }, { name: 'sysadmins-box.igloo.com.', endpoints: ['172.30.79.12'], ttl: 300, type: 'A', }, { name: 'new-fast-box.igloo.com.', endpoints: ['172.30.79.13'], ttl: 300, type: 'A', }, { name: '_foobar._tcp.igloo.com.', endpoints: [ '0 1 9 old-slow-box.igloo.com.', '0 3 9 new-fast-box.igloo.com.', '1 0 9 sysadmins-box.igloo.com.', '1 0 9 server.igloo.com.', ], ttl: 300, type: 'SRV', }, { name: '*._tcp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 300, type: 'SRV', }, { name: '*._udp.igloo.com.', endpoints: ['0 0 0 .'], ttl: 300, type: 'SRV', }, ], complicated2: [ { name: 'neither.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 604800, }, { name: 'ns.igloo.com.', endpoints: ['2.3.4.5'], type: 'A', ttl: 100, }, { name: 'mail.igloo.com.', endpoints: ['10.0.0.2'], type: 'A', ttl: 86400, }, { name: 'mail2.igloo.com.', endpoints: ['10.0.0.3'], type: 'A', ttl: 86400, }, { name: 'www.igloo.com.', endpoints: ['10.0.0.1'], type: 'A', ttl: 86400, }, { name: '\\097.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 60, }, { name: '\\304.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 60, }, { name: '©.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 60, }, { name: '¿.igloo.com.', endpoints: ['1.2.3.4'], type: 'A', ttl: 60, }, { name: 'www.igloo.com.', endpoints: ['\\008\\99.igloo.com.'], type: 'CNAME', ttl: 60, }, { endpoints: ['igloo.com.'], name: 'root.igloo.com.', ttl: 86400, type: 'CNAME', }, { name: 'web.igloo.com.', endpoints: ['www'], type: 'CNAME', ttl: 86400, }, { name: 'www.igloo.com.', endpoints: ['"\\101\\"'], type: 'TXT', ttl: 60, }, { name: 'test.igloo.com.', endpoints: ['"v=spf1" "mx:gcloud-node.com." "-all"'], type: 'SPF', ttl: 604800, }, { name: 'test1.igloo.com.', endpoints: [ '"v=spf2" "mx:gcloud-node.com." "-all"', '"v=spf3" "mx:gcloud-node.com." "-all "', '"v=spf4" "mx:gcloud-node.com." "-all"', ], type: 'SPF', ttl: 604800, }, ], complicated3: [ { name: 'igloo.com.', endpoints: ['192.0.2.1'], type: 'A', ttl: 3600, }, { name: 'ns.igloo.com.', endpoints: ['192.0.2.2'], type: 'A', ttl: 3600, }, { name: 'mail.igloo.com.', endpoints: ['192.0.2.3'], type: 'A', ttl: 3600, }, { name: 'mail2.igloo.com.', endpoints: ['192.0.2.4'], type: 'A', ttl: 3600, }, { name: 'mail3.igloo.com.', endpoints: ['192.0.2.5'], type: 'A', ttl: 3600, }, { name: 'igloo.com.', endpoints: ['2001:db8:10::1'], type: 'AAAA', ttl: 3600, }, { name: 'ns.igloo.com.', endpoints: ['2001:db8:10::2'], type: 'AAAA', ttl: 3600, }, { name: 'www.igloo.com.', endpoints: ['igloo.com.'], type: 'CNAME', ttl: 3600, }, { name: 'wwwtest.igloo.com.', endpoints: ['www'], type: 'CNAME', ttl: 3600, }, { name: 'igloo.com.', endpoints: ['10 mail.igloo.com.', '20 mail2.igloo.com.', '50 mail3'], type: 'MX', ttl: 3600, }, ], complicated4: [ { name: 'mail.igloo.com.', endpoints: ['127.0.0.1', '1.2.3.4'], type: 'A', ttl: 1814400, }, { name: 'mail.igloo.com.', endpoints: ['2001:db8::1'], type: 'AAAA', ttl: 1814400, }, { name: 'mail1.igloo.com.', endpoints: ['mail'], type: 'CNAME', ttl: 1814400, }, { name: 'mail2.igloo.com.', endpoints: ['mail'], type: 'CNAME', ttl: 1814400, }, { name: 'igloo.com.', endpoints: ['0 mail1', '10 mail2'], type: 'MX', ttl: 1814400, }, ], }; /** @note !!! the order is changed (from the consumer) */ it('Should handle - invalid nesting', () => { const { records } = testImportZoneFile( invalidClientMock.parsingErrorInvalidNesting ); const expected = [ { name: 'www.igloo.com.', endpoints: ['5.6.7.8'], type: 'A', ttl: 42, }, { name: 'notme.igloo.com.', endpoints: ['1.2.3.4'], type: 'NS', ttl: 42, }, { name: 'www.igloo.com.', endpoints: ['9.10.11.12'], type: 'NS', ttl: 42, }, ]; expect(records).toEqual(expect.arrayContaining(expected)); }); it.each(Object.keys(complexTestCases) as IComplexMockKeyType[])( '%s', key => { const expected = complexTestCases[key]; const input = complexMock[key]; const { records } = testImportZoneFile(input); // expect(records).toEqual(expected); expect(records).toEqual(expect.arrayContaining(expected!)); } ); }); describe('Client side (invalid)', () => { it('Should handle - unsupported type (+ handle line number)', () => { const { error } = testImportZoneFile(invalidClientMock.unsupportedType); expect(error!.message).toEqual( getErrorText({ errorType: ErrorKeyKind.BadDirective, } as any) ); expect(error!.parsingLineContent).toEqual( 'www 42s IN DNAME www.otherzone.com.' ); expect(error!.parsingLineNumber).toEqual(2); }); it.each` errorType | mock | case ${ErrorKeyKind.EmptyInput} | ${invalidClientMock.empty} | ${'empty'} ${ErrorKeyKind.BadDirective} | ${invalidClientMock.badDirectiveInclude} | ${'bad directive (import)'} ${ErrorKeyKind.BadDirective} | ${invalidClientMock.badDirectiveUnknown} | ${'bad directive (unknown)'} ${ErrorKeyKind.BadDirective} | ${invalidClientMock.incompleteRecords} | ${'incomplete record'} ${ErrorKeyKind.NoRecords} | ${invalidClientMock.onlyComment} | ${'no records'} ${ErrorKeyKind.WrongNameArgsLength} | ${invalidClientMock.tooManyNameArgs} | ${'too many args for name/ttl'} ${ErrorKeyKind.Duplicated} | ${invalidClientMock.duplicateRecord} | ${'duplicated'} ${ErrorKeyKind.EmptyInput} | ${invalidClientMock.empty} | ${'empty'} ${ErrorKeyKind.TtlInvalidFormat} | ${invalidClientMock.badTtl} | ${'invalid ttl'} `( 'Should handle "$case"', ({ errorType, mock }: { errorType: ErrorKeyKind; mock: string }) => { const zone = testImportZoneFile(mock); const { error } = zone; expect(error!.message).toEqual(getErrorText({ errorType } as any)); } ); }); describe('With wrong arg lengths (invalid)', () => { const argLengths = { txtWithNotEnoughArgs: { expectedArgCount: RecordMinLengthKind.TXT, actualArgCount: 0, }, txtWithNotEnoughArgsOrAnything: { expectedArgCount: RecordMinLengthKind.TXT, actualArgCount: 0, }, txtWithTooManyEntries: { expectedArgCount: RecordMinLengthKind.TXT, actualArgCount: RecordMinLengthKind.TXT + 1, }, txtWithTooManyEntriesNoTtl: { expectedArgCount: RecordMinLengthKind.TXT, actualArgCount: RecordMinLengthKind.TXT + 1, }, txtWithTooManyEntriesNoNameOrTtl: { expectedArgCount: RecordMinLengthKind.TXT, actualArgCount: RecordMinLengthKind.TXT + 1, }, ptrWithTooManyEntries: { expectedArgCount: RecordMinLengthKind.PTR, actualArgCount: RecordMinLengthKind.PTR + 1, }, ptrWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.PTR, actualArgCount: RecordMinLengthKind.PTR - 1, }, aaaaWithTooManyEntries: { expectedArgCount: RecordMinLengthKind.AAAA, actualArgCount: RecordMinLengthKind.AAAA + 1, }, aaaaWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.AAAA, actualArgCount: RecordMinLengthKind.AAAA - 1, }, aWithTooManyEntries: { expectedArgCount: RecordMinLengthKind.A, actualArgCount: RecordMinLengthKind.A + 1, }, aWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.A, actualArgCount: RecordMinLengthKind.A - 1, }, spfWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.SPF, actualArgCount: RecordMinLengthKind.SPF - 1, }, spfWithNotEnoughArgs3: { expectedArgCount: RecordMinLengthKind.SPF, actualArgCount: 0, }, caaWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.CAA, actualArgCount: RecordMinLengthKind.CAA + 1, }, caaWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.CAA, actualArgCount: RecordMinLengthKind.CAA - 1, }, caaWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.CAA, actualArgCount: RecordMinLengthKind.CAA - 2, }, caaWithNotEnoughArgs3: { expectedArgCount: RecordMinLengthKind.CAA, actualArgCount: 0, }, naptrWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR + 1, }, naptrWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR - 1, }, naptrWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR - 2, }, naptrWithNotEnoughArgs3: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR - 3, }, naptrWithNotEnoughArgs4: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR - 4, }, naptrWithNotEnoughArgs5: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: RecordMinLengthKind.NAPTR - 5, }, naptrWithNotEnoughArgs6: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: 0, }, naptrWithNotEnoughArgs7: { expectedArgCount: RecordMinLengthKind.NAPTR, actualArgCount: 0, }, srvWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: RecordMinLengthKind.SRV + 1, }, srvWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: RecordMinLengthKind.SRV - 1, }, srvWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: RecordMinLengthKind.SRV - 2, }, srvWithNotEnoughArgs3: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: RecordMinLengthKind.SRV - 3, }, srvWithNotEnoughArgs4: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: RecordMinLengthKind.SRV - 4, }, srvWithNotEnoughArgs5: { expectedArgCount: RecordMinLengthKind.SRV, actualArgCount: 0, }, nsWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.NS, actualArgCount: RecordMinLengthKind.NS + 1, }, nsWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.NS, actualArgCount: 0, }, nsWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.NS, actualArgCount: 0, }, nsWithNotEnoughArgs3: { expectedArgCount: RecordMinLengthKind.NS, actualArgCount: 0, }, nsWithNotEnoughArgs4: { expectedArgCount: RecordMinLengthKind.NS, actualArgCount: 0, }, mxWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.MX, actualArgCount: RecordMinLengthKind.MX + 1, }, mxWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.MX, actualArgCount: RecordMinLengthKind.MX - 1, }, mxWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.MX, actualArgCount: 0, }, cnameWithTooManyArgs: { expectedArgCount: RecordMinLengthKind.CNAME, actualArgCount: RecordMinLengthKind.CNAME + 1, }, cnameWithNotEnoughArgs1: { expectedArgCount: RecordMinLengthKind.CNAME, actualArgCount: 0, }, cnameWithNotEnoughArgs2: { expectedArgCount: RecordMinLengthKind.CNAME, actualArgCount: 0, }, }; it.each(Object.keys(argLengths) as Array<keyof typeof argLengths>)( 'Should throw - `%s`', key => { const input = invalidClientMock[key]; const error: ParserInvalidArgumentsError & ParsingError = testImportZoneFile(input).error as any; const { expectedArgCount, actualArgCount } = argLengths[key]; expect(error).toMatchSnapshot(); /** * @note this is not here unless i18n was added * @example /Too many args\! Expected (\d) for type [a-z]\. Received (\d)/gmi) * * @example * const matches = match(/(\d+)/gim, error!.message).map(Number); * expect(matches).toHaveLength(2) * const [matchedExpected, matchedActual] = matches; */ const { expected: matchedExpected, actual: matchedActual } = JSON.parse( error?.message ); expect(matchedExpected).toEqual(expectedArgCount); expect(matchedActual).toEqual(actualArgCount); } ); }); describe('API side (invalid)', () => { let spy: jest.SpyInstance; beforeEach(() => { spy = spyOnCreateApi().mockImplementation(() => Promise.reject(new Error('Invalid!')) ); }); // these will have other types as well const invalidApiTestCases: { [K in IInvalidApiMockKeyType]: { message: string }; } = { duplicateDistanceRecord: { message: 'Invalid!', }, labelTooLong: { message: 'Invalid!', }, nameTooLong: { message: 'Invalid!', }, negativeTtl: { message: 'Invalid!', }, escapedCharacters: { message: 'Invalid!', }, cnameLoop1: { message: 'Invalid!', }, cnameLoop2: { message: 'Invalid!', }, cnameLoop3: { message: 'Invalid!', }, cnameForExistingRecord: { message: 'Invalid!', }, mxExample1: { message: 'Invalid!', }, invalidIpForAAAA: { message: 'Invalid!', }, invalidIpv6ForA: { message: 'Invalid!', }, invalidIpv4ForAAAA: { message: 'Invalid!', }, invalidIpForA: { message: 'Invalid!', }, txtExampleInvalidSubdomain: { message: 'Invalid!', }, caaExampleBadParams: { message: 'Invalid!', }, caaExampleNotEmailForIoDef: { message: 'Invalid!', }, naptrExampleInvalid: { message: 'Invalid!', }, }; it.each( Object.keys(invalidApiTestCases) as Array< keyof typeof invalidApiTestCases > )('Should throw - `%s`', async key => { const input = invalidApiMock[key]; const expected = invalidApiTestCases[key]; expect.assertions(2); try { await testImportZoneFileAndCallApi(input); } catch (error) { expect(error!.message).toEqual(expected.message); } expect(spy).toHaveBeenCalled(); }); }); });