Просмотр исходного кода

WIP: devide module and add test

Yuki Takei 11 месяцев назад
Родитель
Сommit
10721d8112

+ 7 - 24
apps/app/src/server/routes/attachment/api.js

@@ -4,6 +4,8 @@ import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { Attachment } from '../../models/attachment';
+
+import { validateImageContentType } from './image-content-type-validator';
 /* eslint-disable no-use-before-define */
 
 
@@ -246,30 +248,11 @@ export const routesFactory = (crowi) => {
 
     const file = req.file;
 
-    // check type
-    // Define explicitly allowed image types
-    // Keep supporting wide range of formats as ImageCropModal can handle them:
-    // - When cropping: converts to PNG
-    // - When not cropping: maintains original format
-    const acceptableFileTypes = [
-      'image/png', // Universal web format
-      'image/jpeg', // Universal web format
-      'image/jpg', // Universal web format
-      'image/gif', // Universal web format
-      'image/webp', // Modern efficient format
-      'image/avif', // Next-gen format
-      'image/heic', // iOS format
-      'image/heif', // iOS format
-      'image/tiff', // High quality format
-      'image/svg+xml', // Vector format
-    ];
-
-    // Security: Extract the actual content type from potentially multiple values
-    const contentType = file.mimetype.split(',').map(type => type.trim()).pop();
-
-    if (!acceptableFileTypes.includes(contentType)) {
-      const supportedFormats = 'PNG, JPEG, GIF, WebP, AVIF, HEIC/HEIF, TIFF, SVG';
-      return res.json(ApiResponse.error(`Invalid file type. Supported formats: ${supportedFormats}`));
+    // Validate file type
+    const { isValid, error } = validateImageContentType(file.mimetype);
+
+    if (!isValid) {
+      return res.json(ApiResponse.error(error));
     }
 
     let attachment;

+ 79 - 0
apps/app/src/server/routes/attachment/image-content-type-validator.spec.ts

@@ -0,0 +1,79 @@
+import { describe, test, expect } from 'vitest';
+
+import { validateImageContentType } from './image-content-type-validator';
+
+describe('imageContentTypeValidator', () => {
+  describe('validateImageContentType', () => {
+    test('should accept valid single MIME type', () => {
+      const result = validateImageContentType('image/png');
+      expect(result.isValid).toBe(true);
+      expect(result.contentType).toBe('image/png');
+      expect(result.error).toBeUndefined();
+    });
+
+    test('should accept valid MIME type when multiple types are provided (last type is valid)', () => {
+      const result = validateImageContentType('text/html, image/jpeg');
+      expect(result.isValid).toBe(true);
+      expect(result.contentType).toBe('image/jpeg');
+      expect(result.error).toBeUndefined();
+    });
+
+    test('should reject when last MIME type is invalid', () => {
+      const result = validateImageContentType('image/png, text/html');
+      expect(result.isValid).toBe(false);
+      expect(result.contentType).toBe('text/html');
+      expect(result.error).toContain('Invalid file type');
+    });
+
+    test('should handle empty string', () => {
+      const result = validateImageContentType('');
+      expect(result.isValid).toBe(false);
+      expect(result.contentType).toBe('');
+      expect(result.error).toContain('Invalid file type');
+    });
+
+    test('should handle whitespace in MIME types', () => {
+      const result = validateImageContentType('image/png , image/jpeg');
+      expect(result.isValid).toBe(true);
+      expect(result.contentType).toBe('image/jpeg');
+      expect(result.error).toBeUndefined();
+    });
+
+    test('should reject non-image MIME types', () => {
+      const result = validateImageContentType('application/json');
+      expect(result.isValid).toBe(false);
+      expect(result.contentType).toBe('application/json');
+      expect(result.error).toContain('Invalid file type');
+    });
+
+    test('should reject non-string input', () => {
+      const result = validateImageContentType(undefined as unknown as string);
+      expect(result.isValid).toBe(false);
+      expect(result.contentType).toBeNull();
+      expect(result.error).toBe('Invalid MIME type format');
+    });
+
+    test('should accept all supported image types', () => {
+      const supportedTypes = [
+        'image/png',
+        'image/jpeg',
+        'image/jpg',
+        'image/gif',
+        'image/webp',
+        'image/avif',
+        'image/heic',
+        'image/heif',
+        'image/tiff',
+        'image/svg+xml',
+      ];
+
+      // Use for...of instead of forEach to avoid extra argument
+      for (const mimeType of supportedTypes) {
+        const result = validateImageContentType(mimeType);
+        expect(result.isValid).toBe(true, `${mimeType} should be valid`);
+        expect(result.contentType).toBe(mimeType);
+        expect(result.error).toBeUndefined();
+      }
+    });
+  });
+});

+ 61 - 0
apps/app/src/server/routes/attachment/image-content-type-validator.ts

@@ -0,0 +1,61 @@
+/**
+ * Define supported image MIME types
+ */
+export const SUPPORTED_IMAGE_MIME_TYPES = [
+  'image/png', // Universal web format
+  'image/jpeg', // Universal web format
+  'image/jpg', // Universal web format
+  'image/gif', // Universal web format
+  'image/webp', // Modern efficient format
+  'image/avif', // Next-gen format
+  'image/heic', // iOS format
+  'image/heif', // iOS format
+  'image/tiff', // High quality format
+  'image/svg+xml', // Vector format
+] as const;
+
+// Create a type for supported MIME types
+export type SupportedImageMimeType = typeof SUPPORTED_IMAGE_MIME_TYPES[number];
+
+export interface ImageContentTypeValidatorResult {
+  isValid: boolean;
+  contentType: string | null;
+  error?: string;
+}
+
+/**
+ * Validate and extract content type from MIME type string
+ * @param mimeType MIME type string, possibly containing multiple values
+ * @returns Validation result containing isValid flag and extracted content type
+ */
+export const validateImageContentType = (mimeType: string): ImageContentTypeValidatorResult => {
+  if (typeof mimeType !== 'string') {
+    return {
+      isValid: false,
+      contentType: null,
+      error: 'Invalid MIME type format',
+    };
+  }
+
+  // Extract the last content type from comma-separated values
+  const contentType = mimeType.split(',')
+    .map(type => type.trim())
+    .pop() ?? '';
+
+  // Check if the content type is in supported list
+  const isValid = SUPPORTED_IMAGE_MIME_TYPES.includes(contentType as SupportedImageMimeType);
+
+  if (!isValid) {
+    const supportedFormats = 'PNG, JPEG, GIF, WebP, AVIF, HEIC/HEIF, TIFF, SVG';
+    return {
+      isValid: false,
+      contentType,
+      error: `Invalid file type. Supported formats: ${supportedFormats}`,
+    };
+  }
+
+  return {
+    isValid: true,
+    contentType,
+  };
+};