diff --git a/backend/package-lock.json b/backend/package-lock.json index cfe22d916..a871c38b1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -177,6 +177,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-simple-import-sort": "^10.0.0", + "nock": "^14.0.10", "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", @@ -9705,6 +9706,24 @@ "win32" ] }, + "node_modules/@mswjs/interceptors": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@next/env": { "version": "15.5.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", @@ -10714,6 +10733,31 @@ "urijs": "^1.19.11" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -22958,6 +23002,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -23507,6 +23558,13 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -25074,6 +25132,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nock": { + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">=18.20.0 <20 || >=20.12.1" + } + }, "node_modules/node-abi": { "version": "3.65.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", @@ -27702,6 +27775,13 @@ "@otplib/preset-v11": "^12.0.1" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -29103,6 +29183,16 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -31601,6 +31691,13 @@ "node": ">=4.0.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index 7a5efcb78..aa97de2ed 100644 --- a/backend/package.json +++ b/backend/package.json @@ -123,6 +123,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-simple-import-sort": "^10.0.0", + "nock": "^14.0.10", "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6f0502184..fa649dd8e 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -106,6 +106,7 @@ const envSchema = z HTTPS_ENABLED: zodStrBool, ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), + BDD_MOCK_THIRD_PARTY_API_CALLS: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr( z @@ -398,6 +399,7 @@ const envSchema = z isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS), + shouldMockThirdPartyApiCalls: data.NODE_ENV === "development" && data.BDD_MOCK_THIRD_PARTY_API_CALLS, REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim() ?.split(",") .map((el) => { diff --git a/backend/src/mock-third-party-api-calls.ts b/backend/src/mock-third-party-api-calls.ts new file mode 100644 index 000000000..521fc77bb --- /dev/null +++ b/backend/src/mock-third-party-api-calls.ts @@ -0,0 +1,87 @@ +import nock from "nock"; + +export const mockThirdPartyApiCalls = () => { + nock("https://api.cloudflare.com:443") + .get("/client/v4/accounts/MOCK_ACCOUNT_ID") + .reply(200, { + result: { + id: "A2A6347F-88B5-442D-9798-95E408BC7701", + name: "Mock Account", + type: "standard", + settings: { + enforce_twofactor: true, + api_access_enabled: null, + access_approval_expiry: null, + abuse_contact_email: null, + user_groups_ui_beta: false + }, + legacy_flags: { + enterprise_zone_quota: { maximum: 0, current: 0, available: 0 } + }, + created_on: "2013-04-18T00:41:02.215243Z" + }, + success: true, + errors: [], + messages: [] + }); + + nock("https://api.cloudflare.com:443") + .get("/client/v4/zones") + .reply(200, { + result: [ + { + id: "2DF47D5E-7FE2-4BD9-8503-BF27ACE6EBE5", + name: "example.com", + status: "active", + paused: false, + type: "full", + development_mode: 0, + name_servers: ["abby.ns.cloudflare.com", "cody.ns.cloudflare.com"], + original_name_servers: ["ns1gmz.name.com", "ns2fgp.name.com", "ns3jwx.name.com", "ns4lny.name.com"], + original_registrar: "name.com, inc. (id: 625)", + original_dnshost: null, + modified_on: "2025-11-05T18:08:57.046348Z", + created_on: "2025-11-05T18:05:52.536690Z", + activated_on: "2025-11-05T18:08:57.046348Z", + vanity_name_servers: [], + vanity_name_servers_ips: null, + meta: { + step: 2, + custom_certificate_quota: 0, + page_rule_quota: 3, + phishing_detected: false + }, + owner: { id: null, type: "user", email: null }, + account: { + id: "A2A6347F-88B5-442D-9798-95E408BC7701", + name: "Mock Account" + }, + tenant: { id: null, name: null }, + tenant_unit: { id: null }, + permissions: ["#dns_records:edit", "#dns_records:read", "#zone:read"], + plan: { + id: "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + name: "Free Website", + price: 0, + currency: "USD", + frequency: "", + is_subscribed: false, + can_subscribe: false, + legacy_id: "free", + legacy_discount: false, + externally_managed: false + } + } + ], + result_info: { + page: 1, + per_page: 20, + total_pages: 1, + count: 1, + total_count: 1 + }, + success: true, + errors: [], + messages: [] + }); +}; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 60b678f63..3ced74160 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -26,6 +26,7 @@ import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { globalRateLimiterCfg } from "./config/rateLimiter"; +import { mockThirdPartyApiCalls } from "./mock-third-party-api-calls"; import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas"; import { apiMetrics } from "./plugins/api-metrics"; import { fastifyErrHandler } from "./plugins/error-handler"; @@ -66,6 +67,12 @@ export const main = async ({ }: TMain) => { const appCfg = getConfig(); + if (appCfg.shouldMockThirdPartyApiCalls) { + logger?.info("Mocking third party API calls for BDD"); + // Note: to make BDD tests much easier, we mock some third party API calls here + mockThirdPartyApiCalls(); + } + const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, genReqId: () => `req-${alphaNumericNanoId(14)}`,