custom-fields.e2e-spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // Force the timezone to avoid tests failing in other locales
  2. process.env.TZ = 'UTC';
  3. import { LanguageCode } from '@vendure/common/lib/generated-types';
  4. import { CustomFields } from '@vendure/core/dist/config/custom-field/custom-field-types';
  5. import { createTestEnvironment } from '@vendure/testing';
  6. import gql from 'graphql-tag';
  7. import path from 'path';
  8. import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
  9. import { initialData } from './fixtures/e2e-initial-data';
  10. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  11. // tslint:disable:no-non-null-assertion
  12. const customConfig = {
  13. ...testConfig,
  14. ...{
  15. customFields: {
  16. Product: [
  17. { name: 'nullable', type: 'string' },
  18. { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
  19. { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
  20. { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
  21. { name: 'intWithDefault', type: 'int', defaultValue: 5 },
  22. { name: 'floatWithDefault', type: 'float', defaultValue: 5.5 },
  23. { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
  24. {
  25. name: 'dateTimeWithDefault',
  26. type: 'datetime',
  27. defaultValue: new Date('2019-04-30T12:59:16.4158386Z'),
  28. },
  29. { name: 'validateString', type: 'string', pattern: '^[0-9][a-z]+$' },
  30. { name: 'validateLocaleString', type: 'localeString', pattern: '^[0-9][a-z]+$' },
  31. { name: 'validateInt', type: 'int', min: 0, max: 10 },
  32. { name: 'validateFloat', type: 'float', min: 0.5, max: 10.5 },
  33. {
  34. name: 'validateDateTime',
  35. type: 'datetime',
  36. min: '2019-01-01T08:30',
  37. max: '2019-06-01T08:30',
  38. },
  39. {
  40. name: 'validateFn1',
  41. type: 'string',
  42. validate: value => {
  43. if (value !== 'valid') {
  44. return `The value ['${value}'] is not valid`;
  45. }
  46. },
  47. },
  48. {
  49. name: 'validateFn2',
  50. type: 'string',
  51. validate: value => {
  52. if (value !== 'valid') {
  53. return [
  54. {
  55. languageCode: LanguageCode.en,
  56. value: `The value ['${value}'] is not valid`,
  57. },
  58. ];
  59. }
  60. },
  61. },
  62. {
  63. name: 'stringWithOptions',
  64. type: 'string',
  65. options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  66. },
  67. {
  68. name: 'nonPublic',
  69. type: 'string',
  70. defaultValue: 'hi!',
  71. public: false,
  72. },
  73. {
  74. name: 'public',
  75. type: 'string',
  76. defaultValue: 'ho!',
  77. public: true,
  78. },
  79. {
  80. name: 'longString',
  81. type: 'string',
  82. },
  83. ],
  84. Facet: [
  85. {
  86. name: 'translated',
  87. type: 'localeString',
  88. },
  89. ],
  90. } as CustomFields,
  91. },
  92. };
  93. describe('Custom fields', () => {
  94. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  95. beforeAll(async () => {
  96. await server.init({
  97. dataDir,
  98. initialData,
  99. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  100. customerCount: 1,
  101. });
  102. await adminClient.init();
  103. await adminClient.asSuperAdmin();
  104. await shopClient.init();
  105. }, TEST_SETUP_TIMEOUT_MS);
  106. afterAll(async () => {
  107. await server.destroy();
  108. });
  109. it('globalSettings.serverConfig.customFieldConfig', async () => {
  110. const { globalSettings } = await adminClient.query(gql`
  111. query {
  112. globalSettings {
  113. serverConfig {
  114. customFieldConfig {
  115. Product {
  116. ... on CustomField {
  117. name
  118. type
  119. }
  120. }
  121. }
  122. }
  123. }
  124. }
  125. `);
  126. expect(globalSettings.serverConfig.customFieldConfig).toEqual({
  127. Product: [
  128. { name: 'nullable', type: 'string' },
  129. { name: 'notNullable', type: 'string' },
  130. { name: 'stringWithDefault', type: 'string' },
  131. { name: 'localeStringWithDefault', type: 'localeString' },
  132. { name: 'intWithDefault', type: 'int' },
  133. { name: 'floatWithDefault', type: 'float' },
  134. { name: 'booleanWithDefault', type: 'boolean' },
  135. { name: 'dateTimeWithDefault', type: 'datetime' },
  136. { name: 'validateString', type: 'string' },
  137. { name: 'validateLocaleString', type: 'localeString' },
  138. { name: 'validateInt', type: 'int' },
  139. { name: 'validateFloat', type: 'float' },
  140. { name: 'validateDateTime', type: 'datetime' },
  141. { name: 'validateFn1', type: 'string' },
  142. { name: 'validateFn2', type: 'string' },
  143. { name: 'stringWithOptions', type: 'string' },
  144. { name: 'nonPublic', type: 'string' },
  145. { name: 'public', type: 'string' },
  146. { name: 'longString', type: 'string' },
  147. ],
  148. });
  149. });
  150. it('get nullable with no default', async () => {
  151. const { product } = await adminClient.query(gql`
  152. query {
  153. product(id: "T_1") {
  154. id
  155. name
  156. customFields {
  157. nullable
  158. }
  159. }
  160. }
  161. `);
  162. expect(product).toEqual({
  163. id: 'T_1',
  164. name: 'Laptop',
  165. customFields: {
  166. nullable: null,
  167. },
  168. });
  169. });
  170. it('get entity with localeString only', async () => {
  171. const { facet } = await adminClient.query(gql`
  172. query {
  173. facet(id: "T_1") {
  174. id
  175. name
  176. customFields {
  177. translated
  178. }
  179. }
  180. }
  181. `);
  182. expect(facet).toEqual({
  183. id: 'T_1',
  184. name: 'category',
  185. customFields: {
  186. translated: null,
  187. },
  188. });
  189. });
  190. it('get fields with default values', async () => {
  191. const { product } = await adminClient.query(gql`
  192. query {
  193. product(id: "T_1") {
  194. id
  195. name
  196. customFields {
  197. stringWithDefault
  198. localeStringWithDefault
  199. intWithDefault
  200. floatWithDefault
  201. booleanWithDefault
  202. dateTimeWithDefault
  203. }
  204. }
  205. }
  206. `);
  207. expect(product).toEqual({
  208. id: 'T_1',
  209. name: 'Laptop',
  210. customFields: {
  211. stringWithDefault: 'hello',
  212. localeStringWithDefault: 'hola',
  213. intWithDefault: 5,
  214. floatWithDefault: 5.5,
  215. booleanWithDefault: true,
  216. dateTimeWithDefault: '2019-04-30T12:59:16.415Z',
  217. },
  218. });
  219. });
  220. it(
  221. 'update non-nullable field',
  222. assertThrowsWithMessage(async () => {
  223. await adminClient.query(gql`
  224. mutation {
  225. updateProduct(input: { id: "T_1", customFields: { notNullable: null } }) {
  226. id
  227. }
  228. }
  229. `);
  230. }, 'NOT NULL constraint failed: product.customFieldsNotnullable'),
  231. );
  232. it('string length allows long strings', async () => {
  233. const longString = Array.from({ length: 5000 }, v => 'hello there!').join(' ');
  234. const result = await adminClient.query(
  235. gql`
  236. mutation($stringValue: String!) {
  237. updateProduct(input: { id: "T_1", customFields: { longString: $stringValue } }) {
  238. id
  239. customFields {
  240. longString
  241. }
  242. }
  243. }
  244. `,
  245. { stringValue: longString },
  246. );
  247. expect(result.updateProduct.customFields.longString).toBe(longString);
  248. });
  249. describe('validation', () => {
  250. it(
  251. 'invalid string',
  252. assertThrowsWithMessage(async () => {
  253. await adminClient.query(gql`
  254. mutation {
  255. updateProduct(input: { id: "T_1", customFields: { validateString: "hello" } }) {
  256. id
  257. }
  258. }
  259. `);
  260. }, `The custom field 'validateString' value ['hello'] does not match the pattern [^[0-9][a-z]+$]`),
  261. );
  262. it(
  263. 'invalid string option',
  264. assertThrowsWithMessage(async () => {
  265. await adminClient.query(gql`
  266. mutation {
  267. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "tiny" } }) {
  268. id
  269. }
  270. }
  271. `);
  272. }, `The custom field 'stringWithOptions' value ['tiny'] is invalid. Valid options are ['small', 'medium', 'large']`),
  273. );
  274. it('valid string option', async () => {
  275. const { updateProduct } = await adminClient.query(gql`
  276. mutation {
  277. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) {
  278. id
  279. customFields {
  280. stringWithOptions
  281. }
  282. }
  283. }
  284. `);
  285. expect(updateProduct.customFields.stringWithOptions).toBe('medium');
  286. });
  287. it(
  288. 'invalid localeString',
  289. assertThrowsWithMessage(async () => {
  290. await adminClient.query(gql`
  291. mutation {
  292. updateProduct(
  293. input: {
  294. id: "T_1"
  295. translations: [
  296. {
  297. id: "T_1"
  298. languageCode: en
  299. customFields: { validateLocaleString: "servus" }
  300. }
  301. ]
  302. }
  303. ) {
  304. id
  305. }
  306. }
  307. `);
  308. }, `The custom field 'validateLocaleString' value ['servus'] does not match the pattern [^[0-9][a-z]+$]`),
  309. );
  310. it(
  311. 'invalid int',
  312. assertThrowsWithMessage(async () => {
  313. await adminClient.query(gql`
  314. mutation {
  315. updateProduct(input: { id: "T_1", customFields: { validateInt: 12 } }) {
  316. id
  317. }
  318. }
  319. `);
  320. }, `The custom field 'validateInt' value [12] is greater than the maximum [10]`),
  321. );
  322. it(
  323. 'invalid float',
  324. assertThrowsWithMessage(async () => {
  325. await adminClient.query(gql`
  326. mutation {
  327. updateProduct(input: { id: "T_1", customFields: { validateFloat: 10.6 } }) {
  328. id
  329. }
  330. }
  331. `);
  332. }, `The custom field 'validateFloat' value [10.6] is greater than the maximum [10.5]`),
  333. );
  334. it(
  335. 'invalid datetime',
  336. assertThrowsWithMessage(async () => {
  337. await adminClient.query(gql`
  338. mutation {
  339. updateProduct(
  340. input: {
  341. id: "T_1"
  342. customFields: { validateDateTime: "2019-01-01T05:25:00.000Z" }
  343. }
  344. ) {
  345. id
  346. }
  347. }
  348. `);
  349. }, `The custom field 'validateDateTime' value [2019-01-01T05:25:00.000Z] is less than the minimum [2019-01-01T08:30]`),
  350. );
  351. it(
  352. 'invalid validate function with string',
  353. assertThrowsWithMessage(async () => {
  354. await adminClient.query(gql`
  355. mutation {
  356. updateProduct(input: { id: "T_1", customFields: { validateFn1: "invalid" } }) {
  357. id
  358. }
  359. }
  360. `);
  361. }, `The value ['invalid'] is not valid`),
  362. );
  363. it(
  364. 'invalid validate function with localized string',
  365. assertThrowsWithMessage(async () => {
  366. await adminClient.query(gql`
  367. mutation {
  368. updateProduct(input: { id: "T_1", customFields: { validateFn2: "invalid" } }) {
  369. id
  370. }
  371. }
  372. `);
  373. }, `The value ['invalid'] is not valid`),
  374. );
  375. });
  376. describe('public access', () => {
  377. it(
  378. 'non-public throws for Shop API',
  379. assertThrowsWithMessage(async () => {
  380. await shopClient.query(gql`
  381. query {
  382. product(id: "T_1") {
  383. id
  384. customFields {
  385. nonPublic
  386. }
  387. }
  388. }
  389. `);
  390. }, `Cannot query field "nonPublic" on type "ProductCustomFields"`),
  391. );
  392. it('publicly accessible via Shop API', async () => {
  393. const { product } = await shopClient.query(gql`
  394. query {
  395. product(id: "T_1") {
  396. id
  397. customFields {
  398. public
  399. }
  400. }
  401. }
  402. `);
  403. expect(product.customFields.public).toBe('ho!');
  404. });
  405. });
  406. describe('sort & filter', () => {
  407. it('can sort by custom fields', async () => {
  408. const { products } = await adminClient.query(gql`
  409. query {
  410. products(options: { sort: { nullable: ASC } }) {
  411. totalItems
  412. }
  413. }
  414. `);
  415. expect(products.totalItems).toBe(1);
  416. });
  417. it('can filter by custom fields', async () => {
  418. const { products } = await adminClient.query(gql`
  419. query {
  420. products(options: { filter: { stringWithDefault: { contains: "hello" } } }) {
  421. totalItems
  422. }
  423. }
  424. `);
  425. expect(products.totalItems).toBe(1);
  426. });
  427. });
  428. });