api_spec.js 52 KB

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