custom-fields.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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. },
  88. },
  89. );
  90. await adminClient.init();
  91. await shopClient.init();
  92. }, TEST_SETUP_TIMEOUT_MS);
  93. afterAll(async () => {
  94. await server.destroy();
  95. });
  96. it('globalSettings.serverConfig.customFieldConfig', async () => {
  97. const { globalSettings } = await adminClient.query(gql`
  98. query {
  99. globalSettings {
  100. serverConfig {
  101. customFieldConfig {
  102. Product {
  103. ... on CustomField {
  104. name
  105. type
  106. }
  107. }
  108. }
  109. }
  110. }
  111. }
  112. `);
  113. expect(globalSettings.serverConfig.customFieldConfig).toEqual({
  114. Product: [
  115. { name: 'nullable', type: 'string' },
  116. { name: 'notNullable', type: 'string' },
  117. { name: 'stringWithDefault', type: 'string' },
  118. { name: 'localeStringWithDefault', type: 'localeString' },
  119. { name: 'intWithDefault', type: 'int' },
  120. { name: 'floatWithDefault', type: 'float' },
  121. { name: 'booleanWithDefault', type: 'boolean' },
  122. { name: 'dateTimeWithDefault', type: 'datetime' },
  123. { name: 'validateString', type: 'string' },
  124. { name: 'validateLocaleString', type: 'localeString' },
  125. { name: 'validateInt', type: 'int' },
  126. { name: 'validateFloat', type: 'float' },
  127. { name: 'validateDateTime', type: 'datetime' },
  128. { name: 'validateFn1', type: 'string' },
  129. { name: 'validateFn2', type: 'string' },
  130. { name: 'stringWithOptions', type: 'string' },
  131. { name: 'nonPublic', type: 'string' },
  132. { name: 'public', type: 'string' },
  133. ],
  134. });
  135. });
  136. it('get nullable with no default', async () => {
  137. const { product } = await adminClient.query(gql`
  138. query {
  139. product(id: "T_1") {
  140. id
  141. name
  142. customFields {
  143. nullable
  144. }
  145. }
  146. }
  147. `);
  148. expect(product).toEqual({
  149. id: 'T_1',
  150. name: 'Laptop',
  151. customFields: {
  152. nullable: null,
  153. },
  154. });
  155. });
  156. it('get fields with default values', async () => {
  157. const { product } = await adminClient.query(gql`
  158. query {
  159. product(id: "T_1") {
  160. id
  161. name
  162. customFields {
  163. stringWithDefault
  164. localeStringWithDefault
  165. intWithDefault
  166. floatWithDefault
  167. booleanWithDefault
  168. dateTimeWithDefault
  169. }
  170. }
  171. }
  172. `);
  173. expect(product).toEqual({
  174. id: 'T_1',
  175. name: 'Laptop',
  176. customFields: {
  177. stringWithDefault: 'hello',
  178. localeStringWithDefault: 'hola',
  179. intWithDefault: 5,
  180. floatWithDefault: 5.5,
  181. booleanWithDefault: true,
  182. dateTimeWithDefault: '2019-04-30T12:59:16.415Z',
  183. },
  184. });
  185. });
  186. it(
  187. 'update non-nullable field',
  188. assertThrowsWithMessage(async () => {
  189. await adminClient.query(gql`
  190. mutation {
  191. updateProduct(input: { id: "T_1", customFields: { notNullable: null } }) {
  192. id
  193. }
  194. }
  195. `);
  196. }, 'NOT NULL constraint failed: product.customFieldsNotnullable'),
  197. );
  198. describe('validation', () => {
  199. it(
  200. 'invalid string',
  201. assertThrowsWithMessage(async () => {
  202. await adminClient.query(gql`
  203. mutation {
  204. updateProduct(input: { id: "T_1", customFields: { validateString: "hello" } }) {
  205. id
  206. }
  207. }
  208. `);
  209. }, `The custom field 'validateString' value ['hello'] does not match the pattern [^[0-9][a-z]+$]`),
  210. );
  211. it(
  212. 'invalid string option',
  213. assertThrowsWithMessage(async () => {
  214. await adminClient.query(gql`
  215. mutation {
  216. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "tiny" } }) {
  217. id
  218. }
  219. }
  220. `);
  221. }, `The custom field 'stringWithOptions' value ['tiny'] is invalid. Valid options are ['small', 'medium', 'large']`),
  222. );
  223. it('valid string option', async () => {
  224. const { updateProduct } = await adminClient.query(gql`
  225. mutation {
  226. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) {
  227. id
  228. customFields {
  229. stringWithOptions
  230. }
  231. }
  232. }
  233. `);
  234. expect(updateProduct.customFields.stringWithOptions).toBe('medium');
  235. });
  236. it(
  237. 'invalid localeString',
  238. assertThrowsWithMessage(async () => {
  239. await adminClient.query(gql`
  240. mutation {
  241. updateProduct(
  242. input: {
  243. id: "T_1"
  244. translations: [
  245. {
  246. id: "T_1"
  247. languageCode: en
  248. customFields: { validateLocaleString: "servus" }
  249. }
  250. ]
  251. }
  252. ) {
  253. id
  254. }
  255. }
  256. `);
  257. }, `The custom field 'validateLocaleString' value ['servus'] does not match the pattern [^[0-9][a-z]+$]`),
  258. );
  259. it(
  260. 'invalid int',
  261. assertThrowsWithMessage(async () => {
  262. await adminClient.query(gql`
  263. mutation {
  264. updateProduct(input: { id: "T_1", customFields: { validateInt: 12 } }) {
  265. id
  266. }
  267. }
  268. `);
  269. }, `The custom field 'validateInt' value [12] is greater than the maximum [10]`),
  270. );
  271. it(
  272. 'invalid float',
  273. assertThrowsWithMessage(async () => {
  274. await adminClient.query(gql`
  275. mutation {
  276. updateProduct(input: { id: "T_1", customFields: { validateFloat: 10.6 } }) {
  277. id
  278. }
  279. }
  280. `);
  281. }, `The custom field 'validateFloat' value [10.6] is greater than the maximum [10.5]`),
  282. );
  283. it(
  284. 'invalid datetime',
  285. assertThrowsWithMessage(async () => {
  286. await adminClient.query(gql`
  287. mutation {
  288. updateProduct(
  289. input: {
  290. id: "T_1"
  291. customFields: { validateDateTime: "2019-01-01T05:25:00.000Z" }
  292. }
  293. ) {
  294. id
  295. }
  296. }
  297. `);
  298. }, `The custom field 'validateDateTime' value [2019-01-01T05:25:00.000Z] is less than the minimum [2019-01-01T08:30]`),
  299. );
  300. it(
  301. 'invalid validate function with string',
  302. assertThrowsWithMessage(async () => {
  303. await adminClient.query(gql`
  304. mutation {
  305. updateProduct(input: { id: "T_1", customFields: { validateFn1: "invalid" } }) {
  306. id
  307. }
  308. }
  309. `);
  310. }, `The value ['invalid'] is not valid`),
  311. );
  312. it(
  313. 'invalid validate function with localized string',
  314. assertThrowsWithMessage(async () => {
  315. await adminClient.query(gql`
  316. mutation {
  317. updateProduct(input: { id: "T_1", customFields: { validateFn2: "invalid" } }) {
  318. id
  319. }
  320. }
  321. `);
  322. }, `The value ['invalid'] is not valid`),
  323. );
  324. });
  325. describe('public access', () => {
  326. it(
  327. 'non-public throws for Shop API',
  328. assertThrowsWithMessage(async () => {
  329. await shopClient.query(gql`
  330. query {
  331. product(id: "T_1") {
  332. id
  333. customFields {
  334. nonPublic
  335. }
  336. }
  337. }
  338. `);
  339. }, `Cannot query field "nonPublic" on type "ProductCustomFields"`),
  340. );
  341. it('publicly accessible via Shop API', async () => {
  342. const { product } = await shopClient.query(gql`
  343. query {
  344. product(id: "T_1") {
  345. id
  346. customFields {
  347. public
  348. }
  349. }
  350. }
  351. `);
  352. expect(product.customFields.public).toBe('ho!');
  353. });
  354. });
  355. describe('sort & filter', () => {
  356. it('can sort by custom fields', async () => {
  357. const { products } = await adminClient.query(gql`
  358. query {
  359. products(options: {
  360. sort: {
  361. nullable: ASC
  362. }
  363. }) {
  364. totalItems
  365. }
  366. }
  367. `);
  368. expect(products.totalItems).toBe(1);
  369. });
  370. it('can filter by custom fields', async () => {
  371. const { products } = await adminClient.query(gql`
  372. query {
  373. products(options: {
  374. filter: {
  375. stringWithDefault: {
  376. contains: "hello"
  377. }
  378. }
  379. }) {
  380. totalItems
  381. }
  382. }
  383. `);
  384. expect(products.totalItems).toBe(1);
  385. });
  386. });
  387. });