asset-server-plugin.e2e-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { mergeConfig } from '@vendure/core';
  3. import { AssetFragment } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
  4. import { createTestEnvironment } from '@vendure/testing';
  5. import fs from 'fs-extra';
  6. import gql from 'graphql-tag';
  7. import fetch from 'node-fetch';
  8. import path from 'path';
  9. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  10. import { initialData } from '../../../e2e-common/e2e-initial-data';
  11. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  12. import { AssetServerPlugin } from '../src/plugin';
  13. import {
  14. CreateAssetsMutation,
  15. DeleteAssetMutation,
  16. DeleteAssetMutationVariables,
  17. DeletionResult,
  18. } from './graphql/generated-e2e-asset-server-plugin-types';
  19. const TEST_ASSET_DIR = 'test-assets';
  20. const IMAGE_BASENAME = 'derick-david-409858-unsplash';
  21. describe('AssetServerPlugin', () => {
  22. let asset: AssetFragment;
  23. const sourceFilePath = path.join(__dirname, TEST_ASSET_DIR, `source/b6/${IMAGE_BASENAME}.jpg`);
  24. const previewFilePath = path.join(__dirname, TEST_ASSET_DIR, `preview/71/${IMAGE_BASENAME}__preview.jpg`);
  25. const { server, adminClient, shopClient } = createTestEnvironment(
  26. mergeConfig(testConfig(), {
  27. // logger: new DefaultLogger({ level: LogLevel.Info }),
  28. plugins: [
  29. AssetServerPlugin.init({
  30. assetUploadDir: path.join(__dirname, TEST_ASSET_DIR),
  31. route: 'assets',
  32. }),
  33. ],
  34. }),
  35. );
  36. beforeAll(async () => {
  37. await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'source'));
  38. await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'preview'));
  39. await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'cache'));
  40. await server.init({
  41. initialData,
  42. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
  43. customerCount: 1,
  44. });
  45. await adminClient.asSuperAdmin();
  46. }, TEST_SETUP_TIMEOUT_MS);
  47. afterAll(async () => {
  48. await server.destroy();
  49. });
  50. it('names the Asset correctly', async () => {
  51. const filesToUpload = [path.join(__dirname, `fixtures/assets/${IMAGE_BASENAME}.jpg`)];
  52. const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
  53. mutation: CREATE_ASSETS,
  54. filePaths: filesToUpload,
  55. mapVariables: filePaths => ({
  56. input: filePaths.map(p => ({ file: null })),
  57. }),
  58. });
  59. asset = createAssets[0] as AssetFragment;
  60. expect(asset.name).toBe(`${IMAGE_BASENAME}.jpg`);
  61. });
  62. it('creates the expected asset files', async () => {
  63. expect(fs.existsSync(sourceFilePath)).toBe(true);
  64. expect(fs.existsSync(previewFilePath)).toBe(true);
  65. });
  66. it('serves the source file', async () => {
  67. const res = await fetch(`${asset.source}`);
  68. const responseBuffer = await res.buffer();
  69. const sourceFile = await fs.readFile(sourceFilePath);
  70. expect(Buffer.compare(responseBuffer, sourceFile)).toBe(0);
  71. });
  72. it('serves the untransformed preview file', async () => {
  73. const res = await fetch(`${asset.preview}`);
  74. const responseBuffer = await res.buffer();
  75. const previewFile = await fs.readFile(previewFilePath);
  76. expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
  77. });
  78. it('can handle non-latin filenames', async () => {
  79. const FILE_NAME_ZH = '白飯';
  80. const filesToUpload = [path.join(__dirname, `fixtures/assets/${FILE_NAME_ZH}.jpg`)];
  81. const { createAssets }: { createAssets: AssetFragment[] } = await adminClient.fileUploadMutation({
  82. mutation: CREATE_ASSETS,
  83. filePaths: filesToUpload,
  84. mapVariables: filePaths => ({
  85. input: filePaths.map(p => ({ file: null })),
  86. }),
  87. });
  88. expect(createAssets[0].name).toBe(`${FILE_NAME_ZH}.jpg`);
  89. expect(createAssets[0].source).toContain(`${FILE_NAME_ZH}.jpg`);
  90. const previewUrl = encodeURI(`${createAssets[0].preview}`);
  91. const res = await fetch(previewUrl);
  92. expect(res.status).toBe(200);
  93. const previewFilePathZH = path.join(
  94. __dirname,
  95. TEST_ASSET_DIR,
  96. `preview/3f/${FILE_NAME_ZH}__preview.jpg`,
  97. );
  98. const responseBuffer = await res.buffer();
  99. const previewFile = await fs.readFile(previewFilePathZH);
  100. expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
  101. });
  102. describe('caching', () => {
  103. const cacheDir = path.join(__dirname, TEST_ASSET_DIR, 'cache');
  104. const cacheFileDir = path.join(__dirname, TEST_ASSET_DIR, 'cache', 'preview', '71');
  105. it('cache initially empty', async () => {
  106. const files = await fs.readdir(cacheDir);
  107. expect(files.length).toBe(0);
  108. });
  109. it('creates cached image on first request', async () => {
  110. const res = await fetch(`${asset.preview}?preset=thumb`);
  111. const responseBuffer = await res.buffer();
  112. expect(fs.existsSync(cacheFileDir)).toBe(true);
  113. const files = await fs.readdir(cacheFileDir);
  114. expect(files.length).toBe(1);
  115. expect(files[0]).toContain(`${IMAGE_BASENAME}__preview`);
  116. const cachedFile = await fs.readFile(path.join(cacheFileDir, files[0]));
  117. // was the file returned the exact same file as is stored in the cache dir?
  118. expect(Buffer.compare(responseBuffer, cachedFile)).toBe(0);
  119. });
  120. it('does not create a new cached image on a second request', async () => {
  121. const res = await fetch(`${asset.preview}?preset=thumb`);
  122. const files = await fs.readdir(cacheFileDir);
  123. expect(files.length).toBe(1);
  124. });
  125. it('does not create a new cached image for an untransformed image', async () => {
  126. const res = await fetch(`${asset.preview}`);
  127. const files = await fs.readdir(cacheFileDir);
  128. expect(files.length).toBe(1);
  129. });
  130. it('does not create a new cached image for an invalid preset', async () => {
  131. const res = await fetch(`${asset.preview}?preset=invalid`);
  132. const files = await fs.readdir(cacheFileDir);
  133. expect(files.length).toBe(1);
  134. const previewFile = await fs.readFile(previewFilePath);
  135. const responseBuffer = await res.buffer();
  136. expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
  137. });
  138. it('does not create a new cached image if cache=false', async () => {
  139. const res = await fetch(`${asset.preview}?preset=tiny&cache=false`);
  140. const files = await fs.readdir(cacheFileDir);
  141. expect(files.length).toBe(1);
  142. });
  143. it('creates a new cached image if cache=true', async () => {
  144. const res = await fetch(`${asset.preview}?preset=tiny&cache=true`);
  145. const files = await fs.readdir(cacheFileDir);
  146. expect(files.length).toBe(2);
  147. });
  148. });
  149. describe('unexpected input', () => {
  150. it('does not error on non-integer width', async () => {
  151. return fetch(`${asset.preview}?w=10.5`);
  152. });
  153. it('does not error on non-integer height', async () => {
  154. return fetch(`${asset.preview}?h=10.5`);
  155. });
  156. });
  157. describe('deletion', () => {
  158. it('deleting Asset deletes binary file', async () => {
  159. const { deleteAsset } = await adminClient.query<
  160. DeleteAssetMutation,
  161. DeleteAssetMutationVariables
  162. >(DELETE_ASSET, {
  163. input: {
  164. assetId: asset.id,
  165. force: true,
  166. },
  167. });
  168. expect(deleteAsset.result).toBe(DeletionResult.DELETED);
  169. expect(fs.existsSync(sourceFilePath)).toBe(false);
  170. expect(fs.existsSync(previewFilePath)).toBe(false);
  171. });
  172. });
  173. describe('MIME type detection', () => {
  174. let testImages: AssetFragment[] = [];
  175. async function testMimeTypeOfAssetWithExt(ext: string, expectedMimeType: string) {
  176. const testImage = testImages.find(i => i.source.endsWith(ext))!;
  177. const result = await fetch(testImage.source);
  178. const contentType = result.headers.get('Content-Type');
  179. expect(contentType).toBe(expectedMimeType);
  180. }
  181. beforeAll(async () => {
  182. const formats = ['gif', 'jpg', 'png', 'svg', 'tiff', 'webp'];
  183. const filesToUpload = formats.map(ext => path.join(__dirname, `fixtures/assets/test.${ext}`));
  184. const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
  185. mutation: CREATE_ASSETS,
  186. filePaths: filesToUpload,
  187. mapVariables: filePaths => ({
  188. input: filePaths.map(p => ({ file: null })),
  189. }),
  190. });
  191. testImages = createAssets as AssetFragment[];
  192. });
  193. it('gif', async () => {
  194. await testMimeTypeOfAssetWithExt('gif', 'image/gif');
  195. });
  196. it('jpg', async () => {
  197. await testMimeTypeOfAssetWithExt('jpg', 'image/jpeg');
  198. });
  199. it('png', async () => {
  200. await testMimeTypeOfAssetWithExt('png', 'image/png');
  201. });
  202. it('svg', async () => {
  203. await testMimeTypeOfAssetWithExt('svg', 'image/svg+xml');
  204. });
  205. it('tiff', async () => {
  206. await testMimeTypeOfAssetWithExt('tiff', 'image/tiff');
  207. });
  208. it('webp', async () => {
  209. await testMimeTypeOfAssetWithExt('webp', 'image/webp');
  210. });
  211. });
  212. // https://github.com/vendure-ecommerce/vendure/issues/1563
  213. it('falls back to binary preview if image file cannot be processed', async () => {
  214. const filesToUpload = [path.join(__dirname, 'fixtures/assets/bad-image.jpg')];
  215. const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
  216. mutation: CREATE_ASSETS,
  217. filePaths: filesToUpload,
  218. mapVariables: filePaths => ({
  219. input: filePaths.map(p => ({ file: null })),
  220. }),
  221. });
  222. expect(createAssets.length).toBe(1);
  223. expect(createAssets[0].name).toBe('bad-image.jpg');
  224. });
  225. });
  226. export const CREATE_ASSETS = gql`
  227. mutation CreateAssets($input: [CreateAssetInput!]!) {
  228. createAssets(input: $input) {
  229. ... on Asset {
  230. id
  231. name
  232. source
  233. preview
  234. focalPoint {
  235. x
  236. y
  237. }
  238. }
  239. }
  240. }
  241. `;
  242. export const DELETE_ASSET = gql`
  243. mutation DeleteAsset($input: DeleteAssetInput!) {
  244. deleteAsset(input: $input) {
  245. result
  246. }
  247. }
  248. `;