asset.e2e-spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { omit } from '@vendure/common/lib/omit';
  3. import { pick } from '@vendure/common/lib/pick';
  4. import { mergeConfig } from '@vendure/core';
  5. import { createTestEnvironment } from '@vendure/testing';
  6. import fs from 'fs-extra';
  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. import {
  12. AssetFragment,
  13. AssetWithTagsAndFocalPointFragment,
  14. CreateAssetsMutation,
  15. DeletionResult,
  16. GetProductWithVariantsQuery,
  17. LogicalOperator,
  18. SortOrder,
  19. } from './graphql/generated-e2e-admin-types';
  20. import * as Codegen from './graphql/generated-e2e-admin-types';
  21. import {
  22. CREATE_ASSETS,
  23. DELETE_ASSET,
  24. GET_ASSET,
  25. GET_ASSET_FRAGMENT_FIRST,
  26. GET_ASSET_LIST,
  27. GET_PRODUCT_WITH_VARIANTS,
  28. UPDATE_ASSET,
  29. } from './graphql/shared-definitions';
  30. describe('Asset resolver', () => {
  31. const { server, adminClient } = createTestEnvironment(
  32. mergeConfig(testConfig(), {
  33. assetOptions: {
  34. permittedFileTypes: ['image/*', '.pdf', '.zip'],
  35. },
  36. }),
  37. );
  38. let firstAssetId: string;
  39. let createdAssetId: string;
  40. beforeAll(async () => {
  41. await server.init({
  42. initialData,
  43. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  44. customerCount: 1,
  45. });
  46. await adminClient.asSuperAdmin();
  47. }, TEST_SETUP_TIMEOUT_MS);
  48. afterAll(async () => {
  49. await server.destroy();
  50. });
  51. it('assets', async () => {
  52. const { assets } = await adminClient.query<
  53. Codegen.GetAssetListQuery,
  54. Codegen.GetAssetListQueryVariables
  55. >(GET_ASSET_LIST, {
  56. options: {
  57. sort: {
  58. name: SortOrder.ASC,
  59. },
  60. },
  61. });
  62. expect(assets.totalItems).toBe(4);
  63. expect(assets.items.map(a => omit(a, ['id']))).toEqual([
  64. {
  65. fileSize: 1680,
  66. mimeType: 'image/jpeg',
  67. name: 'alexandru-acea-686569-unsplash.jpg',
  68. preview: 'test-url/test-assets/alexandru-acea-686569-unsplash__preview.jpg',
  69. source: 'test-url/test-assets/alexandru-acea-686569-unsplash.jpg',
  70. type: 'IMAGE',
  71. },
  72. {
  73. fileSize: 1680,
  74. mimeType: 'image/jpeg',
  75. name: 'derick-david-409858-unsplash.jpg',
  76. preview: 'test-url/test-assets/derick-david-409858-unsplash__preview.jpg',
  77. source: 'test-url/test-assets/derick-david-409858-unsplash.jpg',
  78. type: 'IMAGE',
  79. },
  80. {
  81. fileSize: 1680,
  82. mimeType: 'image/jpeg',
  83. name: 'florian-olivo-1166419-unsplash.jpg',
  84. preview: 'test-url/test-assets/florian-olivo-1166419-unsplash__preview.jpg',
  85. source: 'test-url/test-assets/florian-olivo-1166419-unsplash.jpg',
  86. type: 'IMAGE',
  87. },
  88. {
  89. fileSize: 1680,
  90. mimeType: 'image/jpeg',
  91. name: 'vincent-botta-736919-unsplash.jpg',
  92. preview: 'test-url/test-assets/vincent-botta-736919-unsplash__preview.jpg',
  93. source: 'test-url/test-assets/vincent-botta-736919-unsplash.jpg',
  94. type: 'IMAGE',
  95. },
  96. ]);
  97. firstAssetId = assets.items[0].id;
  98. });
  99. it('asset', async () => {
  100. const { asset } = await adminClient.query<Codegen.GetAssetQuery, Codegen.GetAssetQueryVariables>(
  101. GET_ASSET,
  102. {
  103. id: firstAssetId,
  104. },
  105. );
  106. expect(asset).toEqual({
  107. fileSize: 1680,
  108. height: 48,
  109. id: firstAssetId,
  110. mimeType: 'image/jpeg',
  111. name: 'alexandru-acea-686569-unsplash.jpg',
  112. preview: 'test-url/test-assets/alexandru-acea-686569-unsplash__preview.jpg',
  113. source: 'test-url/test-assets/alexandru-acea-686569-unsplash.jpg',
  114. type: 'IMAGE',
  115. width: 48,
  116. });
  117. });
  118. /**
  119. * https://github.com/vendure-ecommerce/vendure/issues/459
  120. */
  121. it('transforms URL when fragment defined before query (GH issue #459)', async () => {
  122. const { asset } = await adminClient.query<
  123. Codegen.GetAssetFragmentFirstQuery,
  124. Codegen.GetAssetFragmentFirstQueryVariables
  125. >(GET_ASSET_FRAGMENT_FIRST, {
  126. id: firstAssetId,
  127. });
  128. expect(asset?.preview).toBe('test-url/test-assets/alexandru-acea-686569-unsplash__preview.jpg');
  129. });
  130. describe('createAssets', () => {
  131. function isAsset(
  132. input: CreateAssetsMutation['createAssets'][number],
  133. ): input is AssetWithTagsAndFocalPointFragment {
  134. return input.hasOwnProperty('name');
  135. }
  136. it('permitted types by mime type', async () => {
  137. const filesToUpload = [
  138. path.join(__dirname, 'fixtures/assets/pps1.jpg'),
  139. path.join(__dirname, 'fixtures/assets/pps2.jpg'),
  140. ];
  141. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  142. mutation: CREATE_ASSETS,
  143. filePaths: filesToUpload,
  144. mapVariables: filePaths => ({
  145. input: filePaths.map(p => ({ file: null })),
  146. }),
  147. });
  148. expect(createAssets.length).toBe(2);
  149. const results = createAssets.filter(isAsset);
  150. expect(results.map(a => omit(a, ['id'])).sort((a, b) => (a.name < b.name ? -1 : 1))).toEqual([
  151. {
  152. fileSize: 1680,
  153. focalPoint: null,
  154. mimeType: 'image/jpeg',
  155. name: 'pps1.jpg',
  156. preview: 'test-url/test-assets/pps1__preview.jpg',
  157. source: 'test-url/test-assets/pps1.jpg',
  158. tags: [],
  159. type: 'IMAGE',
  160. },
  161. {
  162. fileSize: 1680,
  163. focalPoint: null,
  164. mimeType: 'image/jpeg',
  165. name: 'pps2.jpg',
  166. preview: 'test-url/test-assets/pps2__preview.jpg',
  167. source: 'test-url/test-assets/pps2.jpg',
  168. tags: [],
  169. type: 'IMAGE',
  170. },
  171. ]);
  172. createdAssetId = results[0].id;
  173. });
  174. it('permitted type by file extension', async () => {
  175. const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.pdf')];
  176. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  177. mutation: CREATE_ASSETS,
  178. filePaths: filesToUpload,
  179. mapVariables: filePaths => ({
  180. input: filePaths.map(p => ({ file: null })),
  181. }),
  182. });
  183. expect(createAssets.length).toBe(1);
  184. const results = createAssets.filter(isAsset);
  185. expect(results.map(a => omit(a, ['id']))).toEqual([
  186. {
  187. fileSize: 1680,
  188. focalPoint: null,
  189. mimeType: 'application/pdf',
  190. name: 'dummy.pdf',
  191. preview: 'test-url/test-assets/dummy__preview.pdf.png',
  192. source: 'test-url/test-assets/dummy.pdf',
  193. tags: [],
  194. type: 'BINARY',
  195. },
  196. ]);
  197. });
  198. // https://github.com/vendure-ecommerce/vendure/issues/727
  199. it('file extension with shared type', async () => {
  200. const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.zip')];
  201. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  202. mutation: CREATE_ASSETS,
  203. filePaths: filesToUpload,
  204. mapVariables: filePaths => ({
  205. input: filePaths.map(p => ({ file: null })),
  206. }),
  207. });
  208. expect(createAssets.length).toBe(1);
  209. expect(isAsset(createAssets[0])).toBe(true);
  210. const results = createAssets.filter(isAsset);
  211. expect(results.map(a => omit(a, ['id']))).toEqual([
  212. {
  213. fileSize: 1680,
  214. focalPoint: null,
  215. mimeType: 'application/zip',
  216. name: 'dummy.zip',
  217. preview: 'test-url/test-assets/dummy__preview.zip.png',
  218. source: 'test-url/test-assets/dummy.zip',
  219. tags: [],
  220. type: 'BINARY',
  221. },
  222. ]);
  223. });
  224. it('not permitted type', async () => {
  225. const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.txt')];
  226. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  227. mutation: CREATE_ASSETS,
  228. filePaths: filesToUpload,
  229. mapVariables: filePaths => ({
  230. input: filePaths.map(p => ({ file: null })),
  231. }),
  232. });
  233. expect(createAssets.length).toBe(1);
  234. expect(createAssets[0]).toEqual({
  235. message: 'The MIME type "text/plain" is not permitted.',
  236. mimeType: 'text/plain',
  237. fileName: 'dummy.txt',
  238. });
  239. });
  240. it('create with new tags', async () => {
  241. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  242. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  243. mutation: CREATE_ASSETS,
  244. filePaths: filesToUpload,
  245. mapVariables: filePaths => ({
  246. input: filePaths.map(p => ({ file: null, tags: ['foo', 'bar'] })),
  247. }),
  248. });
  249. const results = createAssets.filter(isAsset);
  250. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  251. {
  252. id: 'T_9',
  253. name: 'pps1.jpg',
  254. tags: [
  255. { id: 'T_1', value: 'foo' },
  256. { id: 'T_2', value: 'bar' },
  257. ],
  258. },
  259. ]);
  260. });
  261. it('create with existing tags', async () => {
  262. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  263. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  264. mutation: CREATE_ASSETS,
  265. filePaths: filesToUpload,
  266. mapVariables: filePaths => ({
  267. input: filePaths.map(p => ({ file: null, tags: ['foo', 'bar'] })),
  268. }),
  269. });
  270. const results = createAssets.filter(isAsset);
  271. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  272. {
  273. id: 'T_10',
  274. name: 'pps1.jpg',
  275. tags: [
  276. { id: 'T_1', value: 'foo' },
  277. { id: 'T_2', value: 'bar' },
  278. ],
  279. },
  280. ]);
  281. });
  282. it('create with new and existing tags', async () => {
  283. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  284. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  285. mutation: CREATE_ASSETS,
  286. filePaths: filesToUpload,
  287. mapVariables: filePaths => ({
  288. input: filePaths.map(p => ({ file: null, tags: ['quux', 'bar'] })),
  289. }),
  290. });
  291. const results = createAssets.filter(isAsset);
  292. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  293. {
  294. id: 'T_11',
  295. name: 'pps1.jpg',
  296. tags: [
  297. { id: 'T_3', value: 'quux' },
  298. { id: 'T_2', value: 'bar' },
  299. ],
  300. },
  301. ]);
  302. });
  303. // https://github.com/vendure-ecommerce/vendure/issues/990
  304. it('errors if the filesize is too large', async () => {
  305. /**
  306. * Based on https://stackoverflow.com/a/49433633/772859
  307. */
  308. function createEmptyFileOfSize(fileName: string, sizeInBytes: number) {
  309. return new Promise((resolve, reject) => {
  310. const fh = fs.openSync(fileName, 'w');
  311. fs.writeSync(fh, 'ok', Math.max(0, sizeInBytes - 2));
  312. fs.closeSync(fh);
  313. resolve(true);
  314. });
  315. }
  316. const twentyOneMib = 22020096;
  317. const filename = path.join(__dirname, 'fixtures/assets/temp_large_file.pdf');
  318. await createEmptyFileOfSize(filename, twentyOneMib);
  319. try {
  320. const { createAssets }: Codegen.CreateAssetsMutation = await adminClient.fileUploadMutation({
  321. mutation: CREATE_ASSETS,
  322. filePaths: [filename],
  323. mapVariables: filePaths => ({
  324. input: filePaths.map(p => ({ file: null })),
  325. }),
  326. });
  327. fail('Should have thrown');
  328. } catch (e: any) {
  329. expect(e.message).toContain('File truncated as it exceeds the 20971520 byte size limit');
  330. } finally {
  331. fs.rmSync(filename);
  332. }
  333. });
  334. });
  335. describe('filter by tags', () => {
  336. it('and', async () => {
  337. const { assets } = await adminClient.query<
  338. Codegen.GetAssetListQuery,
  339. Codegen.GetAssetListQueryVariables
  340. >(GET_ASSET_LIST, {
  341. options: {
  342. tags: ['foo', 'bar'],
  343. tagsOperator: LogicalOperator.AND,
  344. },
  345. });
  346. expect(assets.items.map(i => i.id).sort()).toEqual(['T_10', 'T_9']);
  347. });
  348. it('or', async () => {
  349. const { assets } = await adminClient.query<
  350. Codegen.GetAssetListQuery,
  351. Codegen.GetAssetListQueryVariables
  352. >(GET_ASSET_LIST, {
  353. options: {
  354. tags: ['foo', 'bar'],
  355. tagsOperator: LogicalOperator.OR,
  356. },
  357. });
  358. expect(assets.items.map(i => i.id).sort()).toEqual(['T_10', 'T_11', 'T_9']);
  359. });
  360. it('empty array', async () => {
  361. const { assets } = await adminClient.query<
  362. Codegen.GetAssetListQuery,
  363. Codegen.GetAssetListQueryVariables
  364. >(GET_ASSET_LIST, {
  365. options: {
  366. tags: [],
  367. },
  368. });
  369. expect(assets.totalItems).toBe(11);
  370. });
  371. });
  372. describe('updateAsset', () => {
  373. it('update name', async () => {
  374. const { updateAsset } = await adminClient.query<
  375. Codegen.UpdateAssetMutation,
  376. Codegen.UpdateAssetMutationVariables
  377. >(UPDATE_ASSET, {
  378. input: {
  379. id: firstAssetId,
  380. name: 'new name',
  381. },
  382. });
  383. expect(updateAsset.name).toEqual('new name');
  384. });
  385. it('update focalPoint', async () => {
  386. const { updateAsset } = await adminClient.query<
  387. Codegen.UpdateAssetMutation,
  388. Codegen.UpdateAssetMutationVariables
  389. >(UPDATE_ASSET, {
  390. input: {
  391. id: firstAssetId,
  392. focalPoint: {
  393. x: 0.3,
  394. y: 0.9,
  395. },
  396. },
  397. });
  398. expect(updateAsset.focalPoint).toEqual({
  399. x: 0.3,
  400. y: 0.9,
  401. });
  402. });
  403. it('unset focalPoint', async () => {
  404. const { updateAsset } = await adminClient.query<
  405. Codegen.UpdateAssetMutation,
  406. Codegen.UpdateAssetMutationVariables
  407. >(UPDATE_ASSET, {
  408. input: {
  409. id: firstAssetId,
  410. focalPoint: null,
  411. },
  412. });
  413. expect(updateAsset.focalPoint).toEqual(null);
  414. });
  415. it('update tags', async () => {
  416. const { updateAsset } = await adminClient.query<
  417. Codegen.UpdateAssetMutation,
  418. Codegen.UpdateAssetMutationVariables
  419. >(UPDATE_ASSET, {
  420. input: {
  421. id: firstAssetId,
  422. tags: ['foo', 'quux'],
  423. },
  424. });
  425. expect(updateAsset.tags).toEqual([
  426. { id: 'T_1', value: 'foo' },
  427. { id: 'T_3', value: 'quux' },
  428. ]);
  429. });
  430. it('remove tags', async () => {
  431. const { updateAsset } = await adminClient.query<
  432. Codegen.UpdateAssetMutation,
  433. Codegen.UpdateAssetMutationVariables
  434. >(UPDATE_ASSET, {
  435. input: {
  436. id: firstAssetId,
  437. tags: [],
  438. },
  439. });
  440. expect(updateAsset.tags).toEqual([]);
  441. });
  442. });
  443. describe('deleteAsset', () => {
  444. let firstProduct: NonNullable<GetProductWithVariantsQuery['product']>;
  445. beforeAll(async () => {
  446. const { product } = await adminClient.query<
  447. Codegen.GetProductWithVariantsQuery,
  448. Codegen.GetProductWithVariantsQueryVariables
  449. >(GET_PRODUCT_WITH_VARIANTS, {
  450. id: 'T_1',
  451. });
  452. firstProduct = product!;
  453. });
  454. it('non-featured asset', async () => {
  455. const { deleteAsset } = await adminClient.query<
  456. Codegen.DeleteAssetMutation,
  457. Codegen.DeleteAssetMutationVariables
  458. >(DELETE_ASSET, {
  459. input: {
  460. assetId: createdAssetId,
  461. },
  462. });
  463. expect(deleteAsset.result).toBe(DeletionResult.DELETED);
  464. const { asset } = await adminClient.query<Codegen.GetAssetQuery, Codegen.GetAssetQueryVariables>(
  465. GET_ASSET,
  466. {
  467. id: createdAssetId,
  468. },
  469. );
  470. expect(asset).toBeNull();
  471. });
  472. it('featured asset not deleted', async () => {
  473. const { deleteAsset } = await adminClient.query<
  474. Codegen.DeleteAssetMutation,
  475. Codegen.DeleteAssetMutationVariables
  476. >(DELETE_ASSET, {
  477. input: {
  478. assetId: firstProduct.featuredAsset!.id,
  479. },
  480. });
  481. expect(deleteAsset.result).toBe(DeletionResult.NOT_DELETED);
  482. expect(deleteAsset.message).toContain('The selected Asset is featured by 1 Product');
  483. const { asset } = await adminClient.query<Codegen.GetAssetQuery, Codegen.GetAssetQueryVariables>(
  484. GET_ASSET,
  485. {
  486. id: firstAssetId,
  487. },
  488. );
  489. expect(asset).not.toBeNull();
  490. });
  491. it('featured asset force deleted', async () => {
  492. const { product: p1 } = await adminClient.query<
  493. Codegen.GetProductWithVariantsQuery,
  494. Codegen.GetProductWithVariantsQueryVariables
  495. >(GET_PRODUCT_WITH_VARIANTS, {
  496. id: firstProduct.id,
  497. });
  498. expect(p1!.assets.length).toEqual(1);
  499. const { deleteAsset } = await adminClient.query<
  500. Codegen.DeleteAssetMutation,
  501. Codegen.DeleteAssetMutationVariables
  502. >(DELETE_ASSET, {
  503. input: {
  504. assetId: firstProduct.featuredAsset!.id,
  505. force: true,
  506. },
  507. });
  508. expect(deleteAsset.result).toBe(DeletionResult.DELETED);
  509. const { asset } = await adminClient.query<Codegen.GetAssetQuery, Codegen.GetAssetQueryVariables>(
  510. GET_ASSET,
  511. {
  512. id: firstAssetId,
  513. },
  514. );
  515. expect(asset).not.toBeNull();
  516. const { product } = await adminClient.query<
  517. Codegen.GetProductWithVariantsQuery,
  518. Codegen.GetProductWithVariantsQueryVariables
  519. >(GET_PRODUCT_WITH_VARIANTS, {
  520. id: firstProduct.id,
  521. });
  522. expect(product!.featuredAsset).toBeNull();
  523. expect(product!.assets.length).toEqual(0);
  524. });
  525. });
  526. });