| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- import { faker } from '@faker-js/faker';
- import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
- import type { Response } from 'express';
- import { mock } from 'vitest-mock-extended';
- import { SCOPE } from '~/interfaces/scope';
- import type Crowi from '~/server/crowi';
- import type UserEvent from '~/server/events/user';
- import { AccessToken } from '~/server/models/access-token';
- import type { AccessTokenParserReq } from './interfaces';
- import { accessTokenParser } from '.';
- vi.mock('@growi/core/dist/models/serializers', { spy: true });
- 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(serializeUserSecurely).not.toHaveBeenCalled();
- 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(serializeUserSecurely).not.toHaveBeenCalled();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should set req.user with a valid api token in query', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user with an access token
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- apiToken: faker.internet.password(),
- });
- // act
- reqMock.query.access_token = targetUser.apiToken;
- await accessTokenParser()(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should set req.user with a valid api token in body', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user with an access token
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- apiToken: faker.internet.password(),
- });
- // act
- reqMock.body.access_token = targetUser.apiToken;
- await accessTokenParser()(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- });
- describe('access-token-parser middleware for access token with scopes', () => {
- 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 authenticate with no scopes', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- });
- // generate token with read:user:info scope
- const { token } = await AccessToken.generateToken(
- targetUser._id,
- new Date(Date.now() + 1000 * 60 * 60 * 24),
- );
- // act
- reqMock.query.access_token = token;
- await accessTokenParser()(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should authenticate with specific scope', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- });
- // generate token with read:user:info scope
- const { token } = await AccessToken.generateToken(
- targetUser._id,
- new Date(Date.now() + 1000 * 60 * 60 * 24),
- [SCOPE.READ.USER.INFO],
- );
- // act
- reqMock.query.access_token = token;
- await accessTokenParser()(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should reject with insufficient scopes', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- });
- // generate token with read:user:info scope
- const { token } = await AccessToken.generateToken(
- targetUser._id,
- new Date(Date.now() + 1000 * 60 * 60 * 24),
- [SCOPE.READ.USER.INFO],
- );
- // act - try to access with write:user:info scope
- reqMock.query.access_token = token;
- await accessTokenParser([SCOPE.WRITE.USER.INFO])(reqMock, resMock, nextMock);
- // // assert
- expect(reqMock.user).toBeUndefined();
- expect(serializeUserSecurely).not.toHaveBeenCalled();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should authenticate with write scope implying read scope', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- expect(reqMock.user).toBeUndefined();
- // prepare a user
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- });
- // generate token with write:user:info scope
- const { token } = await AccessToken.generateToken(
- targetUser._id,
- new Date(Date.now() + 1000 * 60 * 60 * 24),
- [SCOPE.WRITE.USER.INFO],
- );
- // act - try to access with read:user:info scope
- reqMock.query.access_token = token;
- await accessTokenParser([SCOPE.READ.USER.INFO])(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- it('should authenticate with wildcard scope', async() => {
- // arrange
- const reqMock = mock<AccessTokenParserReq>({
- user: undefined,
- });
- const resMock = mock<Response>();
- const nextMock = vi.fn();
- // prepare a user
- const targetUser = await User.create({
- name: faker.person.fullName(),
- username: faker.string.uuid(),
- password: faker.internet.password(),
- lang: 'en_US',
- });
- // generate token with read:user:* scope
- const { token } = await AccessToken.generateToken(
- targetUser._id,
- new Date(Date.now() + 1000 * 60 * 60 * 24),
- [SCOPE.READ.USER.ALL],
- );
- // act - try to access with read:user:info scope
- reqMock.query.access_token = token;
- await accessTokenParser([SCOPE.READ.USER.INFO, SCOPE.READ.USER.API.ACCESS_TOKEN])(reqMock, resMock, nextMock);
- // assert
- expect(reqMock.user).toBeDefined();
- expect(reqMock.user?._id).toStrictEqual(targetUser._id);
- expect(serializeUserSecurely).toHaveBeenCalledOnce();
- expect(nextMock).toHaveBeenCalled();
- });
- });
|