Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8168 - Implementing video conferences in FE and remaining issues #5420

Merged
merged 15 commits into from
Jan 17, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { RoomModule } from '../room';
import { BoardContextApiHelperService } from './board-context-api-helper.service';
import { BoardModule } from '../board/board.module';
import { LearnroomModule } from '../learnroom';
import { LegacySchoolModule } from '../legacy-school';

@Module({
imports: [BoardModule, LearnroomModule, RoomModule],
imports: [BoardModule, LearnroomModule, RoomModule, LegacySchoolModule],
providers: [BoardContextApiHelperService],
exports: [BoardContextApiHelperService],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { createMock } from '@golevelup/ts-jest';
import { AnyBoardNode, BoardExternalReferenceType, BoardNodeService } from '@modules/board';
import { CourseService } from '@modules/learnroom';
import { RoomService } from '@modules/room';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { CourseFeatures } from '@shared/domain/entity';
import { courseFactory, schoolEntityFactory, setupEntities } from '@shared/testing';
import { BoardFeature } from '../board/domain';
import { cardFactory, columnBoardFactory, columnFactory } from '../board/testing';
import { LegacySchoolService } from '../legacy-school';
import { roomFactory } from '../room/testing';
import { VideoConferenceConfig } from '../video-conference';
import { BoardContextApiHelperService } from './board-context-api-helper.service';

describe('BoardContextApiHelperService', () => {
Expand All @@ -14,6 +19,8 @@ describe('BoardContextApiHelperService', () => {
let courseService: jest.Mocked<CourseService>;
let roomService: jest.Mocked<RoomService>;
let boardNodeService: jest.Mocked<BoardNodeService>;
let legacySchoolService: jest.Mocked<LegacySchoolService>;
let configService: jest.Mocked<ConfigService<VideoConferenceConfig, true>>;

beforeEach(async () => {
await setupEntities();
Expand All @@ -32,13 +39,23 @@ describe('BoardContextApiHelperService', () => {
provide: BoardNodeService,
useValue: createMock<BoardNodeService>(),
},
{
provide: LegacySchoolService,
useValue: createMock<LegacySchoolService>(),
},
{
provide: ConfigService,
useValue: createMock<ConfigService>(),
},
],
}).compile();

service = module.get<BoardContextApiHelperService>(BoardContextApiHelperService);
courseService = module.get(CourseService);
roomService = module.get(RoomService);
boardNodeService = module.get(BoardNodeService);
legacySchoolService = module.get(LegacySchoolService);
configService = module.get(ConfigService);
});

afterAll(async () => {
Expand All @@ -53,16 +70,14 @@ describe('BoardContextApiHelperService', () => {
it('should return schoolId for course context', async () => {
const school = schoolEntityFactory.build();
const course = courseFactory.build({ school });
const cardNode = cardFactory.build();
const columnNode = columnFactory.build();
columnNode.addChild(cardNode);
const card = cardFactory.build();
const column = columnFactory.build({ children: [card] });
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Course, id: 'course.id' },
});
columnBoard.addChild(columnNode);
columnBoard.addChild(column);

boardNodeService.findById.mockResolvedValueOnce(cardNode);
boardNodeService.findRoot.mockResolvedValueOnce(columnBoard);
boardNodeService.findById.mockResolvedValueOnce(card);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);
courseService.findById.mockResolvedValueOnce(course);

Expand All @@ -87,4 +102,136 @@ describe('BoardContextApiHelperService', () => {
expect(result).toBe(room.schoolId);
});
});

describe('getFeaturesForBoardNode', () => {
describe('when context is course', () => {
const setup = () => {
const course = courseFactory.build();
const column = columnFactory.build();
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Course, id: 'course.id' },
children: [column],
});

courseService.findById.mockResolvedValueOnce(course);
boardNodeService.findById.mockResolvedValueOnce(column);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);

return { boardNode: column, course };
};

describe('when video conference is enabled for course', () => {
it('should return video conference feature', async () => {
const { boardNode, course } = setup();

course.features = [CourseFeatures.VIDEOCONFERENCE];
legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is enabled for school', () => {
it('should return video conference feature', async () => {
const { boardNode, course } = setup();

course.features = [];
legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is enabled for config', () => {
it('should return video conference feature', async () => {
const { boardNode, course } = setup();

course.features = [];
legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is disabled entirely', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

const course = courseFactory.build();
courseService.findById.mockResolvedValueOnce(course);
legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});
});

describe('when context is room', () => {
const setup = () => {
const room = roomFactory.build();
const column = columnFactory.build();
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Room, id: 'room.id' },
children: [column],
});

roomService.getSingleRoom.mockResolvedValueOnce(room);
boardNodeService.findById.mockResolvedValueOnce(column);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);

return { boardNode: column, room };
};

describe('when video conference is enabled for school', () => {
it('should return video conference feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is enabled for config', () => {
it('should return video conference feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is disabled entirely', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,42 @@ import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, C
import { CourseService } from '@modules/learnroom';
import { RoomService } from '@modules/room';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain/types';
import { ConfigService } from '@nestjs/config';
import { CourseFeatures } from '@shared/domain/entity';
import { EntityId, SchoolFeature } from '@shared/domain/types';
import { BoardFeature } from '../board/domain';
import { LegacySchoolService } from '../legacy-school';
import { VideoConferenceConfig } from '../video-conference';

@Injectable()
export class BoardContextApiHelperService {
constructor(
private readonly courseService: CourseService,
private readonly roomService: RoomService,
private readonly boardNodeService: BoardNodeService
private readonly boardNodeService: BoardNodeService,
private readonly legacySchoolService: LegacySchoolService,
private readonly configService: ConfigService<VideoConferenceConfig, true>
) {}

public async getSchoolIdForBoardNode(nodeId: EntityId): Promise<EntityId> {
const boardNode = await this.boardNodeService.findById(nodeId);
const board = await this.boardNodeService.findRoot(boardNode);
const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, board.id);
const schoolId = await this.getSchoolIdForBoard(columnBoard.context);
const boardContext = await this.getBoardContext(nodeId);
const schoolId = await this.getSchoolIdForBoardContext(boardContext);
return schoolId;
}

private async getSchoolIdForBoard(context: BoardExternalReference): Promise<EntityId> {
public async getFeaturesForBoardNode(nodeId: EntityId): Promise<BoardFeature[]> {
const boardContext = await this.getBoardContext(nodeId);
const features = await this.getFeaturesForBoardContext(boardContext);
return features;
}

private async getBoardContext(nodeId: EntityId): Promise<BoardExternalReference> {
const boardNode = await this.boardNodeService.findById(nodeId, 0);
const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, boardNode.rootId, 0);
return columnBoard.context;
}

private async getSchoolIdForBoardContext(context: BoardExternalReference): Promise<EntityId> {
if (context.type === BoardExternalReferenceType.Course) {
const course = await this.courseService.findById(context.id);

Expand All @@ -35,4 +52,47 @@ export class BoardContextApiHelperService {
/* istanbul ignore next */
throw new Error(`Unsupported board reference type ${context.type as string}`);
}

private async getFeaturesForBoardContext(context: BoardExternalReference): Promise<BoardFeature[]> {
const features: BoardFeature[] = [];

if (context.type === BoardExternalReferenceType.Course) {
const course = await this.courseService.findById(context.id);

if (
this.isVideoConferenceEnabledForCourse(course.features) ||
(await this.isVideoConferenceEnabledForSchool(course.school.id)) ||
uidp marked this conversation as resolved.
Show resolved Hide resolved
this.isVideoConferenceEnabledForConfig()
) {
features.push(BoardFeature.VIDEOCONFERENCE);
}

return features;
}

if (context.type === BoardExternalReferenceType.Room) {
uidp marked this conversation as resolved.
Show resolved Hide resolved
const room = await this.roomService.getSingleRoom(context.id);

if ((await this.isVideoConferenceEnabledForSchool(room.schoolId)) || this.isVideoConferenceEnabledForConfig()) {
uidp marked this conversation as resolved.
Show resolved Hide resolved
features.push(BoardFeature.VIDEOCONFERENCE);
}

return features;
}

/* istanbul ignore next */
throw new Error(`Unsupported board reference type ${context.type as string}`);
uidp marked this conversation as resolved.
Show resolved Hide resolved
}

private isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): boolean {
return (courseFeatures ?? []).includes(CourseFeatures.VIDEOCONFERENCE);
}

private isVideoConferenceEnabledForSchool(schoolId: EntityId): Promise<boolean> {
return this.legacySchoolService.hasFeature(schoolId, SchoolFeature.VIDEOCONFERENCE);
}

private isVideoConferenceEnabledForConfig(): boolean {
return this.configService.get('FEATURE_VIDEOCONFERENCE_ENABLED');
}
}
10 changes: 9 additions & 1 deletion apps/server/src/modules/board/board-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ import {
import { BoardNodePermissionService } from './service';
import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc';
import { RoomModule } from '../room';
import { BoardContextApiHelperModule } from '../board-context';

@Module({
imports: [BoardModule, LoggerModule, RoomMembershipModule, RoomModule, forwardRef(() => AuthorizationModule)],
imports: [
BoardModule,
LoggerModule,
RoomMembershipModule,
RoomModule,
forwardRef(() => AuthorizationModule),
BoardContextApiHelperModule,
],
controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController],
providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { ServerTestModule } from '@modules/server/server.module';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing';
import {
cleanupCollections,
groupEntityFactory,
roleFactory,
schoolEntityFactory,
TestApiClient,
userFactory,
} from '@shared/testing';

import { Permission, RoleName } from '@shared/domain/interface';
import { accountFactory } from '@src/modules/account/testing';
Expand Down Expand Up @@ -60,7 +67,8 @@ describe(`board lookup in room (api)`, () => {
],
});

const room = roomEntityFactory.buildWithId();
const school = schoolEntityFactory.buildWithId();
const room = roomEntityFactory.buildWithId({ schoolId: school.id });

const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id });

Expand All @@ -76,6 +84,7 @@ describe(`board lookup in room (api)`, () => {
userGroup,
room,
roomMembership,
school,
]);

const columnBoardNode = columnBoardEntityFactory.build({
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/modules/board/controller/board.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ export class BoardController {
@Param() urlParams: BoardUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<BoardResponse> {
const board = await this.boardUc.findBoard(currentUser.userId, urlParams.boardId);
const { board, features } = await this.boardUc.findBoard(currentUser.userId, urlParams.boardId);

const response = BoardResponseMapper.mapToResponse(board);
const response = BoardResponseMapper.mapToResponse(board, features);

return response;
}
Expand Down
Loading
Loading