/** * DC-170/DC-171: GET /api/v1/dns/records — the DNS record-listing route added * so the MCP dashcaddy_list_dns tool (and any client) can list records via the * ACTIVE provider, with honest capability behavior: * - provider supports list-records -> { zone, count, records } * - provider without list-records (e.g. RFC2136 push-only) -> 501 + guidance * - no active provider resolvable -> 501 * Also pins the zone passthrough and the siteConfig.tld default. */ 'use strict'; const express = require('express'); const request = require('supertest'); function buildApp(dnsDep, siteConfig) { const dnsRoutes = require('../../routes/dns'); const app = express(); app.use(express.json()); app.use('/dns', dnsRoutes({ dns: dnsDep, siteConfig: siteConfig || { tld: '.example.net' }, asyncHandler: (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next), log: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }, safeErrorMessage: (e) => (e && e.message) || 'error', fetchT: async () => { throw new Error('not used here'); }, credentialManager: { retrieve: async () => null }, dnsPropagationChecker: {}, })); return app; } const GOOD_PROVIDER = { supportsCapability: (cap) => cap === 'list-records', listRecords: jest.fn(async ({ zone }) => ({ records: [ { name: `host.${zone}`, type: 'A', value: '10.0.0.5' }, { name: `nas.${zone}`, type: 'A', value: '10.0.0.6' }, ] })), }; describe('GET /dns/records (DC-170 route + DC-171 capability behavior)', () => { test('lists records via the active provider when list-records is supported', async () => { const dns = { getActiveProvider: () => GOOD_PROVIDER }; const res = await request(buildApp(dns)).get('/dns/records'); expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.zone).toBe('example.net'); // from siteConfig.tld expect(res.body.count).toBe(2); expect(res.body.records[0]).toMatchObject({ type: 'A', value: '10.0.0.5' }); expect(GOOD_PROVIDER.listRecords).toHaveBeenCalledWith({ zone: 'example.net' }); }); test('zone query param overrides the siteConfig.tld default', async () => { const dns = { getActiveProvider: () => GOOD_PROVIDER }; const res = await request(buildApp(dns)).get('/dns/records?zone=custom.zone'); expect(res.status).toBe(200); expect(res.body.zone).toBe('custom.zone'); expect(GOOD_PROVIDER.listRecords).toHaveBeenCalledWith({ zone: 'custom.zone' }); }); test('provider WITHOUT list-records capability answers 501 with guidance', async () => { // RFC2136-style: push-only protocol, cannot enumerate records const dns = { getActiveProvider: () => ({ supportsCapability: () => false }) }; const res = await request(buildApp(dns)).get('/dns/records'); expect(res.status).toBe(501); expect(String(res.body.error || res.body.message)).toMatch(/not supported/i); }); test('missing/unresolvable active provider answers 501, does not crash', async () => { const dns = {}; // no getActiveProvider at all const res = await request(buildApp(dns)).get('/dns/records'); expect(res.status).toBe(501); }); test('provider listRecords failure surfaces as 500 without leaking internals', async () => { const dns = { getActiveProvider: () => ({ supportsCapability: () => true, listRecords: async () => { throw new Error('upstream token expired with secret abc'); }, }) }; const res = await request(buildApp(dns)).get('/dns/records'); expect([500, 502]).toContain(res.status); }); });