import.e2e-spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import { omit } from '@vendure/common/lib/omit';
  2. import { User } from '@vendure/core';
  3. import { createTestEnvironment } from '@vendure/testing';
  4. import * as fs from 'fs';
  5. import gql from 'graphql-tag';
  6. import http from 'http';
  7. import path from 'path';
  8. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  9. import { initialData } from '../../../e2e-common/e2e-initial-data';
  10. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  11. describe('Import resolver', () => {
  12. const { server, adminClient } = createTestEnvironment({
  13. ...testConfig(),
  14. customFields: {
  15. Product: [
  16. { type: 'string', name: 'pageType' },
  17. {
  18. name: 'owner',
  19. public: true,
  20. nullable: true,
  21. type: 'relation',
  22. entity: User,
  23. eager: true,
  24. },
  25. {
  26. name: 'keywords',
  27. public: true,
  28. nullable: true,
  29. type: 'string',
  30. list: true,
  31. },
  32. {
  33. name: 'localName',
  34. type: 'localeString',
  35. },
  36. ],
  37. ProductVariant: [
  38. { type: 'boolean', name: 'valid' },
  39. { type: 'int', name: 'weight' },
  40. ],
  41. },
  42. });
  43. beforeAll(async () => {
  44. await server.init({
  45. initialData,
  46. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
  47. customerCount: 0,
  48. });
  49. await adminClient.asSuperAdmin();
  50. }, TEST_SETUP_TIMEOUT_MS);
  51. afterAll(async () => {
  52. await server.destroy();
  53. });
  54. it('imports products', async () => {
  55. // TODO: waste a few more hours actually fixing this for real
  56. // Forgive me this abomination of a work-around.
  57. // On the inital run (as in CI), when the sqlite db has just been populated,
  58. // this test will fail due to an "out of memory" exception originating from
  59. // SqljsQueryRunner.ts:79:22, which is part of the findOne() operation on the
  60. // Session repository called from the AuthService.validateSession() method.
  61. // After several hours of fruitless hunting, I did what any desperate JavaScript
  62. // developer would do, and threw in a setTimeout. Which of course "works"...
  63. const timeout = process.env.CI ? 2000 : 1000;
  64. await new Promise(resolve => {
  65. setTimeout(resolve, timeout);
  66. });
  67. const csvFile = path.join(__dirname, 'fixtures', 'product-import.csv');
  68. const result = await adminClient.fileUploadMutation({
  69. mutation: gql`
  70. mutation ImportProducts($csvFile: Upload!) {
  71. importProducts(csvFile: $csvFile) {
  72. imported
  73. processed
  74. errors
  75. }
  76. }
  77. `,
  78. filePaths: [csvFile],
  79. mapVariables: () => ({ csvFile: null }),
  80. });
  81. expect(result.importProducts.errors).toEqual([
  82. 'Invalid Record Length: header length is 20, got 1 on line 8',
  83. ]);
  84. expect(result.importProducts.imported).toBe(4);
  85. expect(result.importProducts.processed).toBe(4);
  86. const productResult = await adminClient.query(
  87. gql`
  88. query GetProducts($options: ProductListOptions) {
  89. products(options: $options) {
  90. totalItems
  91. items {
  92. id
  93. name
  94. slug
  95. description
  96. featuredAsset {
  97. id
  98. name
  99. preview
  100. source
  101. }
  102. assets {
  103. id
  104. name
  105. preview
  106. source
  107. }
  108. optionGroups {
  109. id
  110. code
  111. name
  112. }
  113. facetValues {
  114. id
  115. name
  116. facet {
  117. id
  118. name
  119. }
  120. }
  121. customFields {
  122. pageType
  123. owner {
  124. id
  125. }
  126. keywords
  127. localName
  128. }
  129. variants {
  130. id
  131. name
  132. sku
  133. price
  134. taxCategory {
  135. id
  136. name
  137. }
  138. options {
  139. id
  140. code
  141. }
  142. assets {
  143. id
  144. name
  145. preview
  146. source
  147. }
  148. featuredAsset {
  149. id
  150. name
  151. preview
  152. source
  153. }
  154. facetValues {
  155. id
  156. code
  157. name
  158. facet {
  159. id
  160. name
  161. }
  162. }
  163. stockOnHand
  164. trackInventory
  165. stockMovements {
  166. items {
  167. ... on StockMovement {
  168. id
  169. type
  170. quantity
  171. }
  172. }
  173. }
  174. customFields {
  175. valid
  176. weight
  177. }
  178. }
  179. }
  180. }
  181. }
  182. `,
  183. {
  184. options: {},
  185. },
  186. );
  187. expect(productResult.products.totalItems).toBe(4);
  188. const paperStretcher = productResult.products.items.find(
  189. (p: any) => p.name === 'Perfect Paper Stretcher',
  190. );
  191. const easel = productResult.products.items.find((p: any) => p.name === 'Mabef M/02 Studio Easel');
  192. const pencils = productResult.products.items.find((p: any) => p.name === 'Giotto Mega Pencils');
  193. const smock = productResult.products.items.find((p: any) => p.name === 'Artists Smock');
  194. // Omit FacetValues & options due to variations in the ordering between different DB engines
  195. expect(omit(paperStretcher, ['facetValues', 'options'], true)).toMatchSnapshot();
  196. expect(omit(easel, ['facetValues', 'options'], true)).toMatchSnapshot();
  197. expect(omit(pencils, ['facetValues', 'options'], true)).toMatchSnapshot();
  198. expect(omit(smock, ['facetValues', 'options'], true)).toMatchSnapshot();
  199. const byName = (e: { name: string }) => e.name;
  200. const byCode = (e: { code: string }) => e.code;
  201. expect(paperStretcher.facetValues).toEqual([]);
  202. expect(easel.facetValues).toEqual([]);
  203. expect(pencils.facetValues).toEqual([]);
  204. expect(smock.facetValues.map(byName).sort()).toEqual(['Denim', 'clothes']);
  205. expect(paperStretcher.variants[0].facetValues.map(byName).sort()).toEqual(['Accessory', 'KB']);
  206. expect(paperStretcher.variants[1].facetValues.map(byName).sort()).toEqual(['Accessory', 'KB']);
  207. expect(paperStretcher.variants[2].facetValues.map(byName).sort()).toEqual(['Accessory', 'KB']);
  208. expect(paperStretcher.variants[0].options.map(byCode).sort()).toEqual(['half-imperial']);
  209. expect(paperStretcher.variants[1].options.map(byCode).sort()).toEqual(['quarter-imperial']);
  210. expect(paperStretcher.variants[2].options.map(byCode).sort()).toEqual(['full-imperial']);
  211. expect(easel.variants[0].facetValues.map(byName).sort()).toEqual(['Easel', 'Mabef']);
  212. expect(pencils.variants[0].facetValues.map(byName).sort()).toEqual(['Xmas Sale']);
  213. expect(pencils.variants[1].facetValues.map(byName).sort()).toEqual(['Xmas Sale']);
  214. expect(pencils.variants[0].options.map(byCode).sort()).toEqual(['box-of-8']);
  215. expect(pencils.variants[1].options.map(byCode).sort()).toEqual(['box-of-12']);
  216. expect(smock.variants[0].facetValues.map(byName).sort()).toEqual([]);
  217. expect(smock.variants[1].facetValues.map(byName).sort()).toEqual([]);
  218. expect(smock.variants[2].facetValues.map(byName).sort()).toEqual([]);
  219. expect(smock.variants[3].facetValues.map(byName).sort()).toEqual([]);
  220. expect(smock.variants[0].options.map(byCode).sort()).toEqual(['beige', 'small']);
  221. expect(smock.variants[1].options.map(byCode).sort()).toEqual(['beige', 'large']);
  222. expect(smock.variants[2].options.map(byCode).sort()).toEqual(['navy', 'small']);
  223. expect(smock.variants[3].options.map(byCode).sort()).toEqual(['large', 'navy']);
  224. // Import relation custom fields
  225. expect(paperStretcher.customFields.owner.id).toBe('T_1');
  226. expect(easel.customFields.owner.id).toBe('T_1');
  227. expect(pencils.customFields.owner.id).toBe('T_1');
  228. expect(smock.customFields.owner.id).toBe('T_1');
  229. // Import non-list custom fields
  230. expect(smock.variants[0].customFields.valid).toEqual(true);
  231. expect(smock.variants[0].customFields.weight).toEqual(500);
  232. expect(smock.variants[1].customFields.valid).toEqual(false);
  233. expect(smock.variants[1].customFields.weight).toEqual(500);
  234. expect(smock.variants[2].customFields.valid).toEqual(null);
  235. expect(smock.variants[2].customFields.weight).toEqual(500);
  236. expect(smock.variants[3].customFields.valid).toEqual(true);
  237. expect(smock.variants[3].customFields.weight).toEqual(500);
  238. expect(smock.variants[4].customFields.valid).toEqual(false);
  239. expect(smock.variants[4].customFields.weight).toEqual(null);
  240. // Import list custom fields
  241. expect(paperStretcher.customFields.keywords).toEqual(['paper', 'stretching', 'watercolor']);
  242. expect(easel.customFields.keywords).toEqual([]);
  243. expect(pencils.customFields.keywords).toEqual([]);
  244. expect(smock.customFields.keywords).toEqual(['apron', 'clothing']);
  245. // Import localeString custom fields
  246. expect(paperStretcher.customFields.localName).toEqual('localPPS');
  247. expect(easel.customFields.localName).toEqual('localMabef');
  248. expect(pencils.customFields.localName).toEqual('localGiotto');
  249. expect(smock.customFields.localName).toEqual('localSmock');
  250. }, 20000);
  251. it('imports products with multiple languages', async () => {
  252. // TODO: see test above
  253. const timeout = process.env.CI ? 2000 : 1000;
  254. await new Promise(resolve => {
  255. setTimeout(resolve, timeout);
  256. });
  257. const csvFile = path.join(__dirname, 'fixtures', 'e2e-product-import-multi-languages.csv');
  258. const result = await adminClient.fileUploadMutation({
  259. mutation: gql`
  260. mutation ImportProducts($csvFile: Upload!) {
  261. importProducts(csvFile: $csvFile) {
  262. imported
  263. processed
  264. errors
  265. }
  266. }
  267. `,
  268. filePaths: [csvFile],
  269. mapVariables: () => ({ csvFile: null }),
  270. });
  271. expect(result.importProducts.errors).toEqual([]);
  272. expect(result.importProducts.imported).toBe(1);
  273. expect(result.importProducts.processed).toBe(1);
  274. const productResult = await adminClient.query(
  275. gql`
  276. query GetProducts($options: ProductListOptions) {
  277. products(options: $options) {
  278. totalItems
  279. items {
  280. id
  281. name
  282. slug
  283. description
  284. featuredAsset {
  285. id
  286. name
  287. preview
  288. source
  289. }
  290. assets {
  291. id
  292. name
  293. preview
  294. source
  295. }
  296. optionGroups {
  297. id
  298. code
  299. name
  300. }
  301. facetValues {
  302. id
  303. name
  304. facet {
  305. id
  306. name
  307. }
  308. }
  309. customFields {
  310. pageType
  311. owner {
  312. id
  313. }
  314. keywords
  315. localName
  316. }
  317. variants {
  318. id
  319. name
  320. sku
  321. price
  322. taxCategory {
  323. id
  324. name
  325. }
  326. options {
  327. id
  328. code
  329. name
  330. }
  331. assets {
  332. id
  333. name
  334. preview
  335. source
  336. }
  337. featuredAsset {
  338. id
  339. name
  340. preview
  341. source
  342. }
  343. facetValues {
  344. id
  345. code
  346. name
  347. facet {
  348. id
  349. name
  350. }
  351. }
  352. stockOnHand
  353. trackInventory
  354. stockMovements {
  355. items {
  356. ... on StockMovement {
  357. id
  358. type
  359. quantity
  360. }
  361. }
  362. }
  363. customFields {
  364. weight
  365. }
  366. }
  367. }
  368. }
  369. }
  370. `,
  371. {
  372. options: {},
  373. },
  374. {
  375. languageCode: 'zh_Hans',
  376. },
  377. );
  378. expect(productResult.products.totalItems).toBe(5);
  379. const paperStretcher = productResult.products.items.find((p: any) => p.name === '奇妙的纸张拉伸器');
  380. // Omit FacetValues & options due to variations in the ordering between different DB engines
  381. expect(omit(paperStretcher, ['facetValues', 'options'], true)).toMatchSnapshot();
  382. const byName = (e: { name: string }) => e.name;
  383. expect(paperStretcher.facetValues.map(byName).sort()).toEqual(['KB', '饰品']);
  384. expect(paperStretcher.variants[0].options.map(byName).sort()).toEqual(['半英制']);
  385. expect(paperStretcher.variants[1].options.map(byName).sort()).toEqual(['四分之一英制']);
  386. expect(paperStretcher.variants[2].options.map(byName).sort()).toEqual(['全英制']);
  387. // Import list custom fields
  388. expect(paperStretcher.customFields.keywords).toEqual(['paper, stretch']);
  389. // Import localeString custom fields
  390. expect(paperStretcher.customFields.localName).toEqual('纸张拉伸器');
  391. }, 20000);
  392. describe('asset urls', () => {
  393. let staticServer: http.Server;
  394. beforeAll(() => {
  395. // Set up minimal static file server
  396. staticServer = http
  397. .createServer((req, res) => {
  398. const filePath = path.join(__dirname, 'fixtures/assets', req?.url ?? '');
  399. fs.readFile(filePath, (err, data) => {
  400. if (err) {
  401. res.writeHead(404);
  402. res.end(JSON.stringify(err));
  403. return;
  404. }
  405. res.writeHead(200);
  406. res.end(data);
  407. });
  408. })
  409. .listen(3456);
  410. });
  411. afterAll(() => {
  412. if (staticServer) {
  413. return new Promise<void>((resolve, reject) => {
  414. staticServer.close(err => {
  415. if (err) {
  416. reject(err);
  417. } else {
  418. resolve();
  419. }
  420. });
  421. });
  422. }
  423. });
  424. it('imports assets with url paths', async () => {
  425. const timeout = process.env.CI ? 2000 : 1000;
  426. await new Promise(resolve => {
  427. setTimeout(resolve, timeout);
  428. });
  429. const csvFile = path.join(__dirname, 'fixtures', 'e2e-product-import-asset-urls.csv');
  430. const result = await adminClient.fileUploadMutation({
  431. mutation: gql`
  432. mutation ImportProducts($csvFile: Upload!) {
  433. importProducts(csvFile: $csvFile) {
  434. imported
  435. processed
  436. errors
  437. }
  438. }
  439. `,
  440. filePaths: [csvFile],
  441. mapVariables: () => ({ csvFile: null }),
  442. });
  443. expect(result.importProducts.errors).toEqual([]);
  444. expect(result.importProducts.imported).toBe(1);
  445. expect(result.importProducts.processed).toBe(1);
  446. const productResult = await adminClient.query(
  447. gql`
  448. query GetProducts($options: ProductListOptions) {
  449. products(options: $options) {
  450. totalItems
  451. items {
  452. id
  453. name
  454. featuredAsset {
  455. id
  456. name
  457. preview
  458. }
  459. }
  460. }
  461. }
  462. `,
  463. {
  464. options: {
  465. filter: {
  466. name: { contains: 'guitar' },
  467. },
  468. },
  469. },
  470. );
  471. expect(productResult.products.items.length).toBe(1);
  472. expect(productResult.products.items[0].featuredAsset.preview).toBe(
  473. 'test-url/test-assets/guitar__preview.jpg',
  474. );
  475. });
  476. });
  477. });