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({ event: vi.fn().mockImplementation((eventName) => { if (eventName === 'user') { return mock({ 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ event: vi.fn().mockImplementation((eventName) => { if (eventName === 'user') { return mock({ on: vi.fn(), }); } }), }); const userModelFactory = (await import('../../models/user')).default; User = userModelFactory(crowiMock); }); it('should authenticate with no scopes', async() => { // arrange const reqMock = mock({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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({ user: undefined, }); const resMock = mock(); 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(); }); });