instance.validations.test.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. 'use strict';
  2. const chai = require('chai');
  3. const expect = chai.expect;
  4. const { DataTypes, Sequelize } = require('@sequelize/core');
  5. const Support = require('./support');
  6. describe(Support.getTestDialectTeaser('InstanceValidator'), () => {
  7. describe('#update', () => {
  8. it('should allow us to update specific columns without tripping the validations', async function () {
  9. const User = this.sequelize.define('model', {
  10. username: DataTypes.STRING,
  11. email: {
  12. type: DataTypes.STRING,
  13. allowNull: false,
  14. validate: {
  15. isEmail: {
  16. msg: 'You must enter a valid email address',
  17. },
  18. },
  19. },
  20. });
  21. await User.sync({ force: true });
  22. const user = await User.create({ username: 'bob', email: 'hello@world.com' });
  23. await User.update({ username: 'toni' }, { where: { id: user.id } });
  24. const user0 = await User.findByPk(1);
  25. expect(user0.username).to.equal('toni');
  26. });
  27. it('should be able to emit an error upon updating when a validation has failed from an instance', async function () {
  28. const Model = this.sequelize.define('model', {
  29. name: {
  30. type: DataTypes.STRING,
  31. allowNull: false,
  32. validate: {
  33. notEmpty: true, // don't allow empty strings
  34. },
  35. },
  36. });
  37. await Model.sync({ force: true });
  38. const model = await Model.create({ name: 'World' });
  39. try {
  40. await model.update({ name: '' });
  41. } catch (error) {
  42. expect(error).to.be.an.instanceOf(Error);
  43. expect(error.get('name')[0].message).to.equal('Validation notEmpty on name failed');
  44. }
  45. });
  46. it('should be able to emit an error upon updating when a validation has failed from the factory', async function () {
  47. const Model = this.sequelize.define('model', {
  48. name: {
  49. type: DataTypes.STRING,
  50. allowNull: false,
  51. validate: {
  52. notEmpty: true, // don't allow empty strings
  53. },
  54. },
  55. });
  56. await Model.sync({ force: true });
  57. await Model.create({ name: 'World' });
  58. try {
  59. await Model.update({ name: '' }, { where: { id: 1 } });
  60. } catch (error) {
  61. expect(error).to.be.an.instanceOf(Error);
  62. expect(error.get('name')[0].message).to.equal('Validation notEmpty on name failed');
  63. }
  64. });
  65. it('should enforce a unique constraint', async function () {
  66. const Model = this.sequelize.define('model', {
  67. uniqueName: { type: DataTypes.STRING, unique: 'uniqueName' },
  68. });
  69. const records = [{ uniqueName: 'unique name one' }, { uniqueName: 'unique name two' }];
  70. await Model.sync({ force: true });
  71. const instance0 = await Model.create(records[0]);
  72. expect(instance0).to.be.ok;
  73. const instance = await Model.create(records[1]);
  74. expect(instance).to.be.ok;
  75. const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be
  76. .rejected;
  77. expect(err).to.be.an.instanceOf(Error);
  78. expect(err.errors).to.have.length(1);
  79. if (Support.getTestDialect() === 'ibmi') {
  80. expect(err.errors[0].message).to.include('Duplicate key value specified');
  81. } else {
  82. expect(err.errors[0].path).to.include('uniqueName');
  83. expect(err.errors[0].message).to.include('must be unique');
  84. }
  85. });
  86. if (Support.getTestDialect() !== 'ibmi') {
  87. it('should allow a custom unique constraint error message', async function () {
  88. const Model = this.sequelize.define('model', {
  89. uniqueName: {
  90. type: DataTypes.STRING,
  91. unique: { msg: 'custom unique error message' },
  92. },
  93. });
  94. const records = [{ uniqueName: 'unique name one' }, { uniqueName: 'unique name two' }];
  95. await Model.sync({ force: true });
  96. const instance0 = await Model.create(records[0]);
  97. expect(instance0).to.be.ok;
  98. const instance = await Model.create(records[1]);
  99. expect(instance).to.be.ok;
  100. const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be
  101. .rejected;
  102. expect(err).to.be.an.instanceOf(Error);
  103. expect(err.errors).to.have.length(1);
  104. expect(err.errors[0].path).to.include('uniqueName');
  105. expect(err.errors[0].message).to.equal('custom unique error message');
  106. });
  107. it('should handle multiple unique messages correctly', async function () {
  108. const Model = this.sequelize.define('model', {
  109. uniqueName1: {
  110. type: DataTypes.STRING,
  111. unique: { msg: 'custom unique error message 1' },
  112. },
  113. uniqueName2: {
  114. type: DataTypes.STRING,
  115. unique: { msg: 'custom unique error message 2' },
  116. },
  117. });
  118. const records = [
  119. { uniqueName1: 'unique name one', uniqueName2: 'unique name one' },
  120. { uniqueName1: 'unique name one', uniqueName2: 'this is ok' },
  121. { uniqueName1: 'this is ok', uniqueName2: 'unique name one' },
  122. ];
  123. await Model.sync({ force: true });
  124. const instance = await Model.create(records[0]);
  125. expect(instance).to.be.ok;
  126. const err0 = await expect(Model.create(records[1])).to.be.rejected;
  127. expect(err0).to.be.an.instanceOf(Error);
  128. expect(err0.errors).to.have.length(1);
  129. expect(err0.errors[0].path).to.include('uniqueName1');
  130. expect(err0.errors[0].message).to.equal('custom unique error message 1');
  131. const err = await expect(Model.create(records[2])).to.be.rejected;
  132. expect(err).to.be.an.instanceOf(Error);
  133. expect(err.errors).to.have.length(1);
  134. expect(err.errors[0].path).to.include('uniqueName2');
  135. expect(err.errors[0].message).to.equal('custom unique error message 2');
  136. });
  137. }
  138. });
  139. describe('#create', () => {
  140. describe('generic', () => {
  141. beforeEach(async function () {
  142. const Project = this.sequelize.define('Project', {
  143. name: {
  144. type: DataTypes.STRING,
  145. allowNull: false,
  146. defaultValue: 'unknown',
  147. validate: {
  148. isIn: [['unknown', 'hello', 'test']],
  149. },
  150. },
  151. });
  152. const Task = this.sequelize.define('Task', {
  153. something: DataTypes.INTEGER,
  154. });
  155. Project.hasOne(Task);
  156. Task.belongsTo(Project);
  157. await this.sequelize.sync({ force: true });
  158. this.Project = Project;
  159. this.Task = Task;
  160. });
  161. it('correctly throws an error using create method ', async function () {
  162. try {
  163. await this.Project.create({ name: 'nope' });
  164. } catch (error) {
  165. expect(error).to.have.ownProperty('name');
  166. }
  167. });
  168. it('correctly validates using create method ', async function () {
  169. const project = await this.Project.create({});
  170. const task = await this.Task.create({ something: 1 });
  171. await project.setTask(task);
  172. await task.reload();
  173. expect(task.ProjectId).to.not.be.null;
  174. await task.setProject(project);
  175. await task.reload();
  176. expect(task.ProjectId).to.not.be.null;
  177. });
  178. });
  179. describe('explicitly validating primary/auto incremented columns', () => {
  180. it('should emit an error when we try to enter in a string for the id key without validation arguments', async function () {
  181. const User = this.sequelize.define('UserId', {
  182. id: {
  183. type: DataTypes.INTEGER,
  184. autoIncrement: true,
  185. primaryKey: true,
  186. validate: {
  187. isInt: true,
  188. },
  189. },
  190. });
  191. await User.sync({ force: true });
  192. try {
  193. await User.create({ id: 'helloworld' });
  194. } catch (error) {
  195. expect(error).to.be.an.instanceOf(Error);
  196. expect(error.get('id')[0].message).to.equal('Validation isInt on id failed');
  197. }
  198. });
  199. it('should emit an error when we try to enter in a string for an auto increment key (not named id)', async function () {
  200. const User = this.sequelize.define('UserId', {
  201. username: {
  202. type: DataTypes.INTEGER,
  203. autoIncrement: true,
  204. primaryKey: true,
  205. validate: {
  206. isInt: { args: true, msg: 'Username must be an integer!' },
  207. },
  208. },
  209. });
  210. await User.sync({ force: true });
  211. try {
  212. await User.create({ username: 'helloworldhelloworld' });
  213. } catch (error) {
  214. expect(error).to.be.an.instanceOf(Error);
  215. expect(error.get('username')[0].message).to.equal('Username must be an integer!');
  216. }
  217. });
  218. describe("primaryKey with the name as id with arguments for it's validatio", () => {
  219. beforeEach(async function () {
  220. this.User = this.sequelize.define('UserId', {
  221. id: {
  222. type: DataTypes.INTEGER,
  223. autoIncrement: true,
  224. primaryKey: true,
  225. validate: {
  226. isInt: { args: true, msg: 'ID must be an integer!' },
  227. },
  228. },
  229. });
  230. await this.User.sync({ force: true });
  231. });
  232. it('should emit an error when we try to enter in a string for the id key with validation arguments', async function () {
  233. try {
  234. await this.User.create({ id: 'helloworld' });
  235. } catch (error) {
  236. expect(error).to.be.an.instanceOf(Error);
  237. expect(error.get('id')[0].message).to.equal('ID must be an integer!');
  238. }
  239. });
  240. it('should emit an error when we try to enter in a string for an auto increment key through .build().validate()', async function () {
  241. const user = this.User.build({ id: 'helloworld' });
  242. const err = await expect(user.validate()).to.be.rejected;
  243. expect(err.get('id')[0].message).to.equal('ID must be an integer!');
  244. });
  245. it('should emit an error when we try to .save()', async function () {
  246. const user = this.User.build({ id: 'helloworld' });
  247. try {
  248. await user.save();
  249. } catch (error) {
  250. expect(error).to.be.an.instanceOf(Error);
  251. expect(error.get('id')[0].message).to.equal('ID must be an integer!');
  252. }
  253. });
  254. });
  255. });
  256. describe('pass all paths when validating', () => {
  257. beforeEach(async function () {
  258. const Project = this.sequelize.define('Project', {
  259. name: {
  260. type: DataTypes.STRING,
  261. allowNull: false,
  262. validate: {
  263. isIn: [['unknown', 'hello', 'test']],
  264. },
  265. },
  266. creatorName: {
  267. type: DataTypes.STRING,
  268. allowNull: false,
  269. },
  270. cost: {
  271. type: DataTypes.INTEGER,
  272. allowNull: false,
  273. },
  274. });
  275. const Task = this.sequelize.define('Task', {
  276. something: DataTypes.INTEGER,
  277. });
  278. Project.hasOne(Task);
  279. Task.belongsTo(Project);
  280. await Project.sync({ force: true });
  281. await Task.sync({ force: true });
  282. this.Project = Project;
  283. this.Task = Task;
  284. });
  285. it('produce 3 errors', async function () {
  286. try {
  287. await this.Project.create({});
  288. } catch (error) {
  289. expect(error).to.be.an.instanceOf(Error);
  290. delete error.stack; // longStackTraces
  291. expect(error.errors).to.have.length(3);
  292. }
  293. });
  294. });
  295. describe('not null schema validation', () => {
  296. beforeEach(async function () {
  297. const Project = this.sequelize.define('Project', {
  298. name: {
  299. type: DataTypes.STRING,
  300. allowNull: false,
  301. validate: {
  302. isIn: [['unknown', 'hello', 'test']], // important to be
  303. },
  304. },
  305. });
  306. await this.sequelize.sync({ force: true });
  307. this.Project = Project;
  308. });
  309. it('correctly throws an error using create method ', async function () {
  310. await this.Project.create({}).then(
  311. () => {
  312. throw new Error('Validation must be failed');
  313. },
  314. () => {
  315. // fail is ok
  316. },
  317. );
  318. });
  319. it('correctly throws an error using create method with default generated messages', async function () {
  320. try {
  321. await this.Project.create({});
  322. } catch (error) {
  323. expect(error).to.have.property('name', 'SequelizeValidationError');
  324. expect(error.message).equal('notNull violation: Project.name cannot be null');
  325. expect(error.errors).to.be.an('array').and.have.length(1);
  326. expect(error.errors[0]).to.have.property('message', 'Project.name cannot be null');
  327. }
  328. });
  329. });
  330. });
  331. it('correctly validates using custom validation methods', async function () {
  332. const User = this.sequelize.define(`User${Support.rand()}`, {
  333. name: {
  334. type: DataTypes.STRING,
  335. validate: {
  336. customFn(val, next) {
  337. if (val !== '2') {
  338. next("name should equal '2'");
  339. } else {
  340. next();
  341. }
  342. },
  343. },
  344. },
  345. });
  346. const failingUser = User.build({ name: '3' });
  347. const error = await expect(failingUser.validate()).to.be.rejected;
  348. expect(error).to.be.an.instanceOf(Error);
  349. expect(error.get('name')[0].message).to.equal("name should equal '2'");
  350. const successfulUser = User.build({ name: '2' });
  351. await expect(successfulUser.validate()).not.to.be.rejected;
  352. });
  353. it('supports promises with custom validation methods', async function () {
  354. const User = this.sequelize.define(`User${Support.rand()}`, {
  355. name: {
  356. type: DataTypes.STRING,
  357. validate: {
  358. async customFn(val) {
  359. await User.findAll();
  360. if (val === 'error') {
  361. throw new Error('Invalid username');
  362. }
  363. },
  364. },
  365. },
  366. });
  367. await User.sync();
  368. const error = await expect(User.build({ name: 'error' }).validate()).to.be.rejected;
  369. expect(error).to.be.instanceof(Sequelize.ValidationError);
  370. expect(error.get('name')[0].message).to.equal('Invalid username');
  371. await expect(User.build({ name: 'no error' }).validate()).not.to.be.rejected;
  372. });
  373. it('skips other validations if allowNull is true and the value is null', async function () {
  374. const User = this.sequelize.define(`User${Support.rand()}`, {
  375. age: {
  376. type: DataTypes.INTEGER,
  377. allowNull: true,
  378. validate: {
  379. min: { args: 0, msg: 'must be positive' },
  380. },
  381. },
  382. });
  383. const error = await expect(User.build({ age: -1 }).validate()).to.be.rejected;
  384. expect(error.get('age')[0].message).to.equal('must be positive');
  385. });
  386. it('validates a model with custom model-wide validation methods', async function () {
  387. const Foo = this.sequelize.define(
  388. `Foo${Support.rand()}`,
  389. {
  390. field1: {
  391. type: DataTypes.INTEGER,
  392. allowNull: true,
  393. },
  394. field2: {
  395. type: DataTypes.INTEGER,
  396. allowNull: true,
  397. },
  398. },
  399. {
  400. validate: {
  401. xnor() {
  402. if ((this.field1 === null) === (this.field2 === null)) {
  403. throw new Error('xnor failed');
  404. }
  405. },
  406. },
  407. },
  408. );
  409. const error = await expect(Foo.build({ field1: null, field2: null }).validate()).to.be.rejected;
  410. expect(error.get('xnor')[0].message).to.equal('xnor failed');
  411. await expect(Foo.build({ field1: 33, field2: null }).validate()).not.to.be.rejected;
  412. });
  413. it('validates model with a validator whose arg is an Array successfully twice in a row', async function () {
  414. const Foo = this.sequelize.define(`Foo${Support.rand()}`, {
  415. bar: {
  416. type: DataTypes.STRING,
  417. validate: {
  418. isIn: [['a', 'b']],
  419. },
  420. },
  421. });
  422. const foo = Foo.build({ bar: 'a' });
  423. await expect(foo.validate()).not.to.be.rejected;
  424. await expect(foo.validate()).not.to.be.rejected;
  425. });
  426. it('validates enums', async function () {
  427. const values = ['value1', 'value2'];
  428. const Bar = this.sequelize.define(`Bar${Support.rand()}`, {
  429. field: {
  430. type: DataTypes.ENUM(values),
  431. validate: {
  432. isIn: [values],
  433. },
  434. },
  435. });
  436. const failingBar = Bar.build({ field: 'value3' });
  437. const errors = await expect(failingBar.validate()).to.be.rejected;
  438. expect(errors.get('field')).to.have.length(2);
  439. expect(errors.get('field')[0].message).to.equal(
  440. `'value3' is not a valid choice for enum [ 'value1', 'value2' ]`,
  441. );
  442. expect(errors.get('field')[1].message).to.equal(`Validation isIn on field failed`);
  443. });
  444. it('skips validations for the given fields', async function () {
  445. const values = ['value1', 'value2'];
  446. const Bar = this.sequelize.define(`Bar${Support.rand()}`, {
  447. field: {
  448. type: DataTypes.ENUM(values),
  449. validate: {
  450. isIn: [values],
  451. },
  452. },
  453. });
  454. const failingBar = Bar.build({ field: 'value3' });
  455. await expect(failingBar.validate({ skip: ['field'] })).not.to.be.rejected;
  456. });
  457. it('skips validations for fields with value that is BaseExpression', async function () {
  458. const values = ['value1', 'value2'];
  459. const Bar = this.sequelize.define(`Bar${Support.rand()}`, {
  460. field: {
  461. type: DataTypes.ENUM(values),
  462. validate: {
  463. isIn: [values],
  464. },
  465. },
  466. });
  467. const failingBar = Bar.build({ field: this.sequelize.literal('5 + 1') });
  468. await expect(failingBar.validate()).not.to.be.rejected;
  469. });
  470. it('raises an error if saving a different value into an immutable field', async function () {
  471. const User = this.sequelize.define('User', {
  472. name: {
  473. type: DataTypes.STRING,
  474. validate: {
  475. isImmutable: true,
  476. },
  477. },
  478. });
  479. await User.sync({ force: true });
  480. const user = await User.create({ name: 'RedCat' });
  481. expect(user.getDataValue('name')).to.equal('RedCat');
  482. user.setDataValue('name', 'YellowCat');
  483. const errors = await expect(user.save()).to.be.rejected;
  484. expect(errors.get('name')[0].message).to.eql('Validation isImmutable on name failed');
  485. });
  486. it('allows setting an immutable field if the record is unsaved', async function () {
  487. const User = this.sequelize.define('User', {
  488. name: {
  489. type: DataTypes.STRING,
  490. validate: {
  491. isImmutable: true,
  492. },
  493. },
  494. });
  495. const user = User.build({ name: 'RedCat' });
  496. expect(user.getDataValue('name')).to.equal('RedCat');
  497. user.setDataValue('name', 'YellowCat');
  498. await expect(user.validate()).not.to.be.rejected;
  499. });
  500. it('raises an error for array on a STRING', async function () {
  501. const User = this.sequelize.define('User', {
  502. email: {
  503. type: DataTypes.STRING,
  504. },
  505. });
  506. await expect(
  507. User.build({
  508. email: ['iama', 'dummy.com'],
  509. }).validate(),
  510. ).to.be.rejectedWith(Sequelize.ValidationError);
  511. });
  512. it('raises an error for array on a STRING(20)', async function () {
  513. const User = this.sequelize.define('User', {
  514. email: {
  515. type: DataTypes.STRING(20),
  516. },
  517. });
  518. await expect(
  519. User.build({
  520. email: ['iama', 'dummy.com'],
  521. }).validate(),
  522. ).to.be.rejectedWith(Sequelize.ValidationError);
  523. });
  524. it('raises an error for array on a TEXT', async function () {
  525. const User = this.sequelize.define('User', {
  526. email: {
  527. type: DataTypes.TEXT,
  528. },
  529. });
  530. await expect(
  531. User.build({
  532. email: ['iama', 'dummy.com'],
  533. }).validate(),
  534. ).to.be.rejectedWith(Sequelize.ValidationError);
  535. });
  536. it('raises an error for {} on a STRING', async function () {
  537. const User = this.sequelize.define('User', {
  538. email: {
  539. type: DataTypes.STRING,
  540. },
  541. });
  542. await expect(
  543. User.build({
  544. email: { lol: true },
  545. }).validate(),
  546. ).to.be.rejectedWith(Sequelize.ValidationError);
  547. });
  548. it('raises an error for {} on a STRING(20)', async function () {
  549. const User = this.sequelize.define('User', {
  550. email: {
  551. type: DataTypes.STRING(20),
  552. },
  553. });
  554. await expect(
  555. User.build({
  556. email: { lol: true },
  557. }).validate(),
  558. ).to.be.rejectedWith(Sequelize.ValidationError);
  559. });
  560. it('raises an error for {} on a TEXT', async function () {
  561. const User = this.sequelize.define('User', {
  562. email: {
  563. type: DataTypes.TEXT,
  564. },
  565. });
  566. await expect(
  567. User.build({
  568. email: { lol: true },
  569. }).validate(),
  570. ).to.be.rejectedWith(Sequelize.ValidationError);
  571. });
  572. it('does not raise an error for null on a STRING (where null is allowed)', async function () {
  573. const User = this.sequelize.define('User', {
  574. email: {
  575. type: DataTypes.STRING,
  576. },
  577. });
  578. await expect(
  579. User.build({
  580. email: null,
  581. }).validate(),
  582. ).not.to.be.rejected;
  583. });
  584. it('validates VIRTUAL fields', async function () {
  585. const User = this.sequelize.define('user', {
  586. password_hash: DataTypes.STRING,
  587. salt: DataTypes.STRING,
  588. password: {
  589. type: DataTypes.VIRTUAL,
  590. set(val) {
  591. this.setDataValue('password', val);
  592. this.setDataValue('password_hash', this.salt + val);
  593. },
  594. validate: {
  595. isLongEnough(val) {
  596. if (val.length < 7) {
  597. throw new Error('Please choose a longer password');
  598. }
  599. },
  600. },
  601. },
  602. });
  603. await Promise.all([
  604. expect(
  605. User.build({
  606. password: 'short',
  607. salt: '42',
  608. }).validate(),
  609. ).to.be.rejected.then(errors => {
  610. expect(errors.get('password')[0].message).to.equal('Please choose a longer password');
  611. }),
  612. expect(
  613. User.build({
  614. password: 'loooooooong',
  615. salt: '42',
  616. }).validate(),
  617. ).not.to.be.rejected,
  618. ]);
  619. });
  620. it('allows me to add custom validation functions to validator.js', async function () {
  621. this.sequelize.Validator.extend('isExactly7Characters', val => {
  622. return val.length === 7;
  623. });
  624. const User = this.sequelize.define('User', {
  625. name: {
  626. type: DataTypes.STRING,
  627. validate: {
  628. isExactly7Characters: true,
  629. },
  630. },
  631. });
  632. await expect(
  633. User.build({
  634. name: 'abcdefg',
  635. }).validate(),
  636. ).not.to.be.rejected;
  637. const errors = await expect(
  638. User.build({
  639. name: 'a',
  640. }).validate(),
  641. ).to.be.rejected;
  642. expect(errors.get('name')[0].message).to.equal(
  643. 'Validation isExactly7Characters on name failed',
  644. );
  645. });
  646. });