Yuki Takei 1 year ago
parent
commit
660a711e57

+ 94 - 0
apps/app/src/server/middlewares/access-token-parser/access-token-parser.integ.ts

@@ -0,0 +1,94 @@
+import type { Response, NextFunction } from 'express';
+import { mock } from 'vitest-mock-extended';
+
+import type Crowi from '~/server/crowi';
+import type UserEvent from '~/server/events/user';
+
+import type { AccessTokenParserReq } from './interfaces';
+
+import { accessTokenParser } from '.';
+
+
+// jest.mock('mongoose', () => ({
+//   model: jest.fn().mockReturnValue({
+//     findUserByApiToken: jest.fn(),
+//   }),
+// }));
+
+// const mockUser = {
+//   _id: 'userId',
+//   username: 'testuser',
+//   email: 'testuser@example.com',
+// };
+
+
+describe('access-token-parser middleware', () => {
+
+  let User;
+
+  beforeAll(async() => {
+    const crowiMock = mock<Crowi>({
+      event: vi.fn().mockImplementation((eventName) => {
+        if (eventName === 'user') {
+          return mock<UserEvent>({
+            on: vi.fn(),
+          });
+        }
+      }),
+    });
+    const userModelFactory = (await import('../../models/user')).default;
+    User = userModelFactory(crowiMock);
+  });
+
+  it('should call next if no access token is provided', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // act
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeUndefined();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+  it('should call next if the given access token is invalid', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // act
+    reqMock.query.access_token = 'invalidToken';
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeUndefined();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+  // it('should call next if access token is invalid', async() => {
+  //   (mongoose.model().findUserByApiToken as jest.Mock).mockResolvedValue(null);
+  //   req.query.access_token = 'invalidToken';
+  //   await accessTokenParser(req as Request, res as Response, next);
+  //   expect(next).toHaveBeenCalled();
+  // });
+
+  // it('should set req.user if access token is valid', async() => {
+  //   (mongoose.model().findUserByApiToken as jest.Mock).mockResolvedValue(mockUser);
+  //   req.query.access_token = 'validToken';
+  //   await accessTokenParser(req as Request, res as Response, next);
+  //   expect(req.user).toEqual(mockUser);
+  //   expect(next).toHaveBeenCalled();
+  // });
+});

+ 19 - 36
apps/app/src/server/middlewares/access-token-parser/access-token-parser.ts

@@ -1,55 +1,38 @@
 import type { IUser, IUserHasId } from '@growi/core/dist/interfaces';
-import type { IUserSerializedSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import type { NextFunction, Request, Response } from 'express';
+import type { NextFunction, Response } from 'express';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
+import type { AccessTokenParserReq } from './interfaces';
 
 const logger = loggerFactory('growi:middleware:access-token-parser');
 
-type ReqQuery = {
-  access_token?: string,
-}
-type ReqBody = {
-  access_token?: string,
-}
 
-interface Req extends Request<undefined, undefined, ReqBody, ReqQuery> {
-  user: IUserSerializedSecurely<IUserHasId>,
-}
-
-const middlewareFactory = () => {
-
-  return async(req: Req, res: Response, next: NextFunction): Promise<void> => {
-    // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
-    const accessToken = req.query.access_token ?? req.body.access_token;
-    if (accessToken == null || typeof accessToken !== 'string') {
-      return next();
-    }
-
-    const User = mongoose.model<HydratedDocument<IUser>, { findUserByApiToken }>('User');
+export const accessTokenParser = async(req: AccessTokenParserReq, res: Response, next: NextFunction): Promise<void> => {
+  // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
+  const accessToken = req.query.access_token ?? req.body.access_token;
+  if (accessToken == null || typeof accessToken !== 'string') {
+    return next();
+  }
 
-    logger.debug('accessToken is', accessToken);
+  const User = mongoose.model<HydratedDocument<IUser>, { findUserByApiToken }>('User');
 
-    const user: IUserHasId = await User.findUserByApiToken(accessToken);
+  logger.debug('accessToken is', accessToken);
 
-    if (user == null) {
-      logger.debug('The access token is invalid');
-      return next();
-    }
+  const user: IUserHasId = await User.findUserByApiToken(accessToken);
 
-    // transforming attributes
-    req.user = serializeUserSecurely(user);
+  if (user == null) {
+    logger.debug('The access token is invalid');
+    return next();
+  }
 
-    logger.debug('Access token parsed.');
+  // transforming attributes
+  req.user = serializeUserSecurely(user);
 
-    return next();
-  };
+  logger.debug('Access token parsed.');
 
+  return next();
 };
-
-module.exports = middlewareFactory;
-// export default middlewareFactory;

+ 14 - 0
apps/app/src/server/middlewares/access-token-parser/interfaces.ts

@@ -0,0 +1,14 @@
+import type { IUserHasId } from '@growi/core/dist/interfaces';
+import type { IUserSerializedSecurely } from '@growi/core/dist/models/serializers';
+import type { Request } from 'express';
+
+type ReqQuery = {
+  access_token?: string,
+}
+type ReqBody = {
+  access_token?: string,
+}
+
+export interface AccessTokenParserReq extends Request<undefined, undefined, ReqBody, ReqQuery> {
+  user?: IUserSerializedSecurely<IUserHasId>,
+}