/test/index.js

https://github.com/25th-floor/spected · JavaScript · 624 lines · 541 code · 80 blank · 3 comment · 27 complexity · 1da4f709f534cb2f34edd3fc9c0bf2e2 MD5 · raw file

  1. import { equal, deepEqual } from 'assert'
  2. import {
  3. all,
  4. always,
  5. compose,
  6. curry,
  7. filter,
  8. head,
  9. indexOf,
  10. isEmpty,
  11. length,
  12. partial,
  13. map,
  14. not,
  15. path,
  16. prop,
  17. flip,
  18. uncurryN
  19. } from 'ramda'
  20. import spected, {validate} from '../src/'
  21. const verify = validate(() => true, head)
  22. // Predicates
  23. const colors = ['green', 'blue', 'red']
  24. const notEmpty = compose(not, isEmpty)
  25. const minLength = a => b => length(b) > a
  26. const hasPresetColors = x => indexOf(x, colors) !== -1
  27. const hasCapitalLetter = a => /[A-Z]/.test(a)
  28. const isGreaterThan = curry((len, a) => (a > len))
  29. const isLengthGreaterThan = len => compose(isGreaterThan(len), prop('length'))
  30. const isEqual = compareKey => (a, all) => a === all[compareKey]
  31. // Messages
  32. const notEmptyMsg = field => `${field} should not be empty.`
  33. const minimumMsg = (field, len) => `Minimum ${field} length of ${len} is required.`
  34. const capitalLetterMag = field => `${field} should contain at least one uppercase letter.`
  35. const capitalLetterMsgWithValue = (field) => (value) => `${field} should contain at least one uppercase letter. ${value} is missing an uppercase letter.`
  36. const equalMsg = (field1, field2) => `${field2} should be equal with ${field1}`
  37. // Rules
  38. const nameValidationRule = [[notEmpty, notEmptyMsg('Name')]]
  39. const randomValidationRule = [
  40. [isLengthGreaterThan(2), minimumMsg('Random', 3)],
  41. [hasCapitalLetter, capitalLetterMag('Random')],
  42. ]
  43. const passwordValidationRule = [
  44. [isLengthGreaterThan(5), minimumMsg('Password', 6)],
  45. [hasCapitalLetter, capitalLetterMag('Password')],
  46. ]
  47. const repeatPasswordValidationRule = [
  48. [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)],
  49. [hasCapitalLetter, capitalLetterMag('RepeatedPassword')],
  50. [isEqual('password'), equalMsg('Password', 'RepeatPassword')],
  51. ]
  52. const spec = {
  53. id: [[notEmpty, notEmptyMsg('id')]],
  54. userName: [[notEmpty, notEmptyMsg('userName')], [minLength(5), minimumMsg('UserName', 6)]],
  55. address: {
  56. street: [[notEmpty, notEmptyMsg('street')]],
  57. },
  58. settings: {
  59. profile: {
  60. design: {
  61. color: [[notEmpty, notEmptyMsg('color')], [hasPresetColors, 'Use defined colors']],
  62. background: [[notEmpty, notEmptyMsg('background')], [hasPresetColors, 'Use defined colors']],
  63. },
  64. },
  65. },
  66. }
  67. describe('spected', () => {
  68. it('should return an error when invalid', () => {
  69. const validationRules = {
  70. name: nameValidationRule,
  71. }
  72. const result = spected(validationRules, {name: ''})
  73. deepEqual({name: [notEmptyMsg('Name')]}, result)
  74. })
  75. it('should return true for field when valid', () => {
  76. const validationRules = {
  77. name: nameValidationRule,
  78. }
  79. const result = spected(validationRules, {name: 'foo'})
  80. deepEqual({name: true}, result)
  81. })
  82. it('should handle multiple validations and return the correct errors', () => {
  83. const validationRules = {
  84. name: nameValidationRule,
  85. random: randomValidationRule,
  86. }
  87. const result = spected(validationRules, {name: 'foo', random: 'A'})
  88. deepEqual({name: true, random: [minimumMsg('Random', 3)]}, result)
  89. })
  90. it('should handle multiple validations and return true for all fields when valid', () => {
  91. const validationRules = {
  92. name: nameValidationRule,
  93. random: randomValidationRule,
  94. }
  95. const result = spected(validationRules, {name: 'foo', random: 'Abcd'})
  96. deepEqual({name: true, random: true}, result)
  97. })
  98. it('should enable to spected to true if two form field values are equal', () => {
  99. const validationRules = {
  100. password: passwordValidationRule,
  101. repeatPassword: repeatPasswordValidationRule,
  102. }
  103. const result = spected(validationRules, {password: 'fooBar', repeatPassword: 'fooBar'})
  104. deepEqual({password: true, repeatPassword: true}, result)
  105. })
  106. it('should enable to spected to falsy if two form field values are not equal', () => {
  107. const validationRules = {
  108. password: passwordValidationRule,
  109. repeatPassword: repeatPasswordValidationRule,
  110. }
  111. const result = spected(validationRules, {password: 'fooBar', repeatPassword: 'fooBarBaz'})
  112. deepEqual({password: true, repeatPassword: [equalMsg('Password', 'RepeatPassword')]}, result)
  113. })
  114. it('should skip validation if no predicate function is provided.', () => {
  115. const validationRules = {
  116. password: [],
  117. }
  118. const result = spected(validationRules, {password: 'fooBar'})
  119. deepEqual({password: true}, result)
  120. })
  121. it('should skip validation if no predicate function is provided and other fields have rules', () => {
  122. const validationRules = {
  123. password: [],
  124. repeatPassword: repeatPasswordValidationRule,
  125. }
  126. const result = spected(validationRules, {password: 'fooBar', repeatPassword: 'fooBarBaz'})
  127. deepEqual({password: true, repeatPassword: [equalMsg('Password', 'RepeatPassword')]}, result)
  128. })
  129. it('should return true when no predicates are defined for an input', () => {
  130. const validationRules = {
  131. password: [],
  132. repeatPassword: [],
  133. }
  134. const result = spected(validationRules, {password: '', repeatPassword: ''})
  135. deepEqual({password: true, repeatPassword: true}, result)
  136. })
  137. it('should neglect key ordering', () => {
  138. const validationRules = {
  139. repeatPassword: repeatPasswordValidationRule,
  140. password: passwordValidationRule,
  141. }
  142. const result = spected(validationRules, {password: 'fooBar', repeatPassword: 'foobarbaZ'})
  143. deepEqual({password: true, repeatPassword: [equalMsg('Password', 'RepeatPassword')]}, result)
  144. })
  145. it('should return true when missing validations', () => {
  146. const validationRules = {
  147. password: passwordValidationRule,
  148. }
  149. const result = spected(validationRules, {password: 'fooBar', repeatPassword: 'foobarbaZ'})
  150. deepEqual({password: true, repeatPassword: true}, result)
  151. })
  152. it('should skip missing inputs', () => {
  153. const validationRules = {
  154. password: passwordValidationRule,
  155. repeatPassword: repeatPasswordValidationRule,
  156. }
  157. const result = spected(validationRules, {password: 'fooBar'})
  158. deepEqual({password: true}, result)
  159. })
  160. it('should handle deeply nested inputs', () => {
  161. const input = {
  162. id: 1,
  163. userName: 'Random',
  164. address: {
  165. street: 'Foobar',
  166. },
  167. settings: {
  168. profile: {
  169. design: {
  170. color: 'green',
  171. background: 'blue',
  172. },
  173. },
  174. },
  175. }
  176. const result = spected(spec, input)
  177. deepEqual({
  178. id: true,
  179. userName: true,
  180. address: {
  181. street: true,
  182. },
  183. settings: {
  184. profile: {
  185. design: {
  186. color: true,
  187. background: true,
  188. },
  189. },
  190. },
  191. }, result)
  192. })
  193. it('should return true when no predicates are defined for a nested input', () => {
  194. const repeatDeepPasswordValidationRule = [
  195. [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)],
  196. [hasCapitalLetter, capitalLetterMag('RepeatedPassword')],
  197. [(a, all) => path(['user', 'password'], all) == a, equalMsg('Password', 'RepeatPassword')],
  198. ]
  199. const validationRules = {
  200. user: {
  201. password: passwordValidationRule,
  202. repeatPassword: repeatDeepPasswordValidationRule,
  203. },
  204. }
  205. const result = spected(validationRules, {user: {password: 'foobarR', repeatPassword: 'foobar', random: 'bar'}})
  206. deepEqual({
  207. user: {
  208. password: true,
  209. repeatPassword: [capitalLetterMag('RepeatedPassword'), equalMsg('Password', 'RepeatPassword')],
  210. random: true,
  211. }
  212. }, result)
  213. })
  214. it('should skip missing nested inputs', () => {
  215. const repeatDeepPasswordValidationRule = [
  216. [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)],
  217. [hasCapitalLetter, capitalLetterMag('RepeatedPassword')],
  218. [(a, all) => path(['user', 'password'], all) == a, equalMsg('Password', 'RepeatPassword')],
  219. ]
  220. const validationRules = {
  221. user: {
  222. password: passwordValidationRule,
  223. repeatPassword: repeatDeepPasswordValidationRule,
  224. },
  225. }
  226. const result = spected(validationRules, {user: {password: 'foobarR'}})
  227. deepEqual({
  228. user: {
  229. password: true,
  230. }
  231. }, result)
  232. })
  233. describe('validate', () => {
  234. it('should return the original value if validation is successful based on a success callback a => a', () => {
  235. const verify = validate(a => a, a => a)
  236. const validationRules = {
  237. name: nameValidationRule,
  238. }
  239. const result = verify(validationRules, {name: 'foobarbaz'})
  240. deepEqual({name: 'foobarbaz'}, result)
  241. })
  242. it('should return true if validation is successful based on a success callback a => true', () => {
  243. const verify = validate(() => true, () => false)
  244. const validationRules = {
  245. name: nameValidationRule,
  246. }
  247. const result = verify(validationRules, {name: 'foobarbaz'})
  248. deepEqual({name: true}, result)
  249. })
  250. it('should return false if validation has failed for an input based on an error callback () => false', () => {
  251. const verify = validate(() => true, () => false)
  252. const validationRules = {
  253. name: nameValidationRule,
  254. }
  255. const result = verify(validationRules, {name: ''})
  256. deepEqual({name: false}, result)
  257. })
  258. it('should return false if validation has failed for an input based on an error callback xs => xs[o]', () => {
  259. const verify = validate(() => true, xs => xs[0])
  260. const validationRules = {
  261. name: nameValidationRule,
  262. }
  263. const result = verify(validationRules, {name: ''})
  264. deepEqual({name: notEmptyMsg('Name')}, result)
  265. })
  266. it('should return an error when invalid', () => {
  267. const validationRules = {
  268. name: nameValidationRule,
  269. }
  270. const result = verify(validationRules, {name: ''})
  271. deepEqual({name: notEmptyMsg('Name')}, result)
  272. })
  273. it('should return true for field when valid', () => {
  274. const validationRules = {
  275. name: nameValidationRule,
  276. }
  277. const result = verify(validationRules, {name: 'foo'})
  278. deepEqual({name: true}, result)
  279. })
  280. it('should handle multiple validations and return the correct errors', () => {
  281. const validationRules = {
  282. name: nameValidationRule,
  283. random: randomValidationRule,
  284. }
  285. const result = verify(validationRules, {name: 'foo', random: 'A'})
  286. deepEqual({name: true, random: minimumMsg('Random', 3)}, result)
  287. })
  288. it('should handle multiple validations and return true for all fields when valid', () => {
  289. const validationRules = {
  290. name: nameValidationRule,
  291. random: randomValidationRule,
  292. }
  293. const result = verify(validationRules, {name: 'foo', random: 'Abcd'})
  294. deepEqual({name: true, random: true}, result)
  295. })
  296. it('should enable to verify to true if two form field values are equal', () => {
  297. const validationRules = {
  298. password: passwordValidationRule,
  299. repeatPassword: repeatPasswordValidationRule,
  300. }
  301. const result = verify(validationRules, {password: 'fooBar', repeatPassword: 'fooBar'})
  302. deepEqual({password: true, repeatPassword: true}, result)
  303. })
  304. it('should enable to verify to falsy if two form field values are not equal', () => {
  305. const validationRules = {
  306. password: passwordValidationRule,
  307. repeatPassword: repeatPasswordValidationRule,
  308. }
  309. const result = verify(validationRules, {password: 'fooBar', repeatPassword: 'fooBarBaz'})
  310. deepEqual({password: true, repeatPassword: equalMsg('Password', 'RepeatPassword')}, result)
  311. })
  312. it('should skip validation if no predicate function is provided.', () => {
  313. const validationRules = {
  314. password: [],
  315. }
  316. const result = verify(validationRules, {password: 'fooBar'})
  317. deepEqual({password: true}, result)
  318. })
  319. it('should skip validation if no predicate function is provided and other fields have rules', () => {
  320. const validationRules = {
  321. password: [],
  322. repeatPassword: repeatPasswordValidationRule,
  323. }
  324. const result = verify(validationRules, {password: 'fooBar', repeatPassword: 'fooBarBaz'})
  325. deepEqual({password: true, repeatPassword: equalMsg('Password', 'RepeatPassword')}, result)
  326. })
  327. it('should return true when no predicates are defined for an input', () => {
  328. const validationRules = {
  329. password: [],
  330. repeatPassword: [],
  331. }
  332. const result = verify(validationRules, {password: '', repeatPassword: ''})
  333. deepEqual({password: true, repeatPassword: true}, result)
  334. })
  335. it('should neglect key ordering', () => {
  336. const validationRules = {
  337. repeatPassword: repeatPasswordValidationRule,
  338. password: passwordValidationRule,
  339. }
  340. const result = verify(validationRules, {password: 'fooBar', repeatPassword: 'foobarbaZ'})
  341. deepEqual({password: true, repeatPassword: equalMsg('Password', 'RepeatPassword')}, result)
  342. })
  343. it('should return true when missing validations', () => {
  344. const validationRules = {
  345. password: passwordValidationRule,
  346. }
  347. const result = verify(validationRules, {password: 'fooBar', repeatPassword: 'foobarbaZ'})
  348. deepEqual({password: true, repeatPassword: true}, result)
  349. })
  350. it('should skip missing inputs', () => {
  351. const validationRules = {
  352. password: passwordValidationRule,
  353. repeatPassword: repeatPasswordValidationRule,
  354. }
  355. const result = verify(validationRules, {password: 'fooBar'})
  356. deepEqual({password: true}, result)
  357. })
  358. it('should handle deeply nested inputs', () => {
  359. const input = {
  360. id: 1,
  361. userName: 'Random',
  362. address: {
  363. street: 'Foobar',
  364. },
  365. settings: {
  366. profile: {
  367. design: {
  368. color: 'green',
  369. background: 'blue',
  370. },
  371. },
  372. },
  373. }
  374. const result = verify(spec, input)
  375. deepEqual({
  376. id: true,
  377. userName: true,
  378. address: {
  379. street: true,
  380. },
  381. settings: {
  382. profile: {
  383. design: {
  384. color: true,
  385. background: true,
  386. },
  387. },
  388. },
  389. }, result)
  390. })
  391. it('should return true when no predicates are defined for a nested input', () => {
  392. const repeatDeepPasswordValidationRule = [
  393. [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)],
  394. [hasCapitalLetter, capitalLetterMag('RepeatedPassword')],
  395. [(a, all) => path(['user', 'password'], all) == a, equalMsg('Password', 'RepeatPassword')],
  396. ]
  397. const validationRules = {
  398. user: {
  399. password: passwordValidationRule,
  400. repeatPassword: repeatDeepPasswordValidationRule,
  401. },
  402. }
  403. const result = verify(validationRules, {user: {password: 'foobarR', repeatPassword: 'foobar', random: 'bar'}})
  404. deepEqual({
  405. user: {
  406. password: true,
  407. repeatPassword: capitalLetterMag('RepeatedPassword'),
  408. random: true,
  409. }
  410. }, result)
  411. })
  412. it('should skip missing nested inputs', () => {
  413. const repeatDeepPasswordValidationRule = [
  414. [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)],
  415. [hasCapitalLetter, capitalLetterMag('RepeatedPassword')],
  416. [(a, all) => path(['user', 'password'], all) == a, equalMsg('Password', 'RepeatPassword')],
  417. ]
  418. const validationRules = {
  419. user: {
  420. password: passwordValidationRule,
  421. repeatPassword: repeatDeepPasswordValidationRule,
  422. },
  423. }
  424. const result = verify(validationRules, {user: {password: 'foobarR'}})
  425. deepEqual({
  426. user: {
  427. password: true,
  428. }
  429. }, result)
  430. })
  431. it('should pass the key and the value to the error message if it is a function', () => {
  432. const validationRules = {
  433. password: [[hasCapitalLetter, compose(flip, uncurryN(2))(capitalLetterMsgWithValue)]],
  434. }
  435. const result = verify(validationRules, { password: 'foobar' })
  436. deepEqual({ password: 'password should contain at least one uppercase letter. foobar is missing an uppercase letter.' }, result)
  437. })
  438. it('should work with dynamic rules: an array of inputs', () => {
  439. const capitalLetterMsg = 'capital letter missing'
  440. const userSpec = {
  441. firstName: [[isLengthGreaterThan(5), minimumMsg('firstName', 6)]],
  442. lastName: [[hasCapitalLetter, capitalLetterMsg]],
  443. }
  444. const validationRules = {
  445. id: [[ notEmpty, notEmptyMsg('id') ]],
  446. users: map(always(userSpec)),
  447. }
  448. const input = {
  449. id: 4,
  450. users: [
  451. {firstName: 'foobar', lastName: 'action'},
  452. {firstName: 'foo', lastName: 'bar'},
  453. {firstName: 'foobar', lastName: 'Action'},
  454. ]
  455. }
  456. const expected = {
  457. id: true,
  458. users: [
  459. {firstName: true, lastName: capitalLetterMsg},
  460. {firstName: minimumMsg('firstName', 6), lastName: capitalLetterMsg},
  461. {firstName: true, lastName: true},
  462. ]
  463. }
  464. const result = verify(validationRules, input)
  465. deepEqual(expected, result)
  466. })
  467. it('should work with dynamic rules: an object containing arbitrary inputs', () => {
  468. const capitalLetterMsg = 'capital letter missing'
  469. const userSpec = {
  470. firstName: [[isLengthGreaterThan(5), minimumMsg('firstName', 6)]],
  471. lastName: [[hasCapitalLetter, capitalLetterMsg]],
  472. }
  473. const validationRules = {
  474. id: [[ notEmpty, notEmptyMsg('id') ]],
  475. users: map((always(userSpec)))
  476. }
  477. const input = {
  478. id: 4,
  479. users: {
  480. one: {firstName: 'foobar', lastName: 'action'},
  481. two: {firstName: 'foo', lastName: 'bar'},
  482. three: {firstName: 'foobar', lastName: 'Action'},
  483. }
  484. }
  485. const expected = {
  486. id: true,
  487. users: {
  488. one: {firstName: true, lastName: capitalLetterMsg},
  489. two: {firstName: minimumMsg('firstName', 6), lastName: capitalLetterMsg},
  490. three: {firstName: true, lastName: true},
  491. }
  492. }
  493. const result = verify(validationRules, input)
  494. deepEqual(expected, result)
  495. })
  496. it('should work with an array as input value and display an error when validation fails', () => {
  497. const userSpec = [
  498. [ items => all(isLengthGreaterThan(5), items), 'Every item must have have at least 6 characters!'],
  499. ]
  500. const validationRules = {
  501. id: [[ notEmpty, notEmptyMsg('id') ]],
  502. users: userSpec,
  503. }
  504. const input = {
  505. id: 4,
  506. users: ['foo', 'foobar', 'foobarbaz']
  507. }
  508. const expected = {
  509. id: true,
  510. users: 'Every item must have have at least 6 characters!'
  511. }
  512. const result = verify(validationRules, input)
  513. deepEqual(expected, result)
  514. })
  515. it('should work with an array as input value and run the success function when all inputs validate successfully', () => {
  516. const userSpec = [
  517. [ items => all(isLengthGreaterThan(5), items), 'Every item must have have at least 6 characters!'],
  518. ]
  519. const validationRules = {
  520. id: [[ notEmpty, notEmptyMsg('id') ]],
  521. users: userSpec,
  522. }
  523. const input = {
  524. id: 4,
  525. users: ['foobar', 'foobarbaz']
  526. }
  527. const expected = {
  528. id: true,
  529. users: true,
  530. }
  531. const result = verify(validationRules, input)
  532. deepEqual(expected, result)
  533. })
  534. it('should work with a function as input value', () => {
  535. const verify = validate(a => a, a => a)
  536. const validationRules = {
  537. name: nameValidationRule,
  538. }
  539. const input = {name: 'foobarbaz'}
  540. const result = verify(validationRules, key => key ? ({...input, [key]: ''}) : input)
  541. deepEqual({name: ['Name should not be empty.']}, result)
  542. })
  543. })
  544. })