asset.e2e-spec.ts 21 KB

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