asset.e2e-spec.ts 20 KB

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