From 11ccd678de06da050d57c02c46e220931e45d5a3 Mon Sep 17 00:00:00 2001
From: Tilman Vatteroth <git@tilmanvatteroth.de>
Date: Fri, 19 May 2023 18:17:01 +0200
Subject: [PATCH] test: add test for permissions guard

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
---
 .../src/permissions/permissions.guard.spec.ts | 208 ++++++++++++++++++
 1 file changed, 208 insertions(+)
 create mode 100644 backend/src/permissions/permissions.guard.spec.ts

diff --git a/backend/src/permissions/permissions.guard.spec.ts b/backend/src/permissions/permissions.guard.spec.ts
new file mode 100644
index 000000000..a5bd3d3f7
--- /dev/null
+++ b/backend/src/permissions/permissions.guard.spec.ts
@@ -0,0 +1,208 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { ExecutionContext } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { Mock } from 'ts-mockery';
+
+import * as ExtractNoteIdOrAliasModule from '../api/utils/extract-note-from-request';
+import { CompleteRequest } from '../api/utils/request.type';
+import { ConsoleLoggerService } from '../logger/console-logger.service';
+import { Note } from '../notes/note.entity';
+import { User } from '../users/user.entity';
+import {
+  getNotePermissionDisplayName,
+  NotePermission,
+} from './note-permission.enum';
+import { PermissionsGuard } from './permissions.guard';
+import { PermissionsService } from './permissions.service';
+import { PERMISSION_METADATA_KEY } from './require-permission.decorator';
+import { RequiredPermission } from './required-permission.enum';
+
+jest.mock('../api/utils/extract-note-from-request');
+
+describe('permissions guard', () => {
+  let loggerService: ConsoleLoggerService;
+  let reflector: Reflector;
+  let handler: () => void;
+  let permissionsService: PermissionsService;
+  let requiredPermission: RequiredPermission | undefined;
+  let createAllowed = false;
+  let requestUser: User | undefined;
+  let context: ExecutionContext;
+  let permissionGuard: PermissionsGuard;
+  let determinedPermission: NotePermission;
+  let mockedNote: Note;
+
+  beforeEach(() => {
+    loggerService = Mock.of<ConsoleLoggerService>({
+      setContext: jest.fn(),
+      error: jest.fn(),
+    });
+
+    reflector = Mock.of<Reflector>({
+      get: jest.fn(() => requiredPermission),
+    });
+
+    handler = jest.fn();
+
+    permissionsService = Mock.of<PermissionsService>({
+      mayCreate: jest.fn(() => createAllowed),
+      determinePermission: jest.fn(() => Promise.resolve(determinedPermission)),
+    });
+
+    requestUser = Mock.of<User>({});
+
+    const request = Mock.of<CompleteRequest>({
+      user: requestUser,
+    });
+
+    context = Mock.of<ExecutionContext>({
+      getHandler: () => handler,
+      switchToHttp: () =>
+        Mock.of({
+          getRequest: () => request,
+        }),
+    });
+    mockedNote = Mock.of<Note>({});
+    jest
+      .spyOn(ExtractNoteIdOrAliasModule, 'extractNoteFromRequest')
+      .mockReturnValue(Promise.resolve(mockedNote));
+
+    permissionGuard = new PermissionsGuard(
+      loggerService,
+      reflector,
+      permissionsService,
+      Mock.of({}),
+    );
+
+    createAllowed = false;
+  });
+
+  it('sets the correct logger context', () => {
+    expect(loggerService.setContext).toHaveBeenCalledWith(
+      PermissionsGuard.name,
+    );
+  });
+
+  it('deny fail with no required permission', async () => {
+    requiredPermission = undefined;
+    requestUser = undefined;
+
+    expect(await permissionGuard.canActivate(context)).toBe(false);
+    expect(loggerService.error).toHaveBeenCalledTimes(1);
+    expect(reflector.get).toHaveBeenNthCalledWith(
+      1,
+      PERMISSION_METADATA_KEY,
+      handler,
+    );
+  });
+
+  describe('with create permission required', () => {
+    it('will allow if user is allowed to create a note', async () => {
+      createAllowed = true;
+      requiredPermission = RequiredPermission.CREATE;
+
+      expect(await permissionGuard.canActivate(context)).toBe(true);
+      expect(permissionsService.mayCreate).toHaveBeenNthCalledWith(
+        1,
+        requestUser,
+      );
+      expect(reflector.get).toHaveBeenNthCalledWith(
+        1,
+        PERMISSION_METADATA_KEY,
+        handler,
+      );
+    });
+
+    it("will deny if user isn't allowed to create a note", async () => {
+      requiredPermission = RequiredPermission.CREATE;
+
+      expect(await permissionGuard.canActivate(context)).toBe(false);
+      expect(permissionsService.mayCreate).toHaveBeenNthCalledWith(
+        1,
+        requestUser,
+      );
+      expect(reflector.get).toHaveBeenNthCalledWith(
+        1,
+        PERMISSION_METADATA_KEY,
+        handler,
+      );
+    });
+  });
+
+  it('will deny if no note alias is present', async () => {
+    jest
+      .spyOn(ExtractNoteIdOrAliasModule, 'extractNoteFromRequest')
+      .mockReturnValue(Promise.resolve(undefined));
+
+    requiredPermission = RequiredPermission.READ;
+
+    expect(await permissionGuard.canActivate(context)).toBe(false);
+    expect(permissionsService.mayCreate).toHaveBeenCalledTimes(0);
+    expect(reflector.get).toHaveBeenNthCalledWith(
+      1,
+      PERMISSION_METADATA_KEY,
+      handler,
+    );
+    expect(loggerService.error).toHaveBeenCalledTimes(1);
+  });
+
+  describe.each([
+    [RequiredPermission.READ, NotePermission.READ, NotePermission.DENY],
+    [RequiredPermission.WRITE, NotePermission.WRITE, NotePermission.READ],
+    [RequiredPermission.OWNER, NotePermission.OWNER, NotePermission.WRITE],
+  ])(
+    'with required permission %s',
+    (
+      shouldRequiredPermission,
+      sufficientNotePermission,
+      notEnoughNotePermission,
+    ) => {
+      const sufficientNotePermissionDisplayName = getNotePermissionDisplayName(
+        sufficientNotePermission,
+      );
+      const notEnoughNotePermissionDisplayName = getNotePermissionDisplayName(
+        notEnoughNotePermission,
+      );
+
+      beforeEach(() => {
+        requiredPermission = shouldRequiredPermission;
+      });
+
+      it(`will allow for note permission ${sufficientNotePermissionDisplayName}`, async () => {
+        determinedPermission = sufficientNotePermission;
+        expect(await permissionGuard.canActivate(context)).toBe(true);
+        expect(permissionsService.mayCreate).toHaveBeenCalledTimes(0);
+        expect(reflector.get).toHaveBeenNthCalledWith(
+          1,
+          PERMISSION_METADATA_KEY,
+          handler,
+        );
+        expect(permissionsService.determinePermission).toHaveBeenNthCalledWith(
+          1,
+          requestUser,
+          mockedNote,
+        );
+      });
+
+      it(`will deny for note permission ${notEnoughNotePermissionDisplayName}`, async () => {
+        determinedPermission = notEnoughNotePermission;
+        expect(await permissionGuard.canActivate(context)).toBe(false);
+        expect(permissionsService.mayCreate).toHaveBeenCalledTimes(0);
+        expect(reflector.get).toHaveBeenNthCalledWith(
+          1,
+          PERMISSION_METADATA_KEY,
+          handler,
+        );
+        expect(permissionsService.determinePermission).toHaveBeenNthCalledWith(
+          1,
+          requestUser,
+          mockedNote,
+        );
+      });
+    },
+  );
+});