api_spec.js 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. var chakram = require("./../setup.js").chakram;
  2. var expect = chakram.expect;
  3. var itPropagatesToTheApi = require("./../setup.js").itPropagatesToTheApi;
  4. var itShowsUpInPdnsAs = require("./../setup.js").itShowsUpInPdnsAs;
  5. var schemas = require("./../schemas.js");
  6. var withCaptcha = require("./../setup.js").withCaptcha;
  7. describe("API Versioning", function () {
  8. before(function () {
  9. chakram.setRequestDefaults({
  10. headers: {
  11. 'Host': 'desec.' + process.env.DESECSTACK_DOMAIN,
  12. },
  13. followRedirect: false,
  14. baseUrl: 'https://www/api',
  15. })
  16. });
  17. [
  18. 'v1',
  19. 'v2',
  20. ].forEach(function (version) {
  21. it("maintains the requested version " + version, function() {
  22. chakram.get('/' + version + '/').then(function (response) {
  23. expect(response).to.have.schema(schemas.rootNoLogin);
  24. let regex = new RegExp('http://[^/]+/api/' + version + '/auth/', 'g')
  25. expect(response.body.login).to.match(regex);
  26. return chakram.wait();
  27. });
  28. });
  29. })
  30. });
  31. describe("API v1", function () {
  32. this.timeout(3000);
  33. let publicSuffix = 'dedyn.' + process.env.DESECSTACK_DOMAIN; // see settings.py
  34. before(function () {
  35. chakram.setRequestDefaults({
  36. headers: {
  37. 'Host': 'desec.' + process.env.DESECSTACK_DOMAIN,
  38. },
  39. followRedirect: false,
  40. baseUrl: 'https://www/api/v1',
  41. })
  42. // ensure that the public suffix domain is set up and ready to use
  43. let email = 'admin@example.com';
  44. let password = 'admin123!';
  45. return withCaptcha(function (captcha) {
  46. return chakram.post('/auth/', {
  47. "email": email,
  48. "password": password,
  49. "captcha": captcha,
  50. }).then(function (registerResponse) {
  51. return chakram.post('/auth/login/', {
  52. "email": email,
  53. "password": password,
  54. }).then(function (loginResponse) {
  55. return chakram.post('/domains/', {
  56. name: publicSuffix,
  57. }, {
  58. headers: {'Authorization': 'Token ' + loginResponse.body.token }
  59. }); // note that we ignore errors here
  60. });
  61. });
  62. });
  63. });
  64. it("provides an index page", function () {
  65. return chakram.get('/').then(function (response) {
  66. expect(response).to.have.schema(schemas.rootNoLogin);
  67. expect(response.body.login).to.match(/https:\/\/[^\/]+\/api\/v1\/auth\/login\//);
  68. return chakram.wait();
  69. });
  70. });
  71. it("has HSTS header", function () {
  72. var response = chakram.get('/');
  73. expect(response).to.have.header('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload');
  74. return chakram.wait();
  75. });
  76. it("has CORS headers", function () {
  77. return chakram.options('/', {headers: {'Origin': 'http://foo.example' }}).then(function (response) {
  78. expect(response).to.have.header('access-control-allow-origin', '*');
  79. expect(response).to.have.header('access-control-allow-headers', /.*authorization.*/);
  80. return chakram.wait();
  81. });
  82. });
  83. describe("user registration", function () {
  84. var captcha;
  85. before(function () {
  86. return withCaptcha(function (_captcha) {
  87. captcha = _captcha;
  88. });
  89. });
  90. it("returns a user object", function () {
  91. var email, password, token;
  92. email = require("uuid").v4() + '@e2etest.local';
  93. password = require("uuid").v4();
  94. var response = chakram.post('/auth/', {
  95. "email": email,
  96. "password": password,
  97. "captcha": captcha,
  98. });
  99. return expect(response).to.have.status(202);
  100. });
  101. });
  102. describe("user account", function () {
  103. var email, password;
  104. before(function () {
  105. // register a user that we can work with
  106. email = require("uuid").v4() + '@e2etest.local';
  107. password = require("uuid").v4();
  108. let response = withCaptcha(function (captcha) {
  109. return chakram.post('/auth/', {
  110. "email": email,
  111. "password": password,
  112. "captcha": captcha,
  113. });
  114. });
  115. return expect(response).to.have.status(202);
  116. });
  117. it("returns a token when logging in", function () {
  118. return chakram.post('/auth/login/', {
  119. "email": email,
  120. "password": password,
  121. }).then(function (loginResponse) {
  122. expect(loginResponse.body.token).to.match(schemas.TOKEN_REGEX);
  123. });
  124. });
  125. describe("auth/account/ endpoint", function () {
  126. var email2, password2, token2;
  127. before(function () {
  128. // register an independent user to screw around with
  129. email2 = require("uuid").v4() + '@e2etest.local';
  130. password2 = require("uuid").v4();
  131. return withCaptcha(function (captcha) {
  132. return chakram.post('/auth/', {
  133. "email": email2,
  134. "password": password2,
  135. "captcha": captcha,
  136. }).then(function () {
  137. return chakram.post('/auth/login/', {
  138. "email": email2,
  139. "password": password2,
  140. }).then(function (response) {
  141. token2 = response.body.token
  142. });
  143. });
  144. });
  145. });
  146. it("returns JSON of correct schema", function () {
  147. var response = chakram.get('/auth/account/', {
  148. headers: {'Authorization': 'Token ' + token2 }
  149. });
  150. expect(response).to.have.status(200);
  151. expect(response).to.have.schema(schemas.user);
  152. return chakram.wait();
  153. });
  154. it("allows triggering change email process", function () {
  155. return chakram.post('/auth/account/change-email/', {
  156. "email": email2,
  157. "password": password2,
  158. "new_email": require("uuid").v4() + '@e2etest.local',
  159. }).then(function (response) {
  160. expect(response).to.have.status(202);
  161. });
  162. });
  163. });
  164. });
  165. var email = require("uuid").v4() + '@e2etest.local';
  166. describe("with user account [" + email + "]", function () {
  167. var apiHomeSchema = {
  168. properties: {
  169. domains: {type: "string"},
  170. tokens: {type: "string"},
  171. account: {type: "object"},
  172. },
  173. required: ["domains", "tokens", "account"]
  174. };
  175. var password, token;
  176. before(function () {
  177. chakram.setRequestSettings({
  178. headers: {
  179. 'Host': 'desec.' + process.env.DESECSTACK_DOMAIN,
  180. },
  181. followRedirect: false,
  182. baseUrl: 'https://www/api/v1',
  183. });
  184. // register a user that we can login and work with
  185. password = require("uuid").v4();
  186. return withCaptcha(function (captcha) {
  187. return chakram.post('/auth/', {
  188. "email": email,
  189. "password": password,
  190. "captcha": captcha,
  191. }).then(function () {
  192. return chakram.post('/auth/login/', {
  193. "email": email,
  194. "password": password,
  195. }).then(function (loginResponse) {
  196. expect(loginResponse.body.token).to.match(schemas.TOKEN_REGEX);
  197. token = loginResponse.body.token;
  198. chakram.setRequestHeader('Authorization', 'Token ' + token);
  199. });
  200. });
  201. });
  202. });
  203. describe("(logged in)", function () {
  204. describe("api 'homepage'", function () {
  205. var response;
  206. before(function () {
  207. return chakram.get('/').then(function (_response) {
  208. response = _response;
  209. });
  210. });
  211. it('has status 200', function () {
  212. return expect(response).to.have.status(200);
  213. });
  214. it('looks according to the schema', function () {
  215. return expect(response).to.have.schema(apiHomeSchema);
  216. });
  217. });
  218. describe("on domains/ endpoint", function () {
  219. var domain = 'e2etest-' + require("uuid").v4() + '.' + publicSuffix;
  220. before(function () {
  221. return expect(chakram.post('/domains/', {'name': domain})).to.have.status(201);
  222. });
  223. it("can register a domain name", function () {
  224. var response = chakram.get('/domains/' + domain + '/');
  225. expect(response).to.have.status(200);
  226. expect(response).to.have.schema(schemas.domain);
  227. return chakram.wait();
  228. });
  229. itShowsUpInPdnsAs('', domain, 'NS', process.env.DESECSTACK_NS.split(/\s+/), process.env.DESECSTACK_NSLORD_DEFAULT_TTL);
  230. describe("on rrsets/ endpoint", function () {
  231. it("can retrieve RRsets", function () {
  232. var response = chakram.get('/domains/' + domain + '/rrsets/');
  233. expect(response).to.have.status(200);
  234. expect(response).to.have.schema(schemas.rrsets);
  235. response = chakram.get('/domains/' + domain + '/rrsets/.../NS/');
  236. expect(response).to.have.status(200);
  237. expect(response).to.have.schema(schemas.rrset);
  238. response = chakram.get('/domains/' + domain + '/rrsets/@/NS/');
  239. expect(response).to.have.status(200);
  240. expect(response).to.have.schema(schemas.rrset);
  241. return chakram.wait();
  242. });
  243. });
  244. });
  245. describe('POST rrsets/ with fresh domain', function () {
  246. var domain = 'e2etest-' + require("uuid").v4() + '.' + publicSuffix;
  247. before(function () {
  248. return expect(chakram.post('/domains/', {'name': domain})).to.have.status(201);
  249. });
  250. describe("can set an A RRset", function () {
  251. before(function () {
  252. var response = chakram.post(
  253. '/domains/' + domain + '/rrsets/',
  254. {'subname': '', 'type': 'A', 'records': ['127.0.0.1'], 'ttl': 60}
  255. );
  256. expect(response).to.have.status(201);
  257. expect(response).to.have.schema(schemas.rrset);
  258. expect(response).to.have.json('ttl', 60);
  259. expect(response).to.have.json('records', ['127.0.0.1']);
  260. return chakram.wait();
  261. });
  262. itPropagatesToTheApi([
  263. {subname: '', domain: domain, type: 'A', ttl: 60, records: ['127.0.0.1']},
  264. ]);
  265. itShowsUpInPdnsAs('', domain, 'A', ['127.0.0.1'], 60);
  266. });
  267. describe("cannot create RRsets of restricted or dead type", function () {
  268. var rrTypes = ['DNAME', 'ALIAS', 'SOA', 'RRSIG', 'DNSKEY', 'NSEC3PARAM', 'OPT'];
  269. for (var i = 0; i < rrTypes.length; i++) {
  270. var rrType = rrTypes[i];
  271. it(rrType, function () {
  272. return expect(chakram.post(
  273. '/domains/' + domain + '/rrsets/',
  274. {'subname': 'not-welcome', 'type': rrType, 'records': ['127.0.0.1'], 'ttl': 60}
  275. )).to.have.status(400);
  276. });
  277. }
  278. });
  279. it("cannot update RRSets for nonexistent domain name", function () {
  280. return expect(chakram.patch(
  281. '/domains/nonexistent.e2e.domain/rrsets/',
  282. {'subname': '', 'type': 'A', 'records': ['127.0.0.1'], 'ttl': 60}
  283. )).to.have.status(404);
  284. });
  285. it("cannot create RRSets for nonexistent domain name", function () {
  286. return expect(chakram.post(
  287. '/domains/nonexistent.e2e.domain/rrsets/',
  288. {'subname': '', 'type': 'A', 'records': ['127.0.0.1'], 'ttl': 60}
  289. )).to.have.status(404);
  290. });
  291. it("cannot set unicode RRsets", function () {
  292. return expect(chakram.post(
  293. '/domains/' + domain + '/rrsets/',
  294. {'subname': '想不出来', 'type': 'A', 'records': ['127.0.0.1'], 'ttl': 60}
  295. )).to.have.status(400);
  296. });
  297. describe("can set a wildcard AAAA RRset with multiple records", function () {
  298. before(function () {
  299. return chakram.post(
  300. '/domains/' + domain + '/rrsets/',
  301. {'subname': '*.foobar', 'type': 'AAAA', 'records': ['::1', 'bade::affe'], 'ttl': 60}
  302. );
  303. });
  304. itPropagatesToTheApi([
  305. {subname: '*.foobar', domain: domain, type: 'AAAA', ttl: 60, records: ['::1', 'bade::affe']},
  306. {subname: '*.foobar', domain: domain, type: 'AAAA', records: ['bade::affe', '::1']},
  307. ]);
  308. itShowsUpInPdnsAs('test.foobar', domain, 'AAAA', ['::1', 'bade::affe'], 60);
  309. });
  310. describe("cannot create RRsets with duplicate record content", function () {
  311. it("rejects exact duplicates", function () {
  312. return expect(chakram.post(
  313. '/domains/' + domain + '/rrsets/',
  314. {
  315. 'subname': 'duplicate-contents', 'type': 'AAAA',
  316. 'records': ['::1', '::1'], 'ttl': 60
  317. }
  318. )).to.have.status(400);
  319. });
  320. it("rejects semantic duplicates", function () {
  321. return expect(chakram.post(
  322. '/domains/' + domain + '/rrsets/',
  323. {
  324. 'subname': 'duplicate-contents', 'type': 'AAAA',
  325. 'records': ['::1', '::0001'], 'ttl': 60
  326. }
  327. )).to.have.status(400);
  328. });
  329. describe("even in subsequent requests", function () {
  330. before(function() {
  331. return expect(chakram.post(
  332. '/domains/' + domain + '/rrsets/',
  333. {
  334. 'subname': 'duplicate-contents', 'type': 'AAAA',
  335. 'records': ['::1'], 'ttl': 60
  336. }
  337. )).to.have.status(201);
  338. });
  339. it("still does not accept a semantic duplicate", function () {
  340. return expect(chakram.post(
  341. '/domains/' + domain + '/rrsets/',
  342. {
  343. 'subname': 'duplicate-contents', 'type': 'AAAA',
  344. 'records': ['::0001'], 'ttl': 60
  345. }
  346. )).to.have.status(400);
  347. });
  348. it("still does not accept a semantic duplicates", function () {
  349. return expect(chakram.post(
  350. '/domains/' + domain + '/rrsets/',
  351. {
  352. 'subname': 'duplicate-contents', 'type': 'AAAA',
  353. 'records': ['::1', '::0001'], 'ttl': 60
  354. }
  355. )).to.have.status(400);
  356. });
  357. })
  358. });
  359. describe("can bulk-post an AAAA and an MX record", function () {
  360. before(function () {
  361. var response = chakram.post(
  362. '/domains/' + domain + '/rrsets/',
  363. [
  364. { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
  365. { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
  366. ]
  367. );
  368. expect(response).to.have.status(201);
  369. expect(response).to.have.schema(schemas.rrsets);
  370. return chakram.wait();
  371. });
  372. itPropagatesToTheApi([
  373. {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
  374. {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
  375. ]);
  376. itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
  377. itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
  378. });
  379. describe("cannot bulk-post with missing or invalid fields", function () {
  380. before(function () {
  381. // Set an RRset that we'll try to overwrite
  382. var response = chakram.post(
  383. '/domains/' + domain + '/rrsets/',
  384. {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
  385. );
  386. expect(response).to.have.status(201);
  387. var response = chakram.post(
  388. '/domains/' + domain + '/rrsets/',
  389. [
  390. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  391. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  392. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  393. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  394. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  395. {'subname': 'd.1', 'ttl': 3650, 'type': 'SOA', 'records': ['set.an.example. get.desec.io. 2018034419 10800 3600 604800 60']},
  396. {'subname': 'd.1', 'ttl': 3650, 'type': 'OPT', 'records': ['9999']},
  397. {'subname': 'd.1', 'ttl': 3650, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
  398. ]
  399. );
  400. expect(response).to.have.status(400);
  401. expect(response).to.have.json([
  402. { type: [ 'This field is required.' ] },
  403. { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
  404. { subname: [ 'This field is required.' ] },
  405. { ttl: [ 'This field is required.' ] },
  406. { records: [ 'This field is required.' ] },
  407. { type: [ 'You cannot tinker with the SOA RR set. It is managed automatically.' ] },
  408. { type: [ 'You cannot tinker with the OPT RR set. It is managed automatically.' ] },
  409. { type: [ 'Generic type format is not supported.' ] },
  410. ]);
  411. return chakram.wait();
  412. });
  413. it("does not propagate partially to the API", function () {
  414. return chakram.waitFor([
  415. chakram
  416. .get('/domains/' + domain + '/rrsets/b.1.../AAAA/')
  417. .then(function (response) {
  418. expect(response).to.have.status(404);
  419. }),
  420. chakram
  421. .get('/domains/' + domain + '/rrsets/.../TXT/')
  422. .then(function (response) {
  423. expect(response).to.have.status(200);
  424. expect(response).to.have.json('ttl', 3650);
  425. expect(response.body.records).to.have.members(['"foo"']);
  426. }),
  427. ]);
  428. });
  429. itShowsUpInPdnsAs('b.1', domain, 'AAAA', []);
  430. });
  431. context("with a pre-existing RRset", function () {
  432. before(function () {
  433. var response = chakram.post(
  434. '/domains/' + domain + '/rrsets/',
  435. [
  436. {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  437. {'subname': 'c.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  438. {'subname': 'delete-test', 'ttl': 3650, 'type': 'A', 'records': ['127.1.2.3']},
  439. {'subname': 'replace-test-1', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
  440. {'subname': 'replace-test-2', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
  441. ]
  442. );
  443. return expect(response).to.have.status(201);
  444. });
  445. describe("can delete an RRset", function () {
  446. before(function () {
  447. var response = chakram.delete('/domains/' + domain + '/rrsets/delete-test.../A/');
  448. return expect(response).to.have.status(204);
  449. });
  450. itPropagatesToTheApi([
  451. {subname: 'delete-test', domain: domain, type: 'A', records: []},
  452. ]);
  453. itShowsUpInPdnsAs('delete-test', domain, 'A', []);
  454. });
  455. describe("can be replaced with a CNAME record", function () {
  456. before(function () {
  457. var response = chakram.put(
  458. '/domains/' + domain + '/rrsets/',
  459. [
  460. {'subname': 'replace-test-1', 'ttl': 3650, 'type': 'AAAA', 'records': []},
  461. {'subname': 'replace-test-1', 'ttl': 3601, 'type': 'CNAME', 'records': ['example.com.']},
  462. ]
  463. );
  464. return expect(response).to.have.status(200);
  465. });
  466. itPropagatesToTheApi([
  467. {subname: 'replace-test-1', domain: domain, type: 'AAAA', records: []},
  468. {subname: 'replace-test-1', domain: domain, type: 'CNAME', records: ["example.com."]},
  469. ]);
  470. itShowsUpInPdnsAs('replace-test-1', domain, 'AAAA', ["example.com"]);
  471. itShowsUpInPdnsAs('replace-test-1', domain, 'CNAME', ["example.com"]);
  472. });
  473. describe("cannot be replaced with a malformed CNAME record", function () {
  474. before(function () {
  475. var response = chakram.put(
  476. '/domains/' + domain + '/rrsets/',
  477. [
  478. {'subname': 'replace-test-2', 'ttl': 3650, 'type': 'AAAA', 'records': []},
  479. {'subname': 'replace-test-2', 'ttl': 3601, 'type': 'CNAME', 'records': ['no.trailing.dot']},
  480. ]
  481. );
  482. return expect(response).to.have.status(400);
  483. });
  484. itPropagatesToTheApi([
  485. {subname: 'replace-test-2', domain: domain, type: 'AAAA', records: ["::1", "::2"]},
  486. {subname: 'replace-test-2', domain: domain, type: 'CNAME', records: []},
  487. ]);
  488. itShowsUpInPdnsAs('replace-test-2', domain, 'AAAA', ["::1", "::2"]);
  489. itShowsUpInPdnsAs('replace-test-2', domain, 'CNAME', []);
  490. });
  491. describe("cannot bulk-post existing or duplicate RRsets", function () {
  492. var response;
  493. before(function () {
  494. response = chakram.post(
  495. '/domains/' + domain + '/rrsets/',
  496. [
  497. {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  498. {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  499. ]
  500. );
  501. expect(response).to.have.status(400);
  502. return chakram.wait();
  503. });
  504. it("gives the right response", function () {
  505. expect(response).to.have.json([
  506. {"non_field_errors": ["Same subname and type as in position(s) 1, but must be unique."]},
  507. {"non_field_errors": ["Same subname and type as in position(s) 0, but must be unique."]}
  508. ]);
  509. return chakram.wait();
  510. });
  511. it("does not touch records in the API", function () {
  512. return chakram
  513. .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
  514. .then(function (response) {
  515. expect(response).to.have.status(200);
  516. expect(response).to.have.json('ttl', 3650);
  517. expect(response.body.records).to.have.members(['"foo"']);
  518. });
  519. });
  520. itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"foo"'], 3650);
  521. });
  522. describe("cannot delete RRsets via bulk-post", function () {
  523. var response;
  524. before(function () {
  525. response = chakram.post(
  526. '/domains/' + domain + '/rrsets/',
  527. [
  528. {'subname': 'c.2', 'ttl': 3640, 'type': 'TXT', 'records': []},
  529. ]
  530. );
  531. return expect(response).to.have.status(400);
  532. });
  533. it("gives the right response", function () {
  534. return expect(response).to.have.json([
  535. {'records': ['This field must not be empty when using POST.']},
  536. ]);
  537. });
  538. });
  539. });
  540. describe("cannot bulk-post with invalid input", function () {
  541. it("gives the right response for invalid type", function () {
  542. var response = chakram.post(
  543. '/domains/' + domain + '/rrsets/',
  544. [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
  545. );
  546. return expect(response).to.have.status(400);
  547. });
  548. it("gives the right response for invalid records", function () {
  549. var response = chakram.post(
  550. '/domains/' + domain + '/rrsets/',
  551. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
  552. );
  553. return expect(response).to.have.status(400);
  554. });
  555. it("gives the right response for records contents being null", function () {
  556. var response = chakram.post(
  557. '/domains/' + domain + '/rrsets/',
  558. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
  559. );
  560. return expect(response).to.have.status(400);
  561. });
  562. });
  563. });
  564. describe('PUT rrsets/ with fresh domain', function () {
  565. var domain = 'e2etest-' + require("uuid").v4() + '.' + publicSuffix;
  566. before(function () {
  567. return expect(chakram.post('/domains/', {'name': domain})).to.have.status(201);
  568. });
  569. describe("can overwrite a single existing RRset using PUT", function () {
  570. before(function () {
  571. var response = chakram.post(
  572. '/domains/' + domain + '/rrsets/',
  573. { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 3662 }
  574. ).then(function () {
  575. return chakram.put(
  576. '/domains/' + domain + '/rrsets/single.../AAAA/',
  577. { 'subname': 'single', 'type': 'AAAA', 'records': ['fefe::bade'], 'ttl': 3631 }
  578. );
  579. });
  580. expect(response).to.have.status(200);
  581. expect(response).to.have.schema(schemas.rrset);
  582. return chakram.wait();
  583. });
  584. itPropagatesToTheApi([
  585. {subname: 'single', domain: domain, type: 'AAAA', ttl: 3631, records: ['fefe::bade']},
  586. ]);
  587. itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 3631);
  588. });
  589. describe("can bulk-put an AAAA and an MX record", function () {
  590. before(function () {
  591. var response = chakram.put(
  592. '/domains/' + domain + '/rrsets/',
  593. [
  594. { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
  595. { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
  596. ]
  597. );
  598. expect(response).to.have.status(200);
  599. expect(response).to.have.schema(schemas.rrsets);
  600. return chakram.wait();
  601. });
  602. itPropagatesToTheApi([
  603. {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
  604. {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
  605. ]);
  606. itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
  607. itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
  608. });
  609. describe("cannot bulk-put with missing or invalid fields", function () {
  610. before(function () {
  611. // Set an RRset that we'll try to overwrite
  612. var response = chakram.post(
  613. '/domains/' + domain + '/rrsets/',
  614. {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
  615. );
  616. expect(response).to.have.status(201);
  617. var response = chakram.put(
  618. '/domains/' + domain + '/rrsets/',
  619. [
  620. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  621. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  622. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  623. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  624. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  625. ]
  626. );
  627. expect(response).to.have.status(400);
  628. expect(response).to.have.json([
  629. { type: [ 'This field is required.' ] },
  630. { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
  631. { subname: [ 'This field is required.' ] },
  632. { ttl: [ 'This field is required.' ] },
  633. { records: [ 'This field is required.' ] },
  634. ]);
  635. return chakram.wait();
  636. });
  637. it("does not propagate partially to the API", function () {
  638. return chakram.waitFor([
  639. chakram
  640. .get('/domains/' + domain + '/rrsets/b.1.../AAAA/')
  641. .then(function (response) {
  642. expect(response).to.have.status(404);
  643. }),
  644. chakram
  645. .get('/domains/' + domain + '/rrsets/.../TXT/')
  646. .then(function (response) {
  647. expect(response).to.have.status(200);
  648. expect(response).to.have.json('ttl', 3650);
  649. expect(response.body.records).to.have.members(['"foo"']);
  650. }),
  651. ]);
  652. });
  653. itShowsUpInPdnsAs('b.1', domain, 'AAAA', []);
  654. });
  655. context("with a pre-existing RRset", function () {
  656. before(function () {
  657. var response = chakram.post(
  658. '/domains/' + domain + '/rrsets/',
  659. [
  660. {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  661. {'subname': 'b.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  662. {'subname': 'c.2', 'ttl': 3650, 'type': 'A', 'records': ['1.2.3.4']},
  663. ]
  664. );
  665. expect(response).to.have.status(201);
  666. return chakram.wait();
  667. });
  668. describe("can bulk-put existing RRsets", function () {
  669. var response;
  670. before(function () {
  671. response = chakram.put(
  672. '/domains/' + domain + '/rrsets/',
  673. [
  674. {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  675. ]
  676. );
  677. expect(response).to.have.status(200);
  678. expect(response).to.have.schema(schemas.rrsets);
  679. return chakram.wait();
  680. });
  681. it("does modify records in the API", function () {
  682. return chakram
  683. .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
  684. .then(function (response) {
  685. expect(response).to.have.status(200);
  686. expect(response).to.have.json('ttl', 3640);
  687. expect(response.body.records).to.have.members(['"bar"']);
  688. });
  689. });
  690. itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 3640);
  691. });
  692. describe("cannot bulk-put duplicate RRsets", function () {
  693. var response;
  694. before(function () {
  695. response = chakram.put(
  696. '/domains/' + domain + '/rrsets/',
  697. [
  698. {'subname': 'b.2', 'ttl': 3660, 'type': 'TXT', 'records': ['"bar"']},
  699. {'subname': 'b.2', 'ttl': 3660, 'type': 'TXT', 'records': ['"bar"']},
  700. ]
  701. );
  702. return expect(response).to.have.status(400);
  703. });
  704. it("gives the right response", function () {
  705. return expect(response).to.have.json([
  706. { 'non_field_errors': [ 'Same subname and type as in position(s) 1, but must be unique.' ] },
  707. { 'non_field_errors': [ 'Same subname and type as in position(s) 0, but must be unique.' ] },
  708. ]);
  709. });
  710. it("does not touch records in the API", function () {
  711. return chakram
  712. .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
  713. .then(function (response) {
  714. expect(response).to.have.status(200);
  715. expect(response).to.have.json('ttl', 3650);
  716. expect(response.body.records).to.have.members(['"foo"']);
  717. });
  718. });
  719. itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 3650);
  720. });
  721. describe("can delete RRsets via bulk-put", function () {
  722. var response;
  723. before(function () {
  724. response = chakram.put(
  725. '/domains/' + domain + '/rrsets/',
  726. [
  727. {'subname': 'c.2', 'ttl': 3640, 'type': 'A', 'records': []},
  728. ]
  729. );
  730. return expect(response).to.have.status(200);
  731. });
  732. it("gives the right response", function () {
  733. var response = chakram.get('/domains/' + domain + '/rrsets/c.2.../A/');
  734. return expect(response).to.have.status(404);
  735. });
  736. });
  737. });
  738. describe("cannot bulk-put with invalid input", function () {
  739. it("gives the right response for invalid type", function () {
  740. var response = chakram.put(
  741. '/domains/' + domain + '/rrsets/',
  742. [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
  743. );
  744. return expect(response).to.have.status(400);
  745. });
  746. it("gives the right response for invalid records", function () {
  747. var response = chakram.put(
  748. '/domains/' + domain + '/rrsets/',
  749. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
  750. );
  751. return expect(response).to.have.status(400);
  752. });
  753. it("gives the right response for records contents being null", function () {
  754. var response = chakram.put(
  755. '/domains/' + domain + '/rrsets/',
  756. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
  757. );
  758. return expect(response).to.have.status(400);
  759. });
  760. });
  761. });
  762. describe('PATCH rrsets/ with fresh domain', function () {
  763. var domain = 'e2etest-' + require("uuid").v4() + '.' + publicSuffix;
  764. before(function () {
  765. return expect(chakram.post('/domains/', {'name': domain})).to.have.status(201);
  766. });
  767. describe("can modify a single existing RRset using PATCH", function () {
  768. before(function () {
  769. var response = chakram.post(
  770. '/domains/' + domain + '/rrsets/',
  771. { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 3662 }
  772. ).then(function () {
  773. return chakram.patch(
  774. '/domains/' + domain + '/rrsets/single.../AAAA/',
  775. { 'records': ['fefe::bade'], 'ttl': 3631 }
  776. );
  777. });
  778. expect(response).to.have.status(200);
  779. expect(response).to.have.schema(schemas.rrset);
  780. return chakram.wait();
  781. });
  782. itPropagatesToTheApi([
  783. {subname: 'single', domain: domain, type: 'AAAA', ttl: 3631, records: ['fefe::bade']},
  784. ]);
  785. itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 3631);
  786. });
  787. describe("can bulk-patch an AAAA and an MX record", function () {
  788. before(function () {
  789. var response = chakram.patch(
  790. '/domains/' + domain + '/rrsets/',
  791. [
  792. { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
  793. { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
  794. ]
  795. );
  796. expect(response).to.have.status(200);
  797. expect(response).to.have.schema(schemas.rrsets);
  798. return chakram.wait();
  799. });
  800. itPropagatesToTheApi([
  801. {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
  802. {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
  803. ]);
  804. itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
  805. itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
  806. });
  807. describe("cannot bulk-patch with missing or invalid fields", function () {
  808. before(function () {
  809. // Set an RRset that we'll try to overwrite
  810. var response = chakram.post(
  811. '/domains/' + domain + '/rrsets/',
  812. {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
  813. );
  814. expect(response).to.have.status(201);
  815. var response = chakram.patch(
  816. '/domains/' + domain + '/rrsets/',
  817. [
  818. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  819. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  820. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  821. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  822. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  823. ]
  824. );
  825. expect(response).to.have.status(400);
  826. expect(response).to.have.json([
  827. { type: [ 'This field is required.' ] },
  828. { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
  829. { subname: [ 'This field is required.' ] },
  830. { ttl: ['This field is required.']} ,
  831. { records: ['This field is required.']} ,
  832. ]);
  833. return chakram.wait();
  834. });
  835. it("does not propagate partially to the API", function () {
  836. return chakram.waitFor([
  837. chakram
  838. .get('/domains/' + domain + '/rrsets/b.1.../AAAA/')
  839. .then(function (response) {
  840. expect(response).to.have.status(404);
  841. }),
  842. chakram
  843. .get('/domains/' + domain + '/rrsets/.../TXT/')
  844. .then(function (response) {
  845. expect(response).to.have.status(200);
  846. expect(response).to.have.json('ttl', 3650);
  847. expect(response.body.records).to.have.members(['"foo"']);
  848. }),
  849. ]);
  850. });
  851. itShowsUpInPdnsAs('b.1', domain, 'AAAA', []);
  852. });
  853. context("with a pre-existing RRset", function () {
  854. before(function () {
  855. var response = chakram.post(
  856. '/domains/' + domain + '/rrsets/',
  857. [
  858. {'subname': 'a.1', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  859. {'subname': 'a.2', 'ttl': 3650, 'type': 'A', 'records': ['4.3.2.1']},
  860. {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  861. {'subname': 'b.2', 'ttl': 3650, 'type': 'A', 'records': ['5.4.3.2']},
  862. {'subname': 'b.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  863. {'subname': 'c.2', 'ttl': 3650, 'type': 'A', 'records': ['1.2.3.4']},
  864. ]
  865. );
  866. return expect(response).to.have.status(201);
  867. });
  868. describe("can bulk-patch existing RRsets", function () {
  869. var response;
  870. before(function () {
  871. response = chakram.patch(
  872. '/domains/' + domain + '/rrsets/',
  873. [
  874. {'subname': 'a.1', 'type': 'TXT', 'records': ['"bar"']},
  875. {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  876. ]
  877. );
  878. expect(response).to.have.status(200);
  879. expect(response).to.have.schema(schemas.rrsets);
  880. return chakram.wait();
  881. });
  882. it("does modify records in the API", function () {
  883. return chakram.waitFor([
  884. chakram
  885. .get('/domains/' + domain + '/rrsets/a.1.../TXT/')
  886. .then(function (response) {
  887. expect(response).to.have.status(200);
  888. expect(response).to.have.json('ttl', 3650);
  889. expect(response.body.records).to.have.members(['"bar"']);
  890. }),
  891. chakram
  892. .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
  893. .then(function (response) {
  894. expect(response).to.have.status(200);
  895. expect(response).to.have.json('ttl', 3640);
  896. expect(response.body.records).to.have.members(['"bar"']);
  897. }),
  898. ]);
  899. });
  900. itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 3640);
  901. });
  902. describe("cannot bulk-patch duplicate RRsets", function () {
  903. var response;
  904. before(function () {
  905. response = chakram.patch(
  906. '/domains/' + domain + '/rrsets/',
  907. [
  908. {'subname': 'b.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  909. {'subname': 'b.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  910. ]
  911. );
  912. return expect(response).to.have.status(400);
  913. });
  914. it("gives the right response", function () {
  915. return expect(response).to.have.json([
  916. { 'non_field_errors': [ 'Same subname and type as in position(s) 1, but must be unique.' ] },
  917. { 'non_field_errors': [ 'Same subname and type as in position(s) 0, but must be unique.' ] },
  918. ]);
  919. });
  920. it("does not touch records in the API", function () {
  921. return chakram
  922. .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
  923. .then(function (response) {
  924. expect(response).to.have.status(200);
  925. expect(response).to.have.json('ttl', 3650);
  926. expect(response.body.records).to.have.members(['"foo"']);
  927. });
  928. });
  929. itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 3650);
  930. });
  931. describe("can delete RRsets via bulk-patch", function () {
  932. var response;
  933. before(function () {
  934. response = chakram.patch(
  935. '/domains/' + domain + '/rrsets/',
  936. [
  937. {'subname': 'c.2', 'type': 'A', 'records': []},
  938. ]
  939. );
  940. return expect(response).to.have.status(200);
  941. });
  942. it("gives the right response", function () {
  943. var response = chakram.get('/domains/' + domain + '/rrsets/c.2.../A/');
  944. return expect(response).to.have.status(404);
  945. });
  946. });
  947. });
  948. describe("cannot bulk-patch with invalid input", function () {
  949. it("gives the right response for invalid type", function () {
  950. var response = chakram.patch(
  951. '/domains/' + domain + '/rrsets/',
  952. [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
  953. );
  954. return expect(response).to.have.status(400);
  955. });
  956. it("gives the right response for invalid records", function () {
  957. var response = chakram.patch(
  958. '/domains/' + domain + '/rrsets/',
  959. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
  960. );
  961. return expect(response).to.have.status(400);
  962. });
  963. it("gives the right response for records contents being null", function () {
  964. var response = chakram.patch(
  965. '/domains/' + domain + '/rrsets/',
  966. [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
  967. );
  968. return expect(response).to.have.status(400);
  969. });
  970. });
  971. });
  972. describe("auth/tokens/ endpoint", function () {
  973. var tokenId;
  974. var tokenValue;
  975. function createTokenWithName () {
  976. var tokenname = "e2e-token-" + require("uuid").v4();
  977. return chakram.post('/auth/tokens/', { name: tokenname }).then(function (response) {
  978. expect(response).to.have.status(201);
  979. expect(response).to.have.json('name', tokenname);
  980. tokenId = response.body['id'];
  981. });
  982. }
  983. function createToken () {
  984. return chakram.post('/auth/tokens/').then(function (response) {
  985. expect(response).to.have.status(201);
  986. tokenId = response.body['id'];
  987. tokenValue = response.body['value'];
  988. });
  989. }
  990. it("can create tokens", createToken);
  991. it("can create tokens with name", createTokenWithName)
  992. describe("with tokens", function () {
  993. before(createToken)
  994. it("a list of tokens can be retrieved", function () {
  995. var response = chakram.get('/auth/tokens/');
  996. return expect(response).to.have.schema(schemas.tokens);
  997. });
  998. describe("can delete token", function () {
  999. before( function () {
  1000. var response = chakram.delete('/auth/tokens/' + tokenId + '/');
  1001. return expect(response).to.have.status(204);
  1002. });
  1003. it("deactivates the token", function () {
  1004. return expect(chakram.get('/auth/tokens/', {
  1005. headers: {'Authorization': 'Token ' + tokenValue }
  1006. })).to.have.status(401);
  1007. });
  1008. });
  1009. it("deleting nonexistent tokens yields 204", function () {
  1010. var response = chakram.delete('/auth/tokens/wedonthavethisid/');
  1011. return expect(response).to.have.status(204);
  1012. });
  1013. });
  1014. })
  1015. });
  1016. });
  1017. });