DC-005: Fix all 138 broken test paths after src/ refactor
After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Cloudflare DNS Provider Adapter
|
||||
* Manages DNS records via the Cloudflare API v4.
|
||||
*/
|
||||
const BaseDNSProvider = require('./base');
|
||||
|
||||
const CF_API_BASE = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
class CloudflareDNSProvider extends BaseDNSProvider {
|
||||
constructor(config, ctx) {
|
||||
super(config, ctx);
|
||||
this.providerId = 'cloudflare';
|
||||
this.displayName = 'Cloudflare DNS';
|
||||
|
||||
// Resolve API token: explicit config takes priority, then credential manager
|
||||
this.apiToken = config.apiToken
|
||||
|| (ctx.credentialManager && ctx.credentialManager.get('dns.cloudflare.apiToken'))
|
||||
|| null;
|
||||
this.zoneId = config.zoneId || null;
|
||||
this.domain = config.domain || null;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Build common request headers for Cloudflare API calls */
|
||||
_headers() {
|
||||
return {
|
||||
'Authorization': `Bearer ${this.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
/** Make an authenticated request to the Cloudflare API */
|
||||
async _cfRequest(method, path, body) {
|
||||
const url = `${CF_API_BASE}${path}`;
|
||||
const opts = {
|
||||
method,
|
||||
headers: this._headers(),
|
||||
};
|
||||
if (body !== undefined) {
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
return this.ctx.fetchT(url, opts);
|
||||
}
|
||||
|
||||
/** Map a Cloudflare DNS record to the normalised format expected by routes */
|
||||
_mapRecord(rec) {
|
||||
return {
|
||||
id: rec.id,
|
||||
type: rec.type,
|
||||
name: rec.name,
|
||||
value: rec.content,
|
||||
ttl: rec.ttl,
|
||||
proxied: rec.proxied || false,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Capabilities ───────────────────────────────────────────────────────
|
||||
|
||||
supportsCapability(cap) {
|
||||
return this.getCapabilities().includes(cap);
|
||||
}
|
||||
|
||||
getCapabilities() {
|
||||
return ['create-record', 'delete-record', 'resolve', 'list-records', 'credentials', 'zones'];
|
||||
}
|
||||
|
||||
// ── Authentication ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate the API token by calling the Cloudflare verify endpoint.
|
||||
* Stores basic zone info on success.
|
||||
*/
|
||||
async authenticate() {
|
||||
this.ctx.log('[cloudflare] Authenticating – verifying API token…');
|
||||
|
||||
if (!this.apiToken) {
|
||||
return { status: 'error', message: 'No Cloudflare API token provided' };
|
||||
}
|
||||
|
||||
const res = await this._cfRequest('GET', '/user/tokens/verify');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
const msg = (data.errors && data.errors[0] && data.errors[0].message) || 'Token verification failed';
|
||||
this.ctx.log(`[cloudflare] Authentication failed: ${msg}`);
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
|
||||
this.ctx.log(`[cloudflare] Token verified for status "${data.status}"`);
|
||||
|
||||
// Optionally fetch zone info if zoneId is configured
|
||||
if (this.zoneId) {
|
||||
try {
|
||||
const zoneRes = await this._cfRequest('GET', `/zones/${this.zoneId}`);
|
||||
const zoneData = await zoneRes.json();
|
||||
if (zoneData.success && zoneData.result) {
|
||||
this.zoneInfo = zoneData.result;
|
||||
this.ctx.log(`[cloudflare] Zone loaded: ${zoneData.result.name} (${zoneData.result.id})`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.ctx.log(`[cloudflare] Could not fetch zone info: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'ok', response: { status: data.status } };
|
||||
}
|
||||
|
||||
// ── Create Record ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a DNS record.
|
||||
* If overwrite is true, first delete any existing record with the same name+type.
|
||||
*/
|
||||
async createRecord({ domain, zone, type, value, ttl, overwrite }) {
|
||||
const targetDomain = domain || this.domain;
|
||||
const targetZone = zone || this.zoneId;
|
||||
|
||||
if (!targetZone) {
|
||||
return { status: 'error', message: 'No zone ID configured for Cloudflare' };
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
this.ctx.log(`[cloudflare] Overwrite requested – deleting existing ${type} record for ${targetDomain}`);
|
||||
try {
|
||||
await this.deleteRecord({ domain: targetDomain, type, value });
|
||||
} catch (err) {
|
||||
this.ctx.log(`[cloudflare] No existing record to overwrite (or delete failed): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
type,
|
||||
name: targetDomain,
|
||||
content: value,
|
||||
ttl: ttl || 1, // 1 = automatic TTL in Cloudflare
|
||||
proxied: false,
|
||||
};
|
||||
|
||||
this.ctx.log(`[cloudflare] Creating ${type} record: ${targetDomain} → ${value}`);
|
||||
const res = await this._cfRequest('POST', `/zones/${targetZone}/dns_records`, body);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
const msg = (data.errors && data.errors[0] && data.errors[0].message) || 'Record creation failed';
|
||||
this.ctx.log(`[cloudflare] Create failed: ${msg}`);
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
|
||||
return { status: 'ok', response: { record: this._mapRecord(data.result) } };
|
||||
}
|
||||
|
||||
// ── Delete Record ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Delete DNS records matching domain+type.
|
||||
* Lists matching records first, then deletes each one.
|
||||
*/
|
||||
async deleteRecord({ domain, type, value }) {
|
||||
const targetDomain = domain || this.domain;
|
||||
const targetZone = this.zoneId;
|
||||
|
||||
if (!targetZone) {
|
||||
return { status: 'error', message: 'No zone ID configured for Cloudflare' };
|
||||
}
|
||||
|
||||
// List records matching name + type
|
||||
let queryPath = `/zones/${targetZone}/dns_records?name=${encodeURIComponent(targetDomain)}`;
|
||||
if (type) {
|
||||
queryPath += `&type=${encodeURIComponent(type)}`;
|
||||
}
|
||||
|
||||
const listRes = await this._cfRequest('GET', queryPath);
|
||||
const listData = await listRes.json();
|
||||
|
||||
if (!listData.success) {
|
||||
const msg = (listData.errors && listData.errors[0] && listData.errors[0].message) || 'Failed to list records for deletion';
|
||||
this.ctx.log(`[cloudflare] Delete – list failed: ${msg}`);
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
|
||||
const matching = listData.result || [];
|
||||
if (matching.length === 0) {
|
||||
this.ctx.log(`[cloudflare] No records found for ${targetDomain} (${type || 'any type'})`);
|
||||
return { status: 'ok', response: { deleted: 0 } };
|
||||
}
|
||||
|
||||
// If a specific value is given, only delete records matching that value
|
||||
const toDelete = value
|
||||
? matching.filter((r) => r.content === value)
|
||||
: matching;
|
||||
|
||||
let deleted = 0;
|
||||
for (const record of toDelete) {
|
||||
const delRes = await this._cfRequest('DELETE', `/zones/${targetZone}/dns_records/${record.id}`);
|
||||
const delData = await delRes.json();
|
||||
if (delData.success) {
|
||||
deleted++;
|
||||
this.ctx.log(`[cloudflare] Deleted record ${record.id} (${record.type} ${record.name})`);
|
||||
} else {
|
||||
const msg = (delData.errors && delData.errors[0] && delData.errors[0].message) || 'Delete failed';
|
||||
this.ctx.log(`[cloudflare] Failed to delete record ${record.id}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'ok', response: { deleted } };
|
||||
}
|
||||
|
||||
// ── Resolve Records ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve/query existing records for a domain.
|
||||
* Returns records matching domain (and optionally type).
|
||||
*/
|
||||
async resolveRecords({ domain, zone, type }) {
|
||||
const targetDomain = domain || this.domain;
|
||||
const targetZone = zone || this.zoneId;
|
||||
|
||||
if (!targetZone) {
|
||||
return { status: 'error', message: 'No zone ID configured for Cloudflare' };
|
||||
}
|
||||
|
||||
let queryPath = `/zones/${targetZone}/dns_records?name=${encodeURIComponent(targetDomain)}`;
|
||||
if (type) {
|
||||
queryPath += `&type=${encodeURIComponent(type)}`;
|
||||
}
|
||||
|
||||
this.ctx.log(`[cloudflare] Resolving records for ${targetDomain}${type ? ` (${type})` : ''}`);
|
||||
const res = await this._cfRequest('GET', queryPath);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
const msg = (data.errors && data.errors[0] && data.errors[0].message) || 'Resolve failed';
|
||||
this.ctx.log(`[cloudflare] Resolve failed: ${msg}`);
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
|
||||
const records = (data.result || []).map(this._mapRecord);
|
||||
return { status: 'ok', response: { records } };
|
||||
}
|
||||
|
||||
// ── List Records ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* List all DNS records in a zone.
|
||||
*/
|
||||
async listRecords({ zone }) {
|
||||
const targetZone = zone || this.zoneId;
|
||||
|
||||
if (!targetZone) {
|
||||
return { status: 'error', message: 'No zone ID configured for Cloudflare' };
|
||||
}
|
||||
|
||||
this.ctx.log(`[cloudflare] Listing all records in zone ${targetZone}`);
|
||||
const res = await this._cfRequest('GET', `/zones/${targetZone}/dns_records`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
const msg = (data.errors && data.errors[0] && data.errors[0].message) || 'List failed';
|
||||
this.ctx.log(`[cloudflare] List failed: ${msg}`);
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
|
||||
const records = (data.result || []).map(this._mapRecord);
|
||||
return { status: 'ok', response: { records } };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CloudflareDNSProvider;
|
||||
Reference in New Issue
Block a user