asset.e2e-spec.ts 20 KB

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