|
|
@@ -9,6 +9,12 @@ 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';
|
|
|
|
|
|
+// Extend Request type for test
|
|
|
+interface TestRequest extends Request {
|
|
|
+ isSharedPage?: boolean;
|
|
|
+ crowi?: Crowi;
|
|
|
+}
|
|
|
+
|
|
|
// Passthrough middleware for testing - skips authentication
|
|
|
const passthroughMiddleware = (
|
|
|
_req: Request,
|
|
|
@@ -18,7 +24,7 @@ const passthroughMiddleware = (
|
|
|
|
|
|
// Mock certify-shared-page middleware - sets isSharedPage when shareLinkId is present
|
|
|
const mockCertifySharedPage = (
|
|
|
- req: Request,
|
|
|
+ req: TestRequest,
|
|
|
_res: Response,
|
|
|
next: NextFunction,
|
|
|
) => {
|
|
|
@@ -36,18 +42,17 @@ vi.mock('~/server/middlewares/access-token-parser', () => ({
|
|
|
}));
|
|
|
|
|
|
vi.mock('~/server/middlewares/login-required', () => ({
|
|
|
- default: () => (req: Request, _res: Response, next: NextFunction) => {
|
|
|
+ default: () => (req: TestRequest, _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', () => {
|
|
|
+describe('GET /info', () => {
|
|
|
let app: express.Application;
|
|
|
let crowi: Crowi;
|
|
|
|
|
|
@@ -79,7 +84,7 @@ describe('GET /info with Share Link', () => {
|
|
|
meta: {
|
|
|
isNotFound: false,
|
|
|
isForbidden: false,
|
|
|
- },
|
|
|
+ } as any,
|
|
|
});
|
|
|
|
|
|
// Setup express app
|
|
|
@@ -89,21 +94,21 @@ describe('GET /info with Share Link', () => {
|
|
|
// 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) => {
|
|
|
+ apiRes.apiv3 = (data: unknown) => res.json(data);
|
|
|
+ apiRes.apiv3Err = (error: unknown, statusCode?: number) => {
|
|
|
// Check if error is validation error (array of ErrorV3)
|
|
|
const isValidationError =
|
|
|
Array.isArray(error) &&
|
|
|
- error.some((e: any) => e?.code === 'validation_failed');
|
|
|
+ error.some((e: unknown) => (e as any)?.code === 'validation_failed');
|
|
|
const status = statusCode ?? (isValidationError ? 400 : 500);
|
|
|
- const errorMessage = error?.message || error;
|
|
|
+ const errorMessage = (error as any)?.message || error;
|
|
|
return res.status(status).json({ error: errorMessage });
|
|
|
};
|
|
|
next();
|
|
|
});
|
|
|
|
|
|
// Inject crowi instance
|
|
|
- app.use((req, _res, next) => {
|
|
|
+ app.use((req: TestRequest, _res, next) => {
|
|
|
req.crowi = crowi;
|
|
|
next();
|
|
|
});
|
|
|
@@ -115,9 +120,9 @@ describe('GET /info with Share Link', () => {
|
|
|
app.use('/', pageRouter);
|
|
|
|
|
|
// Error handling middleware (must be after router)
|
|
|
- app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
|
|
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
|
const apiRes = res as ApiV3Response;
|
|
|
- const statusCode = err.statusCode || err.status || 500;
|
|
|
+ const statusCode = (err as any).statusCode || (err as any).status || 500;
|
|
|
return apiRes.apiv3Err(err, statusCode);
|
|
|
});
|
|
|
});
|
|
|
@@ -129,7 +134,61 @@ describe('GET /info with Share Link', () => {
|
|
|
vi.restoreAllMocks();
|
|
|
});
|
|
|
|
|
|
- describe('with valid shareLinkId parameter', () => {
|
|
|
+ describe('Normal page access', () => {
|
|
|
+ it('should return 200 with page meta when pageId is valid', async () => {
|
|
|
+ const response = await request(app)
|
|
|
+ .get('/info')
|
|
|
+ .query({ pageId: validPageId });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ expect(response.body).toHaveProperty('isNotFound');
|
|
|
+ expect(response.body).toHaveProperty('isForbidden');
|
|
|
+ expect(response.body.isNotFound).toBe(false);
|
|
|
+ expect(response.body.isForbidden).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should return 403 when page is forbidden', async () => {
|
|
|
+ vi.spyOn(findPageModule, 'findPageAndMetaDataByViewer').mockResolvedValue(
|
|
|
+ {
|
|
|
+ data: null,
|
|
|
+ meta: {
|
|
|
+ isNotFound: true,
|
|
|
+ isForbidden: true,
|
|
|
+ } as any,
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ const response = await request(app)
|
|
|
+ .get('/info')
|
|
|
+ .query({ pageId: validPageId });
|
|
|
+
|
|
|
+ expect(response.status).toBe(403);
|
|
|
+ expect(response.body).toHaveProperty('error');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should return 200 when page is empty (not found but not forbidden)', async () => {
|
|
|
+ vi.spyOn(findPageModule, 'findPageAndMetaDataByViewer').mockResolvedValue(
|
|
|
+ {
|
|
|
+ data: null,
|
|
|
+ meta: {
|
|
|
+ isNotFound: true,
|
|
|
+ isForbidden: false,
|
|
|
+ isEmpty: true,
|
|
|
+ } as any,
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ const response = await request(app)
|
|
|
+ .get('/info')
|
|
|
+ .query({ pageId: validPageId });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ expect(response.body).toHaveProperty('isEmpty');
|
|
|
+ expect(response.body.isEmpty).toBe(true);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Share link access', () => {
|
|
|
it('should return 200 when accessing with both pageId and shareLinkId', async () => {
|
|
|
const response = await request(app)
|
|
|
.get('/info')
|
|
|
@@ -149,17 +208,15 @@ describe('GET /info with Share Link', () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- describe('without shareLinkId parameter', () => {
|
|
|
- it('should still work for normal page access', async () => {
|
|
|
+ describe('Validation', () => {
|
|
|
+ it('should reject invalid pageId format', async () => {
|
|
|
const response = await request(app)
|
|
|
.get('/info')
|
|
|
- .query({ pageId: validPageId });
|
|
|
+ .query({ pageId: 'invalid-id' });
|
|
|
|
|
|
- expect(response.status).toBe(200);
|
|
|
+ expect(response.status).toBe(400);
|
|
|
});
|
|
|
- });
|
|
|
|
|
|
- describe('validation', () => {
|
|
|
it('should reject invalid shareLinkId format', async () => {
|
|
|
const response = await request(app)
|
|
|
.get('/info')
|
|
|
@@ -168,12 +225,32 @@ describe('GET /info with Share Link', () => {
|
|
|
expect(response.status).toBe(400);
|
|
|
});
|
|
|
|
|
|
- it('should still require pageId parameter', async () => {
|
|
|
+ it('should require pageId parameter', async () => {
|
|
|
+ const response = await request(app).get('/info');
|
|
|
+
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should work with only pageId (shareLinkId is optional)', async () => {
|
|
|
const response = await request(app)
|
|
|
.get('/info')
|
|
|
- .query({ shareLinkId: validShareLinkId });
|
|
|
+ .query({ pageId: validPageId });
|
|
|
|
|
|
- expect(response.status).toBe(400);
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Error handling', () => {
|
|
|
+ it('should return 500 when service throws an error', async () => {
|
|
|
+ vi.spyOn(findPageModule, 'findPageAndMetaDataByViewer').mockRejectedValue(
|
|
|
+ new Error('Service error'),
|
|
|
+ );
|
|
|
+
|
|
|
+ const response = await request(app)
|
|
|
+ .get('/info')
|
|
|
+ .query({ pageId: validPageId });
|
|
|
+
|
|
|
+ expect(response.status).toBe(500);
|
|
|
});
|
|
|
});
|
|
|
});
|