asset.e2e-spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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 gql from 'graphql-tag';
  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 { ASSET_FRAGMENT } from './graphql/fragments';
  11. import {
  12. AssetFragment,
  13. CreateAssets,
  14. DeleteAsset,
  15. DeletionResult,
  16. GetAsset,
  17. GetAssetFragmentFirst,
  18. GetAssetList,
  19. GetProductWithVariants,
  20. LogicalOperator,
  21. SortOrder,
  22. UpdateAsset,
  23. } from './graphql/generated-e2e-admin-types';
  24. import {
  25. DELETE_ASSET,
  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'],
  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<GetAssetList.Query, GetAssetList.Variables>(
  53. GET_ASSET_LIST,
  54. {
  55. options: {
  56. sort: {
  57. name: SortOrder.ASC,
  58. },
  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<GetAsset.Query, GetAsset.Variables>(GET_ASSET, {
  101. id: firstAssetId,
  102. });
  103. expect(asset).toEqual({
  104. fileSize: 1680,
  105. height: 48,
  106. id: firstAssetId,
  107. mimeType: 'image/jpeg',
  108. name: 'alexandru-acea-686569-unsplash.jpg',
  109. preview: 'test-url/test-assets/alexandru-acea-686569-unsplash__preview.jpg',
  110. source: 'test-url/test-assets/alexandru-acea-686569-unsplash.jpg',
  111. type: 'IMAGE',
  112. width: 48,
  113. });
  114. });
  115. /**
  116. * https://github.com/vendure-ecommerce/vendure/issues/459
  117. */
  118. it('transforms URL when fragment defined before query (GH issue #459)', async () => {
  119. const { asset } = await adminClient.query<
  120. GetAssetFragmentFirst.Query,
  121. GetAssetFragmentFirst.Variables
  122. >(GET_ASSET_FRAGMENT_FIRST, {
  123. id: firstAssetId,
  124. });
  125. expect(asset?.preview).toBe('test-url/test-assets/alexandru-acea-686569-unsplash__preview.jpg');
  126. });
  127. describe('createAssets', () => {
  128. function isAsset(input: CreateAssets.CreateAssets): input is AssetFragment {
  129. return input.hasOwnProperty('name');
  130. }
  131. it('permitted types by mime type', async () => {
  132. const filesToUpload = [
  133. path.join(__dirname, 'fixtures/assets/pps1.jpg'),
  134. path.join(__dirname, 'fixtures/assets/pps2.jpg'),
  135. ];
  136. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  137. mutation: CREATE_ASSETS,
  138. filePaths: filesToUpload,
  139. mapVariables: filePaths => ({
  140. input: filePaths.map(p => ({ file: null })),
  141. }),
  142. });
  143. expect(createAssets.length).toBe(2);
  144. const results = createAssets.filter(isAsset);
  145. expect(results.map(a => omit(a, ['id'])).sort((a, b) => (a.name < b.name ? -1 : 1))).toEqual([
  146. {
  147. fileSize: 1680,
  148. focalPoint: null,
  149. mimeType: 'image/jpeg',
  150. name: 'pps1.jpg',
  151. preview: 'test-url/test-assets/pps1__preview.jpg',
  152. source: 'test-url/test-assets/pps1.jpg',
  153. tags: [],
  154. type: 'IMAGE',
  155. },
  156. {
  157. fileSize: 1680,
  158. focalPoint: null,
  159. mimeType: 'image/jpeg',
  160. name: 'pps2.jpg',
  161. preview: 'test-url/test-assets/pps2__preview.jpg',
  162. source: 'test-url/test-assets/pps2.jpg',
  163. tags: [],
  164. type: 'IMAGE',
  165. },
  166. ]);
  167. createdAssetId = results[0].id;
  168. });
  169. it('permitted type by file extension', async () => {
  170. const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.pdf')];
  171. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  172. mutation: CREATE_ASSETS,
  173. filePaths: filesToUpload,
  174. mapVariables: filePaths => ({
  175. input: filePaths.map(p => ({ file: null })),
  176. }),
  177. });
  178. expect(createAssets.length).toBe(1);
  179. const results = createAssets.filter(isAsset);
  180. expect(results.map(a => omit(a, ['id']))).toEqual([
  181. {
  182. fileSize: 1680,
  183. focalPoint: null,
  184. mimeType: 'application/pdf',
  185. name: 'dummy.pdf',
  186. preview: 'test-url/test-assets/dummy__preview.pdf.png',
  187. source: 'test-url/test-assets/dummy.pdf',
  188. tags: [],
  189. type: 'BINARY',
  190. },
  191. ]);
  192. });
  193. it('not permitted type', async () => {
  194. const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.txt')];
  195. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  196. mutation: CREATE_ASSETS,
  197. filePaths: filesToUpload,
  198. mapVariables: filePaths => ({
  199. input: filePaths.map(p => ({ file: null })),
  200. }),
  201. });
  202. expect(createAssets.length).toBe(1);
  203. expect(createAssets[0]).toEqual({
  204. message: `The MIME type 'text/plain' is not permitted.`,
  205. mimeType: 'text/plain',
  206. fileName: 'dummy.txt',
  207. });
  208. });
  209. it('create with new tags', async () => {
  210. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  211. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  212. mutation: CREATE_ASSETS,
  213. filePaths: filesToUpload,
  214. mapVariables: filePaths => ({
  215. input: filePaths.map(p => ({ file: null, tags: ['foo', 'bar'] })),
  216. }),
  217. });
  218. const results = createAssets.filter(isAsset);
  219. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  220. {
  221. id: 'T_8',
  222. name: 'pps1.jpg',
  223. tags: [
  224. { id: 'T_1', value: 'foo' },
  225. { id: 'T_2', value: 'bar' },
  226. ],
  227. },
  228. ]);
  229. });
  230. it('create with existing tags', async () => {
  231. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  232. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  233. mutation: CREATE_ASSETS,
  234. filePaths: filesToUpload,
  235. mapVariables: filePaths => ({
  236. input: filePaths.map(p => ({ file: null, tags: ['foo', 'bar'] })),
  237. }),
  238. });
  239. const results = createAssets.filter(isAsset);
  240. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  241. {
  242. id: 'T_9',
  243. name: 'pps1.jpg',
  244. tags: [
  245. { id: 'T_1', value: 'foo' },
  246. { id: 'T_2', value: 'bar' },
  247. ],
  248. },
  249. ]);
  250. });
  251. it('create with new and existing tags', async () => {
  252. const filesToUpload = [path.join(__dirname, 'fixtures/assets/pps1.jpg')];
  253. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  254. mutation: CREATE_ASSETS,
  255. filePaths: filesToUpload,
  256. mapVariables: filePaths => ({
  257. input: filePaths.map(p => ({ file: null, tags: ['quux', 'bar'] })),
  258. }),
  259. });
  260. const results = createAssets.filter(isAsset);
  261. expect(results.map(a => pick(a, ['id', 'name', 'tags']))).toEqual([
  262. {
  263. id: 'T_10',
  264. name: 'pps1.jpg',
  265. tags: [
  266. { id: 'T_3', value: 'quux' },
  267. { id: 'T_2', value: 'bar' },
  268. ],
  269. },
  270. ]);
  271. });
  272. });
  273. describe('filter by tags', () => {
  274. it('and', async () => {
  275. const { assets } = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
  276. GET_ASSET_LIST,
  277. {
  278. options: {
  279. tags: ['foo', 'bar'],
  280. tagsOperator: LogicalOperator.AND,
  281. },
  282. },
  283. );
  284. expect(assets.items.map(i => i.id).sort()).toEqual(['T_8', 'T_9']);
  285. });
  286. it('or', async () => {
  287. const { assets } = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
  288. GET_ASSET_LIST,
  289. {
  290. options: {
  291. tags: ['foo', 'bar'],
  292. tagsOperator: LogicalOperator.OR,
  293. },
  294. },
  295. );
  296. expect(assets.items.map(i => i.id).sort()).toEqual(['T_10', 'T_8', 'T_9']);
  297. });
  298. it('empty array', async () => {
  299. const { assets } = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
  300. GET_ASSET_LIST,
  301. {
  302. options: {
  303. tags: [],
  304. },
  305. },
  306. );
  307. expect(assets.totalItems).toBe(10);
  308. });
  309. });
  310. describe('updateAsset', () => {
  311. it('update name', async () => {
  312. const { updateAsset } = await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(
  313. UPDATE_ASSET,
  314. {
  315. input: {
  316. id: firstAssetId,
  317. name: 'new name',
  318. },
  319. },
  320. );
  321. expect(updateAsset.name).toEqual('new name');
  322. });
  323. it('update focalPoint', async () => {
  324. const { updateAsset } = await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(
  325. UPDATE_ASSET,
  326. {
  327. input: {
  328. id: firstAssetId,
  329. focalPoint: {
  330. x: 0.3,
  331. y: 0.9,
  332. },
  333. },
  334. },
  335. );
  336. expect(updateAsset.focalPoint).toEqual({
  337. x: 0.3,
  338. y: 0.9,
  339. });
  340. });
  341. it('unset focalPoint', async () => {
  342. const { updateAsset } = await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(
  343. UPDATE_ASSET,
  344. {
  345. input: {
  346. id: firstAssetId,
  347. focalPoint: null,
  348. },
  349. },
  350. );
  351. expect(updateAsset.focalPoint).toEqual(null);
  352. });
  353. it('update tags', async () => {
  354. const { updateAsset } = await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(
  355. UPDATE_ASSET,
  356. {
  357. input: {
  358. id: firstAssetId,
  359. tags: ['foo', 'quux'],
  360. },
  361. },
  362. );
  363. expect(updateAsset.tags).toEqual([
  364. { id: 'T_1', value: 'foo' },
  365. { id: 'T_3', value: 'quux' },
  366. ]);
  367. });
  368. it('remove tags', async () => {
  369. const { updateAsset } = await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(
  370. UPDATE_ASSET,
  371. {
  372. input: {
  373. id: firstAssetId,
  374. tags: [],
  375. },
  376. },
  377. );
  378. expect(updateAsset.tags).toEqual([]);
  379. });
  380. });
  381. describe('deleteAsset', () => {
  382. let firstProduct: GetProductWithVariants.Product;
  383. beforeAll(async () => {
  384. const { product } = await adminClient.query<
  385. GetProductWithVariants.Query,
  386. GetProductWithVariants.Variables
  387. >(GET_PRODUCT_WITH_VARIANTS, {
  388. id: 'T_1',
  389. });
  390. firstProduct = product!;
  391. });
  392. it('non-featured asset', async () => {
  393. const { deleteAsset } = await adminClient.query<DeleteAsset.Mutation, DeleteAsset.Variables>(
  394. DELETE_ASSET,
  395. {
  396. id: createdAssetId,
  397. },
  398. );
  399. expect(deleteAsset.result).toBe(DeletionResult.DELETED);
  400. const { asset } = await adminClient.query<GetAsset.Query, GetAsset.Variables>(GET_ASSET, {
  401. id: createdAssetId,
  402. });
  403. expect(asset).toBeNull();
  404. });
  405. it('featured asset not deleted', async () => {
  406. const { deleteAsset } = await adminClient.query<DeleteAsset.Mutation, DeleteAsset.Variables>(
  407. DELETE_ASSET,
  408. {
  409. id: firstProduct.featuredAsset!.id,
  410. },
  411. );
  412. expect(deleteAsset.result).toBe(DeletionResult.NOT_DELETED);
  413. expect(deleteAsset.message).toContain(`The selected Asset is featured by 1 Product`);
  414. const { asset } = await adminClient.query<GetAsset.Query, GetAsset.Variables>(GET_ASSET, {
  415. id: firstAssetId,
  416. });
  417. expect(asset).not.toBeNull();
  418. });
  419. it('featured asset force deleted', async () => {
  420. const { product: p1 } = await adminClient.query<
  421. GetProductWithVariants.Query,
  422. GetProductWithVariants.Variables
  423. >(GET_PRODUCT_WITH_VARIANTS, {
  424. id: firstProduct.id,
  425. });
  426. expect(p1!.assets.length).toEqual(1);
  427. const { deleteAsset } = await adminClient.query<DeleteAsset.Mutation, DeleteAsset.Variables>(
  428. DELETE_ASSET,
  429. {
  430. id: firstProduct.featuredAsset!.id,
  431. force: true,
  432. },
  433. );
  434. expect(deleteAsset.result).toBe(DeletionResult.DELETED);
  435. const { asset } = await adminClient.query<GetAsset.Query, GetAsset.Variables>(GET_ASSET, {
  436. id: firstAssetId,
  437. });
  438. expect(asset).not.toBeNull();
  439. const { product } = await adminClient.query<
  440. GetProductWithVariants.Query,
  441. GetProductWithVariants.Variables
  442. >(GET_PRODUCT_WITH_VARIANTS, {
  443. id: firstProduct.id,
  444. });
  445. expect(product!.featuredAsset).toBeNull();
  446. expect(product!.assets.length).toEqual(0);
  447. });
  448. });
  449. });
  450. export const GET_ASSET = gql`
  451. query GetAsset($id: ID!) {
  452. asset(id: $id) {
  453. ...Asset
  454. width
  455. height
  456. }
  457. }
  458. ${ASSET_FRAGMENT}
  459. `;
  460. export const GET_ASSET_FRAGMENT_FIRST = gql`
  461. fragment AssetFragFirst on Asset {
  462. id
  463. preview
  464. }
  465. query GetAssetFragmentFirst($id: ID!) {
  466. asset(id: $id) {
  467. ...AssetFragFirst
  468. }
  469. }
  470. `;
  471. export const CREATE_ASSETS = gql`
  472. mutation CreateAssets($input: [CreateAssetInput!]!) {
  473. createAssets(input: $input) {
  474. ...Asset
  475. ... on Asset {
  476. focalPoint {
  477. x
  478. y
  479. }
  480. tags {
  481. id
  482. value
  483. }
  484. }
  485. ... on MimeTypeError {
  486. message
  487. fileName
  488. mimeType
  489. }
  490. }
  491. }
  492. ${ASSET_FRAGMENT}
  493. `;