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,12 +2,17 @@ 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 } from '@testing/factory/course.factory';
import { schoolEntityFactory } from '@testing/factory/school-entity.factory';
import { setupEntities } from '@testing/setup-entities';
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 @@ -16,6 +21,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 @@ -34,13 +41,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 @@ -55,16 +72,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 @@ -89,4 +104,151 @@ 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', () => {
describe('and video conference is enabled for school and config', () => {
Copy link
Contributor

@virgilchiriac virgilchiriac Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many nested describe is also a bit confusing. Better having them separated with their own setup

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 and config', () => {
it('should return video conference feature', async () => {
const { boardNode } = setup();

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

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

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

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

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

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

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

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

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

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

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

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
@@ -1,26 +1,43 @@
import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, ColumnBoard } from '@modules/board';
import { CourseService } from '@modules/learnroom';
import { RoomService } from '@modules/room';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain/types';
import { BadRequestException, Injectable } from '@nestjs/common';
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.isVideoConferenceEnabledForConfig() &&
(await this.isVideoConferenceEnabledForSchool(course.school.id)) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

silly observation, but await inside if statement shoudl be avoided

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will keep an eye on that. Should work for now I think.

this.isVideoConferenceEnabledForCourse(course.features)
) {
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 (this.isVideoConferenceEnabledForConfig() && (await this.isVideoConferenceEnabledForSchool(room.schoolId))) {
features.push(BoardFeature.VIDEOCONFERENCE);
}

return features;
}

/* istanbul ignore next */
throw new BadRequestException(`Unsupported board reference type ${context.type as string}`);
}

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
2 changes: 2 additions & 0 deletions apps/server/src/modules/board/board-ws-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MetricsService } from './metrics/metrics.service';
import { BoardNodePermissionService } from './service';
import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc';
import { RoomModule } from '../room';
import { BoardContextApiHelperModule } from '../board-context';

@Module({
imports: [
Expand All @@ -19,6 +20,7 @@ import { RoomModule } from '../room';
UserModule,
RoomMembershipModule,
RoomModule,
BoardContextApiHelperModule,
],
providers: [
BoardCollaborationGateway,
Expand Down
Loading
Loading