Files
proxy/nodejs/test/helpers/dns_provider_contract.js
T
2025-12-31 16:51:55 -05:00

281 lines
7.7 KiB
JavaScript

'use strict';
const assert = require('node:assert');
const {DnsApi} = require('../../models/dns_provider/common');
/**
* DNS Provider Contract Test Helper
*
* This module provides a contract test suite that validates a DNS provider
* implementation meets all requirements. Use this when adding new DNS providers
* to ensure they implement the required interface correctly.
*
* Required static properties:
* - _keyMap: Object defining required API credentials/config
* - displayName: String for UI display
* - displayIconHtml: SVG markup for provider icon
* - displayIconUni: Unicode icon fallback
*
* Required instance methods:
* - listDomains(): Returns array of {domain, zoneId}
* - getRecords(domain, options): Returns array of DNS records
* - createRecord(domain, options): Creates a record, returns created record
* - deleteRecords(domain, options): Deletes matching records
*
* Required behavior:
* - Must extend DnsApi base class
* - Must throw errors.unauthorized() on auth failures
* - Must implement __apiKeyMap for key translation
* - Must validate record types via __typeCheck
*/
/**
* Validates a DNS provider class meets the contract
*
* @param {Class} ProviderClass - The DNS provider class to validate
* @param {Object} mockCredentials - Mock credentials for testing
* @returns {void}
* @throws {AssertionError} If provider doesn't meet contract
*/
function validateDnsProviderContract(ProviderClass, mockCredentials) {
// Test 1: Must extend DnsApi
assert.ok(
ProviderClass.prototype instanceof DnsApi,
`${ProviderClass.name} must extend DnsApi base class`
);
// Test 2: Must have static _keyMap
assert.ok(
ProviderClass._keyMap,
`${ProviderClass.name} must define static _keyMap`
);
assert.strictEqual(
typeof ProviderClass._keyMap,
'object',
`${ProviderClass.name}._keyMap must be an object`
);
// Test 3: Must have display properties
assert.ok(
ProviderClass.displayName,
`${ProviderClass.name} must define static displayName`
);
assert.ok(
ProviderClass.displayIconHtml,
`${ProviderClass.name} must define static displayIconHtml`
);
assert.ok(
ProviderClass.displayIconUni,
`${ProviderClass.name} must define static displayIconUni`
);
// Test 4: Can be instantiated
let instance;
assert.doesNotThrow(
() => {
instance = new ProviderClass(mockCredentials);
},
`${ProviderClass.name} must be instantiable with mock credentials`
);
// Test 5: Must have required methods
const requiredMethods = [
'listDomains',
'getRecords',
'createRecord',
'deleteRecords'
];
for(let method of requiredMethods) {
assert.strictEqual(
typeof instance[method],
'function',
`${ProviderClass.name} must implement ${method}() method`
);
}
// Test 6: Must have __apiKeyMap for key translation
assert.ok(
instance.hasOwnProperty('__apiKeyMap') || instance.constructor.prototype.hasOwnProperty('__apiKeyMap'),
`${ProviderClass.name} must define __apiKeyMap property`
);
// Test 7: Must have __typeCheck method (inherited or overridden)
assert.strictEqual(
typeof instance.__typeCheck,
'function',
`${ProviderClass.name} must have __typeCheck method`
);
// Test 8: Must have error methods (inherited from DnsApi)
assert.ok(
instance.errors,
`${ProviderClass.name} must have errors object`
);
assert.strictEqual(
typeof instance.errors.unauthorized,
'function',
`${ProviderClass.name} must have errors.unauthorized method`
);
assert.strictEqual(
typeof instance.errors.invalidInput,
'function',
`${ProviderClass.name} must have errors.invalidInput method`
);
assert.strictEqual(
typeof instance.errors.other,
'function',
`${ProviderClass.name} must have errors.other method`
);
// Test 9: info() method should return expected structure
const info = ProviderClass.info();
assert.ok(info.displayName, 'info() must include displayName');
assert.ok(info.displayIconHtml, 'info() must include displayIconHtml');
assert.ok(info.displayIconUni, 'info() must include displayIconUni');
assert.ok(info.fields, 'info() must include fields');
// Test 10: toJSON() should work
const json = instance.toJSON();
assert.ok(json.displayName, 'toJSON() must include displayName');
return instance;
}
/**
* Validates method signatures for a DNS provider instance
*
* @param {Object} instance - Instance of DNS provider
* @param {Object} mockDomain - Mock domain object with {domain, zoneId}
* @returns {void}
*/
function validateMethodSignatures(instance, mockDomain = {domain: 'example.com', zoneId: 'mock-zone'}) {
const className = instance.constructor.name;
// These tests just verify the methods accept the expected parameters
// and return promises (actual API calls would require real credentials)
// listDomains() should return a promise
const listDomainsResult = instance.listDomains();
assert.ok(
listDomainsResult instanceof Promise,
`${className}.listDomains() must return a Promise`
);
// getRecords(domain, options) should return a promise
const getRecordsResult = instance.getRecords(mockDomain, {type: 'A'});
assert.ok(
getRecordsResult instanceof Promise,
`${className}.getRecords() must return a Promise`
);
// createRecord(domain, options) should return a promise
const createRecordResult = instance.createRecord(mockDomain, {
type: 'TXT',
name: 'test',
data: 'test-value'
});
assert.ok(
createRecordResult instanceof Promise,
`${className}.createRecord() must return a Promise`
);
// deleteRecords(domain, options) should return a promise
const deleteRecordsResult = instance.deleteRecords(mockDomain, {
type: 'TXT',
name: 'test'
});
assert.ok(
deleteRecordsResult instanceof Promise,
`${className}.deleteRecords() must return a Promise`
);
}
/**
* Validates __parseOptions and __parseRes behavior
*
* @param {Object} instance - Instance of DNS provider
* @returns {void}
*/
function validateKeyMapping(instance) {
const className = instance.constructor.name;
// Test __parseOptions normalizes keys
if(Object.keys(instance.__apiKeyMap).length > 0) {
const testOptions = {type: 'A'};
// Add a class key that should be mapped to API key
const [apiKey, clsKey] = Object.entries(instance.__apiKeyMap)[0];
testOptions[clsKey] = 'test-value';
const parsed = instance.__parseOptions(testOptions);
assert.ok(
parsed.hasOwnProperty(apiKey),
`${className}.__parseOptions() should map '${clsKey}' to '${apiKey}'`
);
assert.strictEqual(
parsed[clsKey],
undefined,
`${className}.__parseOptions() should remove class key '${clsKey}' after mapping`
);
}
// Test __parseRes normalizes response keys
const testResponse = [];
const [apiKey, clsKey] = Object.entries(instance.__apiKeyMap)[0] || ['content', 'data'];
testResponse.push({[apiKey]: 'test-value', name: 'test.example.com'});
const parsedRes = instance.__parseRes(testResponse);
if(Object.keys(instance.__apiKeyMap).length > 0) {
assert.ok(
parsedRes[0].hasOwnProperty(clsKey),
`${className}.__parseRes() should map '${apiKey}' to '${clsKey}'`
);
}
}
/**
* Validates type checking behavior
*
* @param {Object} instance - Instance of DNS provider
* @returns {void}
*/
function validateTypeChecking(instance) {
const className = instance.constructor.name;
const validTypes = ['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA'];
// Valid types should not throw
for(let type of validTypes) {
assert.doesNotThrow(
() => instance.__typeCheck(type),
`${className}.__typeCheck() should accept valid type '${type}'`
);
}
// Invalid type should throw
assert.throws(
() => instance.__typeCheck('INVALID'),
/Invalid.*type/i,
`${className}.__typeCheck() should reject invalid types`
);
}
module.exports = {
validateDnsProviderContract,
validateMethodSignatures,
validateKeyMapping,
validateTypeChecking
};