custom-fields.e2e-spec.ts 16 KB

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