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

refactor external-user-group-sync.integ.ts

Yuki Takei 2 месяцев назад
Родитель
Сommit
7d205f5b79

+ 57 - 23
apps/app/src/features/external-user-group/server/service/external-user-group-sync.integ.ts

@@ -1,4 +1,4 @@
-import type { IUserHasId } from '@growi/core';
+import type { IPage, IUserHasId } from '@growi/core';
 import mongoose, { Types } from 'mongoose';
 import {
   afterEach,
@@ -11,7 +11,11 @@ import {
 } from 'vitest';
 import { mock } from 'vitest-mock-extended';
 
+import { getInstance } from '^/test-with-vite/setup/crowi';
+
+import type Crowi from '~/server/crowi';
 import ExternalAccount from '~/server/models/external-account';
+import type { PageModel } from '~/server/models/page';
 import { configManager } from '~/server/service/config-manager';
 import instanciateExternalAccountService from '~/server/service/external-account';
 import type PassportService from '~/server/service/passport';
@@ -215,9 +219,17 @@ const checkSync = async (autoGenerateUserOnGroupSync = true) => {
       'previouslySyncedGroupUser',
     );
 
-    // Note: User page creation is handled by crowi.events.user.onActivated
-    // which is mocked in this test. The actual page creation is tested
-    // in integration tests with real Crowi instance.
+    const userPages = await mongoose.model<IPage>('Page').find({
+      path: {
+        $in: [
+          '/user/childGroupUser',
+          '/user/parentGroupUser',
+          '/user/grandParentGroupUser',
+          '/user/previouslySyncedGroupUser',
+        ],
+      },
+    });
+    expect(userPages.length).toBe(4);
   } else {
     expect(grandParentGroupRelations.length).toBe(0);
     expect(parentGroupRelations.length).toBe(0);
@@ -228,6 +240,10 @@ const checkSync = async (autoGenerateUserOnGroupSync = true) => {
 
 describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
   let testService: TestExternalUserGroupSyncService;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  let Page: PageModel;
+  let rootPageId: Types.ObjectId;
+  let userPageId: Types.ObjectId;
 
   beforeAll(async () => {
     // Initialize configManager
@@ -235,30 +251,14 @@ describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
     configManager.setS2sMessagingService(s2sMessagingServiceMock);
     await configManager.loadConfigs();
 
-    // Create mock Crowi with events for User and Page model initialization
-    const crowiMockForModels = {
-      events: {
-        user: {
-          on: vi.fn(),
-          emit: vi.fn(),
-          onActivated: vi.fn(),
-        },
-        page: {
-          on: vi.fn(),
-          emit: vi.fn(),
-          onCreate: vi.fn(),
-          onUpdate: vi.fn(),
-          onCreateMany: vi.fn(),
-        },
-      },
-    };
+    const crowi: Crowi = await getInstance();
 
     // Initialize models with crowi mock
     const pageModule = await import('~/server/models/page');
-    pageModule.default(crowiMockForModels);
+    Page = pageModule.default(crowi);
 
     const userModule = await import('~/server/models/user/index');
-    userModule.default(crowiMockForModels);
+    userModule.default(crowi);
 
     // Initialize services with mocked PassportService
     await configManager.updateConfig('app:isV5Compatible', true);
@@ -269,6 +269,40 @@ describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
       isSameEmailTreatedAsIdenticalUser: vi.fn().mockReturnValue(false),
     });
     instanciateExternalAccountService(passportServiceMock);
+
+    // Create root page and /user page for UserEvent.onActivated to work
+    rootPageId = new Types.ObjectId();
+    userPageId = new Types.ObjectId();
+
+    // Check if root page already exists
+    const existingRootPage = await Page.findOne({ path: '/' });
+    if (existingRootPage == null) {
+      await Page.insertMany([
+        {
+          _id: rootPageId,
+          path: '/',
+          grant: Page.GRANT_PUBLIC,
+        },
+      ]);
+    } else {
+      rootPageId = existingRootPage._id;
+    }
+
+    // Check if /user page already exists
+    const existingUserPage = await Page.findOne({ path: '/user' });
+    if (existingUserPage == null) {
+      await Page.insertMany([
+        {
+          _id: userPageId,
+          path: '/user',
+          grant: Page.GRANT_PUBLIC,
+          parent: rootPageId,
+          isEmpty: true,
+        },
+      ]);
+    } else {
+      userPageId = existingUserPage._id;
+    }
   });
 
   beforeEach(async () => {

+ 57 - 0
apps/app/src/server/crowi/crowi-setup.integ.ts

@@ -0,0 +1,57 @@
+/**
+ * Integration test to verify Crowi setup works correctly in Vitest environment.
+ * This ensures the test-with-vite/setup/crowi.ts utility functions properly.
+ */
+import { beforeAll, describe, expect, it } from 'vitest';
+
+import {
+  getInstance,
+  resetInstance,
+} from '../../../test-with-vite/setup/crowi';
+import type Crowi from './index';
+
+describe('Crowi Setup for Integration Tests', () => {
+  let crowi: Crowi;
+
+  beforeAll(async () => {
+    resetInstance();
+    crowi = await getInstance();
+  });
+
+  it('should create a Crowi instance', () => {
+    expect(crowi).toBeDefined();
+    expect(crowi.version).toBeDefined();
+  });
+
+  it('should have events initialized', () => {
+    expect(crowi.events).toBeDefined();
+    expect(crowi.events.user).toBeDefined();
+    expect(crowi.events.page).toBeDefined();
+  });
+
+  it('should have configManager initialized', () => {
+    expect(crowi.configManager).toBeDefined();
+  });
+
+  it('should have pageService initialized', () => {
+    expect(crowi.pageService).toBeDefined();
+  });
+
+  it('should have models initialized', () => {
+    expect(crowi.models).toBeDefined();
+  });
+
+  it('should return singleton instance on subsequent calls', async () => {
+    const crowi2 = await getInstance();
+    expect(crowi2).toBe(crowi);
+  });
+
+  // Skip: Creating a new instance causes mongoose discriminator conflicts
+  // because models are registered globally. In practice, tests should use
+  // the singleton instance for isolation.
+  it.skip('should create new instance when isNewInstance is true', async () => {
+    const newCrowi = await getInstance(true);
+    expect(newCrowi).not.toBe(crowi);
+    expect(newCrowi.version).toBeDefined();
+  });
+});

+ 66 - 0
apps/app/test-with-vite/setup/crowi.ts

@@ -0,0 +1,66 @@
+import { Server } from 'node:http';
+
+import Crowi from '~/server/crowi';
+import { setupModelsDependentOnCrowi } from '~/server/crowi/setup-models';
+
+let _instance: Crowi | null = null;
+
+/**
+ * Initialize a Crowi instance with minimal required services for integration testing.
+ * This is the Vitest equivalent of test/integration/setup-crowi.ts
+ */
+const initCrowi = async (crowi: Crowi): Promise<void> => {
+  // Setup models that depend on Crowi instance
+  crowi.models = await setupModelsDependentOnCrowi(crowi);
+
+  // Setup config manager
+  await crowi.setupConfigManager();
+
+  // Setup Socket.IO service with dummy server
+  await crowi.setupSocketIoService();
+  await crowi.socketIoService.attachServer(new Server());
+
+  // Setup application
+  await crowi.setUpApp();
+
+  // Setup services required for most integration tests
+  await Promise.all([
+    crowi.setupPassport(),
+    crowi.setupAttachmentService(),
+    crowi.setUpAcl(),
+    crowi.setupPageService(),
+    crowi.setupInAppNotificationService(),
+    crowi.setupActivityService(),
+    crowi.setupUserGroupService(),
+  ]);
+};
+
+/**
+ * Get a Crowi instance for integration testing.
+ * By default, returns a singleton instance. Pass true to create a new instance.
+ *
+ * @param isNewInstance - If true, creates a new Crowi instance instead of returning singleton
+ * @returns Promise resolving to a Crowi instance
+ */
+export async function getInstance(isNewInstance?: boolean): Promise<Crowi> {
+  if (isNewInstance) {
+    const crowi = new Crowi();
+    await initCrowi(crowi);
+    return crowi;
+  }
+
+  // Initialize singleton instance
+  if (_instance == null) {
+    _instance = new Crowi();
+    await initCrowi(_instance);
+  }
+  return _instance;
+}
+
+/**
+ * Reset the singleton instance.
+ * Useful for test isolation when needed.
+ */
+export function resetInstance(): void {
+  _instance = null;
+}

+ 19 - 0
apps/app/vitest.workspace.mts

@@ -23,11 +23,30 @@ export default defineWorkspace([
 
   // integration test
   mergeConfig(configShared, {
+    resolve: {
+      // Prefer require (CJS) for server-side packages
+      conditions: ['require', 'node', 'default'],
+    },
     test: {
       name: 'app-integration',
       environment: 'node',
       include: ['**/*.integ.ts'],
       setupFiles: ['./test-with-vite/setup/mongoms.ts'],
+      deps: {
+        // Transform inline modules (allows ESM in require context)
+        interopDefault: true,
+      },
+      server: {
+        deps: {
+          // Inline workspace packages that use CJS format
+          inline: [
+            '@growi/remark-attachment-refs',
+            '@growi/remark-drawio',
+            '@growi/remark-lsx',
+            /src\/server\/events/,
+          ],
+        },
+      },
     },
   }),