ryosei-f 6 месяцев назад
Родитель
Сommit
52084bb339

+ 275 - 0
apps/app/src/client/services/AdminContentDispositionContainer.spec.ts

@@ -0,0 +1,275 @@
+import {
+  beforeEach, describe, expect, it, vi,
+} from 'vitest';
+
+import * as apiv3Client from '../util/apiv3-client';
+
+import AdminContentDispositionContainer from './AdminContentDispositionContainer';
+
+// Mock apiv3-client
+vi.mock('../util/apiv3-client', () => ({
+  apiv3Get: vi.fn(),
+  apiv3Put: vi.fn(),
+}));
+
+// Mock @growi/core/dist/utils to make isServer return false
+vi.mock('@growi/core/dist/utils', () => ({
+  isServer: vi.fn(() => false),
+}));
+
+describe('AdminContentDispositionContainer', () => {
+  let container: AdminContentDispositionContainer;
+  let mockAppContainer: any;
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockAppContainer = {};
+    container = new AdminContentDispositionContainer(mockAppContainer);
+  });
+
+  describe('retrieveContentDispositionSettings', () => {
+    it('should retrieve and set content disposition settings', async() => {
+      const mockSettings = {
+        'text/html': 'attachment' as const,
+        'image/svg+xml': 'attachment' as const,
+        'image/png': 'inline' as const,
+        'application/pdf': 'inline' as const,
+      };
+
+      vi.mocked(apiv3Client.apiv3Get).mockResolvedValue({
+        data: {
+          contentDispositionSettings: mockSettings,
+        },
+      } as any);
+
+      await container.retrieveContentDispositionSettings();
+
+      expect(apiv3Client.apiv3Get).toHaveBeenCalledWith('/content-disposition-settings/');
+      expect(container.state.contentDispositionSettings).toEqual(mockSettings);
+    });
+  });
+
+  describe('setInline', () => {
+    it('should set a MIME type to inline', async() => {
+      const mimeType = 'application/json';
+
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValue({
+        data: {
+          mimeType,
+          disposition: 'inline',
+        },
+      } as any);
+
+      await container.setInline(mimeType);
+
+      expect(apiv3Client.apiv3Put).toHaveBeenCalledWith(
+        `/content-disposition-settings/${encodeURIComponent(mimeType)}`,
+        { disposition: 'inline' },
+      );
+      expect(container.state.contentDispositionSettings[mimeType]).toBe('inline');
+    });
+
+    it('should handle MIME types with special characters', async() => {
+      const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValue({
+        data: {
+          mimeType,
+          disposition: 'inline',
+        },
+      } as any);
+
+      await container.setInline(mimeType);
+
+      expect(apiv3Client.apiv3Put).toHaveBeenCalledWith(
+        `/content-disposition-settings/${encodeURIComponent(mimeType)}`,
+        { disposition: 'inline' },
+      );
+    });
+  });
+
+  describe('setAttachment', () => {
+    it('should set a MIME type to attachment', async() => {
+      const mimeType = 'text/html';
+
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValue({
+        data: {
+          mimeType,
+          disposition: 'attachment',
+        },
+      } as any);
+
+      await container.setAttachment(mimeType);
+
+      expect(apiv3Client.apiv3Put).toHaveBeenCalledWith(
+        `/content-disposition-settings/${encodeURIComponent(mimeType)}`,
+        { disposition: 'attachment' },
+      );
+      expect(container.state.contentDispositionSettings[mimeType]).toBe('attachment');
+    });
+  });
+
+  describe('getDispositionForMimeType', () => {
+    it('should return the disposition for a specific MIME type', async() => {
+      await container.setState({
+        contentDispositionSettings: {
+          'text/html': 'attachment',
+          'image/png': 'inline',
+        },
+      });
+
+      expect(container.getDispositionForMimeType('text/html')).toBe('attachment');
+      expect(container.getDispositionForMimeType('image/png')).toBe('inline');
+    });
+
+    it('should return undefined for unconfigured MIME types', async() => {
+      await container.setState({
+        contentDispositionSettings: {},
+      });
+
+      expect(container.getDispositionForMimeType('text/html')).toBeUndefined();
+    });
+  });
+
+  describe('getInlineMimeTypes', () => {
+    it('should return all MIME types set to inline', async() => {
+      await container.setState({
+        contentDispositionSettings: {
+          'text/html': 'attachment',
+          'image/png': 'inline',
+          'image/jpeg': 'inline',
+          'application/pdf': 'inline',
+          'image/svg+xml': 'attachment',
+        },
+      });
+
+      const inlineTypes = container.getInlineMimeTypes();
+
+      expect(inlineTypes).toHaveLength(3);
+      expect(inlineTypes).toContain('image/png');
+      expect(inlineTypes).toContain('image/jpeg');
+      expect(inlineTypes).toContain('application/pdf');
+      expect(inlineTypes).not.toContain('text/html');
+      expect(inlineTypes).not.toContain('image/svg+xml');
+    });
+
+    it('should return empty array when no inline types exist', async() => {
+      await container.setState({
+        contentDispositionSettings: {
+          'text/html': 'attachment',
+          'image/svg+xml': 'attachment',
+        },
+      });
+
+      expect(container.getInlineMimeTypes()).toEqual([]);
+    });
+  });
+
+  describe('getAllConfiguredMimeTypes', () => {
+    it('should return all configured MIME types', async() => {
+      await container.setState({
+        contentDispositionSettings: {
+          'text/html': 'attachment',
+          'image/png': 'inline',
+          'image/svg+xml': 'attachment',
+          'application/pdf': 'inline',
+        },
+      });
+
+      const allTypes = container.getAllConfiguredMimeTypes();
+
+      expect(allTypes).toHaveLength(4);
+      expect(allTypes).toContain('text/html');
+      expect(allTypes).toContain('image/png');
+      expect(allTypes).toContain('image/svg+xml');
+      expect(allTypes).toContain('application/pdf');
+    });
+
+    it('should return empty array when no types are configured', async() => {
+      await container.setState({
+        contentDispositionSettings: {},
+      });
+
+      expect(container.getAllConfiguredMimeTypes()).toEqual([]);
+    });
+  });
+
+  describe('integration scenarios', () => {
+    it('should handle a complete workflow of setting MIME types', async() => {
+      // Initial state - retrieve settings
+      vi.mocked(apiv3Client.apiv3Get).mockResolvedValue({
+        data: {
+          contentDispositionSettings: {
+            'text/html': 'attachment',
+            'image/png': 'inline',
+          },
+        },
+      } as any);
+
+      await container.retrieveContentDispositionSettings();
+
+      expect(container.getInlineMimeTypes()).toEqual(['image/png']);
+
+      // Set a new inline MIME type
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValue({
+        data: {
+          mimeType: 'application/pdf',
+          disposition: 'inline',
+        },
+      } as any);
+
+      await container.setInline('application/pdf');
+
+      expect(container.getInlineMimeTypes()).toContain('application/pdf');
+
+      // Change inline to attachment
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValue({
+        data: {
+          mimeType: 'image/png',
+          disposition: 'attachment',
+        },
+      } as any);
+
+      await container.setAttachment('image/png');
+
+      expect(container.getDispositionForMimeType('image/png')).toBe('attachment');
+    });
+
+    it('should correctly manage state when updating multiple MIME types', async() => {
+      await container.setState({
+        contentDispositionSettings: {
+          'text/html': 'attachment',
+        },
+      });
+
+      // Set first MIME type
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValueOnce({
+        data: {
+          mimeType: 'image/png',
+          disposition: 'inline',
+        },
+      } as any);
+
+      await container.setInline('image/png');
+
+      // Set second MIME type
+      vi.mocked(apiv3Client.apiv3Put).mockResolvedValueOnce({
+        data: {
+          mimeType: 'application/pdf',
+          disposition: 'inline',
+        },
+      } as any);
+
+      await container.setInline('application/pdf');
+
+      // Verify both are in state
+      expect(container.state.contentDispositionSettings).toEqual({
+        'text/html': 'attachment',
+        'image/png': 'inline',
+        'application/pdf': 'inline',
+      });
+
+      expect(container.getInlineMimeTypes()).toHaveLength(2);
+    });
+  });
+});

