Yuki Takei 1 месяц назад
Родитель
Сommit
9ff1b9fefc

+ 4 - 1
apps/app/src/server/routes/apiv3/page/index.ts

@@ -108,7 +108,10 @@ module.exports = (crowi: Crowi) => {
       query('includeEmpty').optional().isBoolean(),
     ],
     likes: [body('pageId').isString(), body('bool').isBoolean()],
-    info: [query('pageId').isMongoId().withMessage('pageId is required')],
+    info: [
+      query('pageId').isMongoId().withMessage('pageId is required'),
+      query('shareLinkId').optional().isMongoId(),
+    ],
     getGrantData: [
       query('pageId').isMongoId().withMessage('pageId is required'),
     ],

+ 179 - 0
apps/app/src/server/routes/apiv3/page/page-info-sharelink.integ.ts

@@ -0,0 +1,179 @@
+import type { NextFunction, Request, Response } from 'express';
+import express from 'express';
+import mockRequire from 'mock-require';
+import request from 'supertest';
+
+import { getInstance } from '^/test/setup/crowi';
+
+import type Crowi from '~/server/crowi';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import * as findPageModule from '~/server/service/page/find-page-and-meta-data-by-viewer';
+
+// Passthrough middleware for testing - skips authentication
+const passthroughMiddleware = (
+  _req: Request,
+  _res: Response,
+  next: NextFunction,
+) => next();
+
+// Mock certify-shared-page middleware - sets isSharedPage when shareLinkId is present
+const mockCertifySharedPage = (
+  req: Request,
+  _res: Response,
+  next: NextFunction,
+) => {
+  const { shareLinkId, pageId } = req.query;
+  if (shareLinkId && pageId) {
+    // In real implementation, this checks if shareLink exists and is valid
+    req.isSharedPage = true;
+  }
+  next();
+};
+
+// Mock middlewares using vi.mock (hoisted to top)
+vi.mock('~/server/middlewares/access-token-parser', () => ({
+  accessTokenParser: () => passthroughMiddleware,
+}));
+
+vi.mock('~/server/middlewares/login-required', () => ({
+  default: () => (req: Request, _res: Response, next: NextFunction) => {
+    // Allow access if isSharedPage is true (anonymous user accessing share link)
+    if (req.isSharedPage) {
+      return next();
+    }
+    // For non-shared pages, authentication would be required
+    // In this test, we should not reach this path for share links
+    return next();
+  },
+}));
+
+describe('GET /info with Share Link', () => {
+  let app: express.Application;
+  let crowi: Crowi;
+
+  // Valid ObjectId strings for testing
+  const validPageId = '507f1f77bcf86cd799439011';
+  const validShareLinkId = '507f1f77bcf86cd799439012';
+
+  beforeAll(async () => {
+    crowi = await getInstance();
+  });
+
+  beforeEach(async () => {
+    // Mock certify-shared-page using mock-require
+    mockRequire(
+      '../../../middlewares/certify-shared-page',
+      () => mockCertifySharedPage,
+    );
+
+    // Mock findPageAndMetaDataByViewer to return minimal successful response
+    vi.spyOn(findPageModule, 'findPageAndMetaDataByViewer').mockResolvedValue({
+      data: {
+        _id: validPageId,
+        path: '/test-page',
+        revision: {
+          _id: '507f1f77bcf86cd799439013',
+          body: 'Test page content',
+        },
+      } as any,
+      meta: {
+        isNotFound: false,
+        isForbidden: false,
+      },
+    });
+
+    // Setup express app
+    app = express();
+    app.use(express.json());
+
+    // Mock apiv3 response methods
+    app.use((_req, res, next) => {
+      const apiRes = res as ApiV3Response;
+      apiRes.apiv3 = (data) => res.json(data);
+      apiRes.apiv3Err = (error, statusCode?: number) => {
+        // Check if error is validation error (array of ErrorV3)
+        const isValidationError =
+          Array.isArray(error) &&
+          error.some((e: any) => e?.code === 'validation_failed');
+        const status = statusCode ?? (isValidationError ? 400 : 500);
+        const errorMessage = error?.message || error;
+        return res.status(status).json({ error: errorMessage });
+      };
+      next();
+    });
+
+    // Inject crowi instance
+    app.use((req, _res, next) => {
+      req.crowi = crowi;
+      next();
+    });
+
+    // Import and mount the actual router
+    const pageModule = await import('./index');
+    const pageRouterFactory = (pageModule as any).default || pageModule;
+    const pageRouter = pageRouterFactory(crowi);
+    app.use('/', pageRouter);
+
+    // Error handling middleware (must be after router)
+    app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
+      const apiRes = res as ApiV3Response;
+      const statusCode = err.statusCode || err.status || 500;
+      return apiRes.apiv3Err(err, statusCode);
+    });
+  });
+
+  afterEach(() => {
+    // Clean up mocks
+    mockRequire.stopAll();
+    vi.clearAllMocks();
+    vi.restoreAllMocks();
+  });
+
+  describe('with valid shareLinkId parameter', () => {
+    it('should return 200 when accessing with both pageId and shareLinkId', async () => {
+      const response = await request(app)
+        .get('/info')
+        .query({ pageId: validPageId, shareLinkId: validShareLinkId });
+
+      expect(response.status).toBe(200);
+      expect(response.body).toHaveProperty('isNotFound');
+      expect(response.body).toHaveProperty('isForbidden');
+    });
+
+    it('should accept shareLinkId as optional parameter', async () => {
+      const response = await request(app)
+        .get('/info')
+        .query({ pageId: validPageId, shareLinkId: validShareLinkId });
+
+      expect(response.status).not.toBe(400); // Should not be validation error
+    });
+  });
+
+  describe('without shareLinkId parameter', () => {
+    it('should still work for normal page access', async () => {
+      const response = await request(app)
+        .get('/info')
+        .query({ pageId: validPageId });
+
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe('validation', () => {
+    it('should reject invalid shareLinkId format', async () => {
+      const response = await request(app)
+        .get('/info')
+        .query({ pageId: validPageId, shareLinkId: 'invalid-id' });
+
+      expect(response.status).toBe(400);
+    });
+
+    it('should still require pageId parameter', async () => {
+      const response = await request(app)
+        .get('/info')
+        .query({ shareLinkId: validShareLinkId });
+
+      expect(response.status).toBe(400);
+    });
+  });
+});