import.e2e-spec.ts 20 KB

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