dns-providers.test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import assert from 'node:assert/strict';
  2. import { afterEach, test } from 'node:test';
  3. import { applyDnsSetup, testDnsCredential } from '../src/dns-providers.js';
  4. const originalFetch = globalThis.fetch;
  5. afterEach(() => {
  6. globalThis.fetch = originalFetch;
  7. });
  8. test('cloudflare provider tests credentials and replaces duplicate SPF records', async () => {
  9. const calls = [];
  10. globalThis.fetch = async (url, options = {}) => {
  11. calls.push({ url: String(url), method: options.method || 'GET', body: options.body });
  12. if (String(url).includes('/zones?')) return json({ success: true, result: [{ id: 'zone-1', name: 'example.com' }] });
  13. if (String(url).includes('/zones/zone-1') && !String(url).includes('/dns_records')) {
  14. return json({ success: true, result: { id: 'zone-1', name: 'example.com' } });
  15. }
  16. if (String(url).includes('/dns_records?')) {
  17. return json({
  18. success: true,
  19. result: [
  20. { id: 'spf-1', type: 'TXT', name: 'example.com', content: 'v=spf1 include:old ~all' },
  21. { id: 'spf-2', type: 'TXT', name: 'example.com', content: 'v=spf1 include:duplicate ~all' }
  22. ]
  23. });
  24. }
  25. return json({ success: true, result: { id: 'ok' } });
  26. };
  27. const credential = cloudflareCredential();
  28. assert.equal((await testDnsCredential(credential)).ok, true);
  29. const result = await applyDnsSetup(domainFixture(), credential, {
  30. records: [{ key: 'spf', host: 'example.com', type: 'TXT', value: 'v=spf1 ip4:127.0.0.1 ~all' }]
  31. });
  32. assert.equal(result.ok, true);
  33. assert.ok(calls.some((call) => call.method === 'PUT' && call.url.includes('/dns_records/spf-1')));
  34. assert.ok(calls.some((call) => call.method === 'DELETE' && call.url.includes('/dns_records/spf-2')));
  35. });
  36. test('aliyun provider signs and sends create/update record actions', async () => {
  37. const actions = [];
  38. globalThis.fetch = async (url) => {
  39. const params = new URL(String(url)).searchParams;
  40. const action = params.get('Action');
  41. actions.push(action);
  42. if (action === 'DescribeDomainRecords') {
  43. return json({
  44. DomainRecords: {
  45. Record: [{ RecordId: '1', RR: '_dmarc', Type: 'TXT', Value: 'v=DMARC1; p=none' }]
  46. }
  47. });
  48. }
  49. return json({});
  50. };
  51. const credential = aliyunCredential();
  52. assert.equal((await testDnsCredential(credential)).ok, true);
  53. const result = await applyDnsSetup(domainFixture(), credential, {
  54. records: [{ key: 'dmarc', host: '_dmarc.example.com', type: 'TXT', value: 'v=DMARC1; p=reject' }]
  55. });
  56. assert.equal(result.ok, true);
  57. assert.ok(actions.includes('UpdateDomainRecord'));
  58. });
  59. test('aliyun one-click dns falls back to parent zone for subdomain sending domains', async () => {
  60. const calls = [];
  61. globalThis.fetch = async (url) => {
  62. const params = new URL(String(url)).searchParams;
  63. calls.push({
  64. action: params.get('Action'),
  65. domainName: params.get('DomainName'),
  66. rr: params.get('RR')
  67. });
  68. if (params.get('DomainName') === 'notify.example.com') {
  69. return json({
  70. Code: 'InvalidDomainName.NoExist',
  71. Message: 'domain not found'
  72. });
  73. }
  74. if (params.get('Action') === 'DescribeDomainRecords') {
  75. return json({ DomainRecords: { Record: [] } });
  76. }
  77. return json({});
  78. };
  79. const result = await applyDnsSetup(
  80. { ...domainFixture(), domain: 'notify.example.com', senderHost: 'smtp.example.com' },
  81. { ...aliyunCredential(), zoneName: 'notify.example.com' },
  82. {
  83. records: [
  84. {
  85. key: 'verification',
  86. host: '_mailhub.notify.example.com',
  87. type: 'TXT',
  88. value: 'mailhub-verification=token',
  89. status: 'missing'
  90. }
  91. ]
  92. }
  93. );
  94. assert.equal(result.ok, true);
  95. assert.ok(calls.some((call) => call.action === 'DescribeDomainRecords' && call.domainName === 'notify.example.com'));
  96. assert.ok(calls.some((call) => call.action === 'DescribeDomainRecords' && call.domainName === 'example.com'));
  97. assert.ok(calls.some((call) => (
  98. call.action === 'AddDomainRecord'
  99. && call.domainName === 'example.com'
  100. && call.rr === '_mailhub.notify'
  101. )));
  102. });
  103. test('dnspod provider signs and sends create record actions', async () => {
  104. const actions = [];
  105. globalThis.fetch = async (url, options = {}) => {
  106. assert.equal(String(url), 'https://dnspod.tencentcloudapi.com');
  107. actions.push(options.headers['X-TC-Action']);
  108. if (options.headers['X-TC-Action'] === 'DescribeRecordList') {
  109. return json({ Response: { RecordList: [] } });
  110. }
  111. return json({ Response: { RecordId: 123 } });
  112. };
  113. const credential = dnspodCredential();
  114. assert.equal((await testDnsCredential(credential)).ok, true);
  115. const result = await applyDnsSetup(domainFixture(), credential, {
  116. records: [{ key: 'dkim', host: 'mh._domainkey.example.com', type: 'TXT', value: 'v=DKIM1; k=rsa; p=abc' }]
  117. });
  118. assert.equal(result.ok, true);
  119. assert.ok(actions.includes('CreateRecord'));
  120. });
  121. test('dnspod one-click dns falls back to parent zone for subdomain sending domains', async () => {
  122. const calls = [];
  123. globalThis.fetch = async (url, options = {}) => {
  124. assert.equal(String(url), 'https://dnspod.tencentcloudapi.com');
  125. const payload = JSON.parse(options.body);
  126. calls.push({ action: options.headers['X-TC-Action'], payload });
  127. if (payload.Domain === 'notify.example.com') {
  128. return json({
  129. Response: {
  130. Error: {
  131. Code: 'ResourceNotFound.NoDataOfRecord',
  132. Message: 'domain not found'
  133. }
  134. }
  135. });
  136. }
  137. if (options.headers['X-TC-Action'] === 'DescribeRecordList') {
  138. return json({ Response: { RecordList: [] } });
  139. }
  140. return json({ Response: { RecordId: 123 } });
  141. };
  142. const result = await applyDnsSetup(
  143. { ...domainFixture(), domain: 'notify.example.com', senderHost: 'smtp.example.com' },
  144. { ...dnspodCredential(), zoneName: 'notify.example.com' },
  145. {
  146. records: [
  147. {
  148. key: 'verification',
  149. host: '_mailhub.notify.example.com',
  150. type: 'TXT',
  151. value: 'mailhub-verification=token',
  152. status: 'missing'
  153. }
  154. ]
  155. }
  156. );
  157. assert.equal(result.ok, true);
  158. assert.ok(calls.some((call) => call.action === 'DescribeRecordList' && call.payload.Domain === 'notify.example.com'));
  159. assert.ok(calls.some((call) => call.action === 'DescribeRecordList' && call.payload.Domain === 'example.com'));
  160. assert.ok(calls.some((call) => (
  161. call.action === 'CreateRecord'
  162. && call.payload.Domain === 'example.com'
  163. && call.payload.SubDomain === '_mailhub.notify'
  164. )));
  165. });
  166. test('one-click dns setup only applies records under the user domain zone', async () => {
  167. const calls = [];
  168. globalThis.fetch = async (url, options = {}) => {
  169. calls.push({ url: String(url), method: options.method || 'GET' });
  170. if (String(url).includes('/zones?')) return json({ success: true, result: [{ id: 'zone-1', name: 'example.com' }] });
  171. if (String(url).includes('/dns_records?')) return json({ success: true, result: [] });
  172. return json({ success: true, result: { id: 'ok' } });
  173. };
  174. const result = await applyDnsSetup(domainFixture(), cloudflareCredential(), {
  175. records: [
  176. {
  177. key: 'dkim',
  178. host: 'mh._domainkey.example.com',
  179. type: 'TXT',
  180. value: 'v=DKIM1; k=rsa; p=abc',
  181. status: 'missing'
  182. },
  183. {
  184. key: 'sender-a',
  185. host: 'smtp.example.com',
  186. type: 'A',
  187. value: '127.0.0.1',
  188. status: 'ok'
  189. }
  190. ]
  191. });
  192. assert.equal(result.ok, true);
  193. assert.equal(result.results.length, 1);
  194. assert.equal(result.results[0].key, 'dkim');
  195. assert.equal(calls.filter((call) => call.method === 'POST').length, 1);
  196. });
  197. test('cloudflare one-click dns can use the current domain zone with a multi-zone token', async () => {
  198. const calls = [];
  199. globalThis.fetch = async (url, options = {}) => {
  200. calls.push({ url: String(url), method: options.method || 'GET' });
  201. if (String(url).includes('/zones?name=other.com')) {
  202. return json({ success: true, result: [{ id: 'zone-other', name: 'other.com' }] });
  203. }
  204. if (String(url).includes('/dns_records?')) return json({ success: true, result: [] });
  205. return json({ success: true, result: { id: 'ok' } });
  206. };
  207. const result = await applyDnsSetup(
  208. { ...domainFixture(), domain: 'other.com', senderHost: 'mail.other.com' },
  209. cloudflareCredential(),
  210. {
  211. records: [
  212. {
  213. key: 'verification',
  214. host: '_mailhub.other.com',
  215. type: 'TXT',
  216. value: 'mailhub-verification=token',
  217. status: 'missing'
  218. }
  219. ]
  220. }
  221. );
  222. assert.equal(result.ok, true);
  223. assert.equal(result.results[0].ok, true);
  224. assert.ok(calls.some((call) => call.url.includes('/zones?name=other.com')));
  225. assert.ok(calls.some((call) => call.method === 'POST' && call.url.includes('/zones/zone-other/dns_records')));
  226. });
  227. test('cloudflare one-click dns discovers the parent zone for subdomain sending domains', async () => {
  228. const calls = [];
  229. globalThis.fetch = async (url, options = {}) => {
  230. calls.push({ url: String(url), method: options.method || 'GET' });
  231. if (String(url).includes('/zones?name=sender.example.com')) {
  232. return json({ success: true, result: [] });
  233. }
  234. if (String(url).includes('/zones?name=example.com')) {
  235. return json({ success: true, result: [{ id: 'zone-example', name: 'example.com' }] });
  236. }
  237. if (String(url).includes('/dns_records?')) return json({ success: true, result: [] });
  238. return json({ success: true, result: { id: 'ok' } });
  239. };
  240. const result = await applyDnsSetup(
  241. { ...domainFixture(), domain: 'sender.example.com', senderHost: 'smtp.example.com' },
  242. { ...cloudflareCredential(), zoneName: 'example.org' },
  243. {
  244. records: [
  245. {
  246. key: 'verification',
  247. host: '_mailhub.sender.example.com',
  248. type: 'TXT',
  249. value: 'mailhub-verification=token',
  250. status: 'missing'
  251. }
  252. ]
  253. }
  254. );
  255. assert.equal(result.ok, true);
  256. assert.equal(result.results[0].ok, true);
  257. assert.ok(calls.some((call) => call.url.includes('/zones?name=sender.example.com')));
  258. assert.ok(calls.some((call) => call.url.includes('/zones?name=example.com')));
  259. assert.ok(calls.some((call) => call.method === 'POST' && call.url.includes('/zones/zone-example/dns_records')));
  260. });
  261. test('cloudflare one-click dns falls back from configured child zone to parent zone', async () => {
  262. const calls = [];
  263. globalThis.fetch = async (url, options = {}) => {
  264. calls.push({ url: String(url), method: options.method || 'GET' });
  265. if (String(url).includes('/zones?name=notify.example.com')) {
  266. return json({ success: true, result: [] });
  267. }
  268. if (String(url).includes('/zones?name=example.com')) {
  269. return json({ success: true, result: [{ id: 'zone-example', name: 'example.com' }] });
  270. }
  271. if (String(url).includes('/dns_records?')) return json({ success: true, result: [] });
  272. return json({ success: true, result: { id: 'ok' } });
  273. };
  274. const result = await applyDnsSetup(
  275. { ...domainFixture(), domain: 'notify.example.com', senderHost: 'smtp.example.com' },
  276. { ...cloudflareCredential(), zoneName: 'notify.example.com' },
  277. {
  278. records: [
  279. {
  280. key: 'verification',
  281. host: '_mailhub.notify.example.com',
  282. type: 'TXT',
  283. value: 'mailhub-verification=token',
  284. status: 'missing'
  285. }
  286. ]
  287. }
  288. );
  289. assert.equal(result.ok, true);
  290. assert.equal(result.results[0].ok, true);
  291. assert.ok(calls.some((call) => call.url.includes('/zones?name=notify.example.com')));
  292. assert.ok(calls.some((call) => call.url.includes('/zones?name=example.com')));
  293. assert.ok(calls.some((call) => call.method === 'POST' && call.url.includes('/zones/zone-example/dns_records')));
  294. });
  295. function cloudflareCredential() {
  296. return {
  297. provider: 'cloudflare',
  298. zoneName: 'example.com',
  299. defaultTtl: 600,
  300. credentials: { apiToken: 'token' }
  301. };
  302. }
  303. function aliyunCredential() {
  304. return {
  305. provider: 'aliyun',
  306. zoneName: 'example.com',
  307. defaultTtl: 600,
  308. credentials: { accessKeyId: 'id', accessKeySecret: 'secret' }
  309. };
  310. }
  311. function dnspodCredential() {
  312. return {
  313. provider: 'dnspod',
  314. zoneName: 'example.com',
  315. defaultTtl: 600,
  316. credentials: { secretId: 'id', secretKey: 'secret' }
  317. };
  318. }
  319. function domainFixture() {
  320. return {
  321. domain: 'example.com',
  322. senderHost: 'mail.example.com',
  323. sendingIp: '127.0.0.1'
  324. };
  325. }
  326. function json(payload) {
  327. return {
  328. ok: true,
  329. status: 200,
  330. async json() {
  331. return payload;
  332. }
  333. };
  334. }