import.e2e-spec.ts 19 KB

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