+ 51 - 60
apps/app/src/client/services/AdminContentDispositionContainer.ts

@@ -3,9 +3,24 @@ import { Container } from 'unstated';
 
 import { apiv3Get, apiv3Put } from '../util/apiv3-client';
 
-export default class AdminContentDispositionContainer extends Container<AdminContentDispositionContainer> {
+import type AdminAppContainer from './AdminAppContainer';
 
-  constructor(appContainer) {
+type Disposition = 'inline' | 'attachment';
+type ContentDispositionSettings = Record<string, Disposition>;
+
+interface State {
+  contentDispositionSettings: ContentDispositionSettings;
+}
+interface UpdateResponse {
+  mimeType: string;
+  disposition: Disposition;
+}
+
+export default class AdminContentDispositionContainer extends Container<State> {
+
+  appContainer: AdminAppContainer;
+
+  constructor(appContainer: AdminAppContainer) {
     super();
 
     if (isServer()) {
@@ -15,84 +30,60 @@ export default class AdminContentDispositionContainer extends Container<AdminCon
     this.appContainer = appContainer;
 
     this.state = {
-      currentMode: 'strict',
-      contentDispositionSettings: {
-        'text/html': 'attachment',
-        'image/svg+xml': 'attachment',
-        'application/pdf': 'attachment',
-        'application/json': 'attachment',
-        'text/csv': 'attachment',
-        'font/woff2': 'attachment',
-        'font/woff': 'attachment',
-        'font/ttf': 'attachment',
-        'font/otf': 'attachment',
-      },
+      contentDispositionSettings: {},
     };
-
-
   }
 
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
+  static getClassName(): string {
     return 'AdminContentDispositionContainer';
   }
 
-  /**
-   * retrieve markdown data
-   */
-  async retrieveContentDispositionSettings() {
-    const response = await apiv3Get('/content-disposition-settings/');
-    const { currentMode, contentDispositionSettings } = response.data;
+  async retrieveContentDispositionSettings(): Promise<void> {
+    const response = await apiv3Get<State>('/content-disposition-settings/');
+    const { contentDispositionSettings } = response.data;
 
     this.setState({
-      currentMode,
-      contentDispositionSettings
+      contentDispositionSettings,
     });
   }
 
-  async setStrictMode(): Promise<Response>  {
-    const response = await apiv3Put('/content-disposition-settings/strict');
-
-    const { currentMode, contentDispositionSettings } = response.data;
+  async updateMimeTypeDisposition(mimeType: string, disposition: Disposition): Promise<void> {
+    const response = await apiv3Put<UpdateResponse>(`/content-disposition-settings/${encodeURIComponent(mimeType)}`, {
+      disposition,
+    });
 
     this.setState({
-      currentMode: currentMode,
-      contentDispositionSettings: contentDispositionSettings
-    })
-
-    return response;
+      contentDispositionSettings: {
+        ...this.state.contentDispositionSettings,
+        [response.data.mimeType]: response.data.disposition,
+      },
+    });
   }
 
-  async setLaxMode(): Promise<Response>  {
-    const response = await apiv3Put('/content-disposition-settings/lax');
-
-    const { currentMode, contentDispositionSettings } = response.data;
-
-    this.setState({
-      currentMode: currentMode,
-      contentDispositionSettings: contentDispositionSettings
-    })
-
-    return response;
+  async setInline(mimeType: string): Promise<void> {
+    await this.updateMimeTypeDisposition(mimeType, 'inline');
   }
 
-  async setHighRiskMimeType(mimeType: 'text/html' | 'image/svg+xml', disposition: 'inline' | 'attachment'): Promise<Response> {
-    // double check if valid admin
-
-    const body = {
-      [mimeType]: disposition,
-    };
+  async setAttachment(mimeType: string): Promise<void> {
+    await this.updateMimeTypeDisposition(mimeType, 'attachment');
+  }
 
-    const response = await apiv3Put('/content-disposition-settings/admin-override', body);
+  getDispositionForMimeType(mimeType: string): Disposition | undefined {
+    return this.state.contentDispositionSettings[mimeType];
+  }
 
-    const { contentDispositionSettings } = response.data;
+  private getMimeTypesByDisposition(disposition: Disposition): string[] {
+    return Object.entries(this.state.contentDispositionSettings)
+      .filter(([, d]) => d === disposition)
+      .map(([mimeType]) => mimeType);
+  }
 
-    this.setState({
-      contentDispositionSettings: contentDispositionSettings
-    })
+  getInlineMimeTypes(): string[] {
+    return this.getMimeTypesByDisposition('inline');
+  }
 
-    return respone;
+  getAllConfiguredMimeTypes(): string[] {
+    return Object.keys(this.state.contentDispositionSettings);
   }
+
 }