From 0038bd6755eac0bebd935f3ef342697622e924bc Mon Sep 17 00:00:00 2001 From: Nicholas Morsman Date: Tue, 16 Jul 2024 12:21:47 +0200 Subject: [PATCH] Add utility method tests --- src/util.js | 21 +++--- test/10-util.spec.js | 120 ++++++++++++++++++++++++++++++++++ test/fixtures/letsencrypt.crt | 23 +++++++ 3 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 test/10-util.spec.js create mode 100644 test/fixtures/letsencrypt.crt diff --git a/src/util.js b/src/util.js index be2cdd4..6a54e73 100644 --- a/src/util.js +++ b/src/util.js @@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) { } /** - * Parse URLs from link header + * Parse URLs from Link header * - * @param {string} header Link header contents + * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2 + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + * + * @param {string} header Header contents * @param {string} rel Link relation, default: `alternate` * @returns {string[]} Array of URLs */ @@ -161,14 +164,16 @@ function findCertificateChainForIssuer(chains, issuer) { function formatResponseError(resp) { let result; - if (resp.data.error) { - result = resp.data.error.detail || resp.data.error; - } - else { - result = resp.data.detail || JSON.stringify(resp.data); + if (resp.data) { + if (resp.data.error) { + result = resp.data.error.detail || resp.data.error; + } + else { + result = resp.data.detail || JSON.stringify(resp.data); + } } - return result.replace(/\n/g, ''); + return (result || '').replace(/\n/g, ''); } /** diff --git a/test/10-util.spec.js b/test/10-util.spec.js new file mode 100644 index 0000000..a975eb2 --- /dev/null +++ b/test/10-util.spec.js @@ -0,0 +1,120 @@ +/** + * Utility method tests + */ + +const dns = require('dns').promises; +const fs = require('fs').promises; +const path = require('path'); +const { assert } = require('chai'); +const util = require('./../src/util'); +const { readCertificateInfo } = require('./../src/crypto'); + +describe('util', () => { + const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt'); + const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt'); + + it('retry()', async () => { + let attempts = 0; + const backoffOpts = { + min: 100, + max: 500, + }; + + await assert.isRejected(util.retry(() => { + throw new Error('oops'); + }, backoffOpts)); + + const r = await util.retry(() => { + attempts += 1; + + if (attempts < 3) { + throw new Error('oops'); + } + + return 'abc'; + }, backoffOpts); + + assert.strictEqual(r, 'abc'); + assert.strictEqual(attempts, 3); + }); + + it('parseLinkHeader()', () => { + const r1 = util.parseLinkHeader(';rel="alternate"'); + assert.isArray(r1); + assert.strictEqual(r1.length, 1); + assert.strictEqual(r1[0], 'https://example.com/a'); + + const r2 = util.parseLinkHeader(';rel="test"'); + assert.isArray(r2); + assert.strictEqual(r2.length, 0); + + const r3 = util.parseLinkHeader('; rel="test"', 'test'); + assert.isArray(r3); + assert.strictEqual(r3.length, 1); + assert.strictEqual(r3[0], 'http://example.com/c'); + + const r4 = util.parseLinkHeader(`; rel="alternate", + ; rel="nope", + ;rel="alternate", + ; rel="alternate"`); + assert.isArray(r4); + assert.strictEqual(r4.length, 3); + assert.strictEqual(r4[0], 'https://example.com/a'); + assert.strictEqual(r4[1], 'https://example.com/b'); + assert.strictEqual(r4[2], 'https://example.com/c'); + }); + + it('findCertificateChainForIssuer()', async () => { + const certs = [ + (await fs.readFile(testCertPath1)).toString(), + (await fs.readFile(testCertPath2)).toString(), + ]; + + const r1 = util.findCertificateChainForIssuer(certs, 'abc123'); + const r2 = util.findCertificateChainForIssuer(certs, 'example.com'); + const r3 = util.findCertificateChainForIssuer(certs, 'E6'); + + [r1, r2, r3].forEach((r) => { + assert.isString(r); + assert.isNotEmpty(r); + }); + + assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com'); + assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com'); + assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6'); + }); + + it('formatResponseError()', () => { + const e1 = util.formatResponseError({ data: { error: 'aaa' } }); + assert.strictEqual(e1, 'aaa'); + + const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } }); + assert.strictEqual(e2, 'bbb'); + + const e3 = util.formatResponseError({ data: { detail: 'ccc' } }); + assert.strictEqual(e3, 'ccc'); + + const e4 = util.formatResponseError({ data: { a: 123 } }); + assert.strictEqual(e4, '{"a":123}'); + + const e5 = util.formatResponseError({}); + assert.isString(e5); + assert.isEmpty(e5); + }); + + it('getAuthoritativeDnsResolver()', async () => { + /* valid domain - should not use global default */ + const r1 = await util.getAuthoritativeDnsResolver('example.com'); + assert.instanceOf(r1, dns.Resolver); + assert.isNotEmpty(r1.getServers()); + assert.notDeepEqual(r1.getServers(), dns.getServers()); + + /* invalid domain - fallback to global default */ + const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx'); + assert.instanceOf(r2, dns.Resolver); + assert.deepStrictEqual(r2.getServers(), dns.getServers()); + }); + + /* TODO: Figure out how to test this */ + it('retrieveTlsAlpnCertificate()'); +}); diff --git a/test/fixtures/letsencrypt.crt b/test/fixtures/letsencrypt.crt new file mode 100644 index 0000000..7efd44c --- /dev/null +++ b/test/fixtures/letsencrypt.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl +bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP +V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW +6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM +seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF +BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG +AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy +Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j +ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw +EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/ +F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC +IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh +ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq +AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR +LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG +CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9 +rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5 +aYWylBi/Uf2RPj0LWFZh8tNa1Q== +-----END CERTIFICATE-----