Skip to content

Commit

Permalink
Merge branch 'main' into BC-5714-store-upload-state
Browse files Browse the repository at this point in the history
  • Loading branch information
bischofmax authored Jan 16, 2024
2 parents 339dfe1 + 22c0c76 commit 17269a9
Show file tree
Hide file tree
Showing 177 changed files with 4,273 additions and 1,863 deletions.
3 changes: 3 additions & 0 deletions apps/server/src/apps/server.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { join } from 'path';
// register source-map-support for debugging
import { install as sourceMapInstall } from 'source-map-support';

import { ColumnBoardService } from '@modules/board';
import { AppStartLoggable } from './helpers/app-start-loggable';
import {
addPrometheusMetricsMiddlewaresIfEnabled,
Expand Down Expand Up @@ -87,6 +88,8 @@ async function bootstrap() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
feathersExpress.services['nest-group-service'] = nestApp.get(GroupService);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
feathersExpress.services['nest-column-board-service'] = nestApp.get(ColumnBoardService);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
feathersExpress.services['nest-system-rule'] = nestApp.get(SystemRule);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
feathersExpress.services['nest-orm'] = orm;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CourseRepo } from '@shared/repo';
import { LoggerModule } from '@src/core/logger';
import { DrawingElementAdapterService } from '@modules/tldraw-client/service/drawing-element-adapter.service';
import { HttpModule } from '@nestjs/axios';
import { ToolConfigModule } from '@modules/tool/tool-config.module';
import { BoardDoRepo, BoardNodeRepo, RecursiveDeleteVisitor } from './repo';
import {
BoardDoAuthorizableService,
Expand All @@ -29,6 +30,7 @@ import { ColumnBoardCopyService } from './service/column-board-copy.service';
UserModule,
ContextExternalToolModule,
HttpModule,
ToolConfigModule,
],
providers: [
BoardDoAuthorizableService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { ServerTestModule } from '@modules/server';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType } from '@shared/domain/domainobject';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
} from '@shared/testing';
import { drawingElementNodeFactory } from '@shared/testing/factory/boardnode/drawing-element-node.factory';

const baseRouteName = '/elements';
describe('drawing permission check (api)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

describe('when user is a valid teacher who is part of course', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const course = courseFactory.build({ teachers: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode });

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, teacherUser, columnBoardNode, columnNode, cardNode, drawingItemNode };
};

it('should return status 200', async () => {
const { loggedInClient, drawingItemNode } = await setup();

const response = await loggedInClient.get(`${drawingItemNode.id}/permission`);

expect(response.status).toEqual(200);
});
});

describe('when only teacher is part of course', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();

const course = courseFactory.build({ students: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course, studentAccount, studentUser]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode });

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount);

return { loggedInClient, studentUser, columnBoardNode, columnNode, cardNode, drawingItemNode };
};

it('should return status 403 for student not assigned to course', async () => {
const { loggedInClient, drawingItemNode } = await setup();

const response = await loggedInClient.get(`${drawingItemNode.id}/permission`);

expect(response.status).toEqual(403);
});
});

describe('when asking for non-existing resource', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
await em.persistAndFlush([teacherAccount, teacherUser]);

em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient };
};

it('should return status 404 for wrong id', async () => {
const { loggedInClient } = await setup();
const wrongRandomId = '655b048616056135293d1e63';

const response = await loggedInClient.get(`${wrongRandomId}/permission`);

expect(response.status).toEqual(404);
});
});
});
14 changes: 14 additions & 0 deletions apps/server/src/modules/board/controller/element.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
NotFoundException,
Param,
Expand Down Expand Up @@ -141,4 +142,17 @@ export class ElementController {

return response;
}

@ApiOperation({ summary: 'Check if user has read permission for any board element.' })
@ApiResponse({ status: 200 })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@Get(':contentElementId/permission')
async readPermission(
@Param() urlParams: ContentElementUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.elementUc.checkElementReadPermission(currentUser.userId, urlParams.contentElementId);
}
}
68 changes: 65 additions & 3 deletions apps/server/src/modules/board/repo/board-do.repo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createMock } from '@golevelup/ts-jest';
import { MongoMemoryDatabaseModule } from '@infra/database';
import { NotFoundError } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/mongodb';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { FilesStorageClientAdapterService } from '@modules/files-storage-client';
import { DrawingElementAdapterService } from '@modules/tldraw-client/service/drawing-element-adapter.service';
import { ContextExternalToolService } from '@modules/tool/context-external-tool/service';
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AnyBoardDo, BoardExternalReferenceType, Card, Column, ColumnBoard } from '@shared/domain/domainobject';
import { CardNode, RichTextElementNode } from '@shared/domain/entity';
import { CardNode, ColumnBoardNode, ExternalToolElementNodeEntity, RichTextElementNode } from '@shared/domain/entity';
import { EntityId } from '@shared/domain/types';
import {
cardFactory,
cardNodeFactory,
Expand All @@ -16,12 +18,16 @@ import {
columnBoardNodeFactory,
columnFactory,
columnNodeFactory,
contextExternalToolEntityFactory,
contextExternalToolFactory,
courseFactory,
externalToolElementNodeFactory,
fileElementFactory,
richTextElementFactory,
richTextElementNodeFactory,
} from '@shared/testing';
import { DrawingElementAdapterService } from '@modules/tldraw-client/service/drawing-element-adapter.service';
import { ContextExternalToolEntity } from '../../tool';
import { ContextExternalTool } from '../../tool/context-external-tool/domain';
import { BoardDoRepo } from './board-do.repo';
import { BoardNodeRepo } from './board-node.repo';
import { RecursiveDeleteVisitor } from './recursive-delete.vistor';
Expand Down Expand Up @@ -267,6 +273,62 @@ describe(BoardDoRepo.name, () => {
});
});

describe('countBoardUsageForExternalTools', () => {
describe('when counting the amount of boards used by the selected tools', () => {
const setup = async () => {
const contextExternalToolId: EntityId = new ObjectId().toHexString();
const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(
undefined,
contextExternalToolId
);
const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId(
undefined,
contextExternalToolId
);
const otherContextExternalToolEntity: ContextExternalToolEntity =
contextExternalToolEntityFactory.buildWithId();

const board: ColumnBoardNode = columnBoardNodeFactory.buildWithId();
const otherBoard: ColumnBoardNode = columnBoardNodeFactory.buildWithId();
const card: CardNode = cardNodeFactory.buildWithId({ parent: board });
const otherCard: CardNode = cardNodeFactory.buildWithId({ parent: otherBoard });
const externalToolElements: ExternalToolElementNodeEntity[] = externalToolElementNodeFactory.buildListWithId(
2,
{
parent: card,
contextExternalTool: contextExternalToolEntity,
}
);
const otherExternalToolElement: ExternalToolElementNodeEntity = externalToolElementNodeFactory.buildWithId({
parent: otherCard,
contextExternalTool: otherContextExternalToolEntity,
});

await em.persistAndFlush([
board,
otherBoard,
card,
otherCard,
...externalToolElements,
otherExternalToolElement,
contextExternalToolEntity,
]);

return {
contextExternalTool,
};
};

it('should return the amount of boards used by the selected tools', async () => {
const { contextExternalTool } = await setup();

const result: number = await repo.countBoardUsageForExternalTools([contextExternalTool]);

expect(result).toEqual(1);
});
});
});

describe('getAncestorIds', () => {
describe('when having only a root boardnode', () => {
const setup = async () => {
Expand Down
18 changes: 17 additions & 1 deletion apps/server/src/modules/board/repo/board-do.repo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Utils } from '@mikro-orm/core';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import type { ContextExternalTool } from '@modules/tool/context-external-tool/domain';
import { Injectable, NotFoundException } from '@nestjs/common';
import { AnyBoardDo, BoardExternalReference } from '@shared/domain/domainobject';
import { BoardNode, ColumnBoardNode } from '@shared/domain/entity';
import { BoardNode, ColumnBoardNode, ExternalToolElementNodeEntity } from '@shared/domain/entity';
import { EntityId } from '@shared/domain/types';
import { BoardDoBuilderImpl } from './board-do.builder-impl';
import { BoardNodeRepo } from './board-node.repo';
Expand Down Expand Up @@ -81,6 +82,21 @@ export class BoardDoRepo {
return domainObject;
}

async countBoardUsageForExternalTools(contextExternalTools: ContextExternalTool[]) {
const toolIds: EntityId[] = contextExternalTools
.map((tool: ContextExternalTool): EntityId | undefined => tool.id)
.filter((id: EntityId | undefined): id is EntityId => !!id);

const boardNodes: ExternalToolElementNodeEntity[] = await this.em.find(ExternalToolElementNodeEntity, {
contextExternalTool: { $in: toolIds },
});

const boardIds: EntityId[] = boardNodes.map((node: ExternalToolElementNodeEntity): EntityId => node.ancestorIds[0]);
const boardCount: number = new Set(boardIds).size;

return boardCount;
}

async getAncestorIds(boardDo: AnyBoardDo): Promise<EntityId[]> {
const boardNode = await this.boardNodeRepo.findById(boardDo.id);
return boardNode.ancestorIds;
Expand Down
Loading

0 comments on commit 17269a9

Please sign in to comment.