import.e2e-spec.ts 21 KB

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