async_validate.spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. 'use strict';
  2. var Ajv = require('./ajv')
  3. , Promise = require('./promise')
  4. , getAjvInstances = require('./ajv_async_instances')
  5. , should = require('./chai').should();
  6. describe('async schemas, formats and keywords', function() {
  7. this.timeout(30000);
  8. var ajv, instances;
  9. beforeEach(function () {
  10. instances = getAjvInstances();
  11. ajv = instances[0];
  12. });
  13. describe('async schemas without async elements', function() {
  14. it('should return result as promise', function() {
  15. var schema = {
  16. $async: true,
  17. type: 'string',
  18. maxLength: 3
  19. };
  20. return repeat(function() { return Promise.all(instances.map(test)); });
  21. function test(_ajv) {
  22. var validate = _ajv.compile(schema);
  23. return Promise.all([
  24. shouldBeValid( validate('abc'), 'abc' ),
  25. shouldBeInvalid( validate('abcd') ),
  26. shouldBeInvalid( validate(1) ),
  27. ]);
  28. }
  29. });
  30. it('should fail compilation if async schema is inside sync schema', function() {
  31. var schema = {
  32. properties: {
  33. foo: {
  34. $async: true,
  35. type: 'string',
  36. maxLength: 3
  37. }
  38. }
  39. };
  40. shouldThrowFunc('async schema in sync schema', function() {
  41. ajv.compile(schema);
  42. });
  43. schema.$async = true;
  44. ajv.compile(schema);
  45. });
  46. });
  47. describe('async formats', function() {
  48. beforeEach(addFormatEnglishWord);
  49. it('should fail compilation if async format is inside sync schema', function() {
  50. instances.forEach(function (_ajv) {
  51. var schema = {
  52. type: 'string',
  53. format: 'english_word'
  54. };
  55. shouldThrowFunc('async format in sync schema', function() {
  56. _ajv.compile(schema);
  57. });
  58. schema.$async = true;
  59. _ajv.compile(schema);
  60. });
  61. });
  62. });
  63. describe('async custom keywords', function() {
  64. beforeEach(function() {
  65. instances.forEach(function (_ajv) {
  66. _ajv.addKeyword('idExists', {
  67. async: true,
  68. type: 'number',
  69. validate: checkIdExists,
  70. errors: false
  71. });
  72. _ajv.addKeyword('idExistsWithError', {
  73. async: true,
  74. type: 'number',
  75. validate: checkIdExistsWithError,
  76. errors: true
  77. });
  78. });
  79. });
  80. it('should fail compilation if async keyword is inside sync schema', function() {
  81. instances.forEach(function (_ajv) {
  82. var schema = {
  83. type: 'object',
  84. properties: {
  85. userId: {
  86. type: 'integer',
  87. idExists: { table: 'users' }
  88. }
  89. }
  90. };
  91. shouldThrowFunc('async keyword in sync schema', function() {
  92. _ajv.compile(schema);
  93. });
  94. schema.$async = true;
  95. _ajv.compile(schema);
  96. });
  97. });
  98. it('should return custom error', function() {
  99. return Promise.all(instances.map(function (_ajv) {
  100. var schema = {
  101. $async: true,
  102. type: 'object',
  103. properties: {
  104. userId: {
  105. type: 'integer',
  106. idExistsWithError: { table: 'users' }
  107. },
  108. postId: {
  109. type: 'integer',
  110. idExistsWithError: { table: 'posts' }
  111. }
  112. }
  113. };
  114. var validate = _ajv.compile(schema);
  115. return Promise.all([
  116. shouldBeInvalid( validate({ userId: 5, postId: 10 }), [ 'id not found in table posts' ] ),
  117. shouldBeInvalid( validate({ userId: 9, postId: 25 }), [ 'id not found in table users' ] )
  118. ]);
  119. }));
  120. });
  121. function checkIdExists(schema, data) {
  122. switch (schema.table) {
  123. case 'users': return check([1, 5, 8]);
  124. case 'posts': return check([21, 25, 28]);
  125. default: throw new Error('no such table');
  126. }
  127. function check(IDs) {
  128. return Promise.resolve(IDs.indexOf(data) >= 0);
  129. }
  130. }
  131. function checkIdExistsWithError(schema, data) {
  132. var table = schema.table;
  133. switch (table) {
  134. case 'users': return check(table, [1, 5, 8]);
  135. case 'posts': return check(table, [21, 25, 28]);
  136. default: throw new Error('no such table');
  137. }
  138. function check(_table, IDs) {
  139. if (IDs.indexOf(data) >= 0) return Promise.resolve(true);
  140. var error = {
  141. keyword: 'idExistsWithError',
  142. message: 'id not found in table ' + _table
  143. };
  144. return Promise.reject(new Ajv.ValidationError([error]));
  145. }
  146. }
  147. });
  148. describe('async referenced schemas', function() {
  149. beforeEach(function() {
  150. instances = getAjvInstances({ inlineRefs: false, extendRefs: 'ignore' });
  151. addFormatEnglishWord();
  152. });
  153. it('should validate referenced async schema', function() {
  154. var schema = {
  155. $async: true,
  156. definitions: {
  157. english_word: {
  158. $async: true,
  159. type: 'string',
  160. format: 'english_word'
  161. }
  162. },
  163. properties: {
  164. word: { $ref: '#/definitions/english_word' }
  165. }
  166. };
  167. return repeat(function() { return Promise.all(instances.map(function (_ajv) {
  168. var validate = _ajv.compile(schema);
  169. var validData = { word: 'tomorrow' };
  170. return Promise.all([
  171. shouldBeValid( validate(validData), validData ),
  172. shouldBeInvalid( validate({ word: 'manana' }) ),
  173. shouldBeInvalid( validate({ word: 1 }) ),
  174. shouldThrow( validate({ word: 'today' }), 'unknown word' )
  175. ]);
  176. })); });
  177. });
  178. it('should validate recursive async schema', function() {
  179. var schema = {
  180. $async: true,
  181. definitions: {
  182. english_word: {
  183. $async: true,
  184. type: 'string',
  185. format: 'english_word'
  186. }
  187. },
  188. type: 'object',
  189. properties: {
  190. foo: {
  191. anyOf: [
  192. { $ref: '#/definitions/english_word' },
  193. { $ref: '#' }
  194. ]
  195. }
  196. }
  197. };
  198. return recursiveTest(schema);
  199. });
  200. it('should validate recursive ref to async sub-schema, issue #612', function() {
  201. var schema = {
  202. $async: true,
  203. type: 'object',
  204. properties: {
  205. foo: {
  206. $async: true,
  207. anyOf: [
  208. {
  209. type: 'string',
  210. format: 'english_word'
  211. },
  212. {
  213. type: 'object',
  214. properties: {
  215. foo: { $ref: '#/properties/foo' }
  216. }
  217. }
  218. ]
  219. }
  220. }
  221. };
  222. return recursiveTest(schema);
  223. });
  224. it('should validate ref from referenced async schema to root schema', function() {
  225. var schema = {
  226. $async: true,
  227. definitions: {
  228. wordOrRoot: {
  229. $async: true,
  230. anyOf: [
  231. {
  232. type: 'string',
  233. format: 'english_word'
  234. },
  235. { $ref: '#' }
  236. ]
  237. }
  238. },
  239. type: 'object',
  240. properties: {
  241. foo: { $ref: '#/definitions/wordOrRoot' }
  242. }
  243. };
  244. return recursiveTest(schema);
  245. });
  246. it('should validate refs between two async schemas', function() {
  247. var schemaObj = {
  248. $id: 'http://e.com/obj.json#',
  249. $async: true,
  250. type: 'object',
  251. properties: {
  252. foo: { $ref: 'http://e.com/word.json#' }
  253. }
  254. };
  255. var schemaWord = {
  256. $id: 'http://e.com/word.json#',
  257. $async: true,
  258. anyOf: [
  259. {
  260. type: 'string',
  261. format: 'english_word'
  262. },
  263. { $ref: 'http://e.com/obj.json#' }
  264. ]
  265. };
  266. return recursiveTest(schemaObj, schemaWord);
  267. });
  268. it('should fail compilation if sync schema references async schema', function() {
  269. var schema = {
  270. $id: 'http://e.com/obj.json#',
  271. type: 'object',
  272. properties: {
  273. foo: { $ref: 'http://e.com/word.json#' }
  274. }
  275. };
  276. var schemaWord = {
  277. $id: 'http://e.com/word.json#',
  278. $async: true,
  279. anyOf: [
  280. {
  281. type: 'string',
  282. format: 'english_word'
  283. },
  284. { $ref: 'http://e.com/obj.json#' }
  285. ]
  286. };
  287. ajv.addSchema(schemaWord);
  288. ajv.addFormat('english_word', {
  289. async: true,
  290. validate: checkWordOnServer
  291. });
  292. shouldThrowFunc('async schema referenced by sync schema', function() {
  293. ajv.compile(schema);
  294. });
  295. schema.$id = 'http://e.com/obj2.json#';
  296. schema.$async = true;
  297. ajv.compile(schema);
  298. });
  299. function recursiveTest(schema, refSchema) {
  300. return repeat(function() { return Promise.all(instances.map(function (_ajv) {
  301. if (refSchema) try { _ajv.addSchema(refSchema); } catch(e) {}
  302. var validate = _ajv.compile(schema);
  303. var data;
  304. return Promise.all([
  305. shouldBeValid( validate(data = { foo: 'tomorrow' }), data ),
  306. shouldBeInvalid( validate({ foo: 'manana' }) ),
  307. shouldBeInvalid( validate({ foo: 1 }) ),
  308. shouldThrow( validate({ foo: 'today' }), 'unknown word' ),
  309. shouldBeValid( validate(data = { foo: { foo: 'tomorrow' }}), data ),
  310. shouldBeInvalid( validate({ foo: { foo: 'manana' }}) ),
  311. shouldBeInvalid( validate({ foo: { foo: 1 }}) ),
  312. shouldThrow( validate({ foo: { foo: 'today' }}), 'unknown word' ),
  313. shouldBeValid( validate(data = { foo: { foo: { foo: 'tomorrow' }}}), data ),
  314. shouldBeInvalid( validate({ foo: { foo: { foo: 'manana' }}}) ),
  315. shouldBeInvalid( validate({ foo: { foo: { foo: 1 }}}) ),
  316. shouldThrow( validate({ foo: { foo: { foo: 'today' }}}), 'unknown word' )
  317. ]);
  318. })); });
  319. }
  320. });
  321. function addFormatEnglishWord() {
  322. instances.forEach(function (_ajv) {
  323. _ajv.addFormat('english_word', {
  324. async: true,
  325. validate: checkWordOnServer
  326. });
  327. });
  328. }
  329. });
  330. function checkWordOnServer(str) {
  331. return str == 'tomorrow' ? Promise.resolve(true)
  332. : str == 'manana' ? Promise.resolve(false)
  333. : Promise.reject(new Error('unknown word'));
  334. }
  335. function shouldThrowFunc(message, func) {
  336. var err;
  337. should.throw(function() {
  338. try { func(); }
  339. catch(e) { err = e; throw e; }
  340. });
  341. err.message .should.equal(message);
  342. }
  343. function shouldBeValid(p, data) {
  344. return p.then(function (valid) {
  345. valid .should.equal(data);
  346. });
  347. }
  348. var SHOULD_BE_INVALID = 'test: should be invalid';
  349. function shouldBeInvalid(p, expectedMessages) {
  350. return checkNotValid(p)
  351. .then(function (err) {
  352. err .should.be.instanceof(Ajv.ValidationError);
  353. err.errors .should.be.an('array');
  354. err.validation .should.equal(true);
  355. if (expectedMessages) {
  356. var messages = err.errors.map(function (e) {
  357. return e.message;
  358. });
  359. messages .should.eql(expectedMessages);
  360. }
  361. });
  362. }
  363. function shouldThrow(p, exception) {
  364. return checkNotValid(p)
  365. .then(function (err) {
  366. err.message .should.equal(exception);
  367. });
  368. }
  369. function checkNotValid(p) {
  370. return p.then(function (/* valid */) {
  371. throw new Error(SHOULD_BE_INVALID);
  372. })
  373. .catch(function (err) {
  374. err. should.be.instanceof(Error);
  375. if (err.message == SHOULD_BE_INVALID) throw err;
  376. return err;
  377. });
  378. }
  379. function repeat(func) {
  380. return func();
  381. // var promises = [];
  382. // for (var i=0; i<1000; i++) promises.push(func());
  383. // return Promise.all(promises);
  384. }