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-8193 - Expanding and registering BBB #5354

Merged
merged 32 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0095fc
implementing board videoconf in VC service with tests
MartinSchuhmacher Nov 25, 2024
0ddbb76
recent state for implementing board authorization in VC
MartinSchuhmacher Nov 25, 2024
777e81b
implementing VC element and room + VC element scope
MartinSchuhmacher Nov 26, 2024
1e2e125
adjusting VC scope to board element
MartinSchuhmacher Nov 27, 2024
8bd7dba
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Nov 27, 2024
23be7be
adding VC response and node
MartinSchuhmacher Nov 28, 2024
2a5dd3c
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 2, 2024
7ffc545
adding more tests
MartinSchuhmacher Dec 3, 2024
e110ae3
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 3, 2024
633d4af
adjusting VC service
MartinSchuhmacher Dec 4, 2024
0dea59c
adding api tests for VC scopes
MartinSchuhmacher Dec 4, 2024
232ab4e
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 4, 2024
d8b752a
fixing rename issues
MartinSchuhmacher Dec 4, 2024
b27c068
adding unit tests for coverage
MartinSchuhmacher Dec 5, 2024
647dbd2
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 5, 2024
802ad7a
add school id indexes
uidp Dec 5, 2024
ab4ea5a
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 6, 2024
48c38b7
adding yellow to room color selector
MartinSchuhmacher Dec 10, 2024
d42ec95
repositioning of yellow color in room color picker
MartinSchuhmacher Dec 10, 2024
1d536ed
adjusting after review feedback
MartinSchuhmacher Dec 10, 2024
dc7c768
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 10, 2024
66b7a98
removing url property from VC element
MartinSchuhmacher Dec 11, 2024
d830df8
implementing review feedback
MartinSchuhmacher Dec 11, 2024
07a775e
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 11, 2024
b20cc2d
fixing merge conflicts
MartinSchuhmacher Dec 11, 2024
9c7ed6e
adding more tests for coverage
MartinSchuhmacher Dec 13, 2024
013c5f6
seting type alias for better maintainability
MartinSchuhmacher Dec 13, 2024
7b305ce
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 13, 2024
594e25e
implementing FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED for FE usage
MartinSchuhmacher Dec 13, 2024
59c42f5
adding missing dev activation for FE env
MartinSchuhmacher Dec 13, 2024
b177a9c
fixing typo
MartinSchuhmacher Dec 13, 2024
1c69034
Merge branch 'main' into BC-8193-start-bbb
MartinSchuhmacher Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { courseFactory, roleFactory, setupEntities, userDoFactory, userFactory }
import { teamFactory } from '@shared/testing/factory/team.factory';
import { teamUserFactory } from '@shared/testing/factory/teamuser.factory';
import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory';
import { BoardNodeAuthorizableService, BoardRoles, ColumnBoard, ColumnBoardService } from '@src/modules/board';
import { boardNodeAuthorizableFactory, columnBoardFactory } from '@src/modules/board/testing';
import { BBBRole } from '../bbb';
import { ErrorStatus } from '../error';
import { VideoConferenceOptions } from '../interface';
Expand All @@ -27,6 +29,8 @@ import { VideoConferenceService } from './video-conference.service';

describe(VideoConferenceService.name, () => {
let service: DeepMocked<VideoConferenceService>;
let boardNodeAuthorizableService: DeepMocked<BoardNodeAuthorizableService>;
let columnBoardService: DeepMocked<ColumnBoardService>;
let courseService: DeepMocked<CourseService>;
let calendarService: DeepMocked<CalendarService>;
let authorizationService: DeepMocked<AuthorizationService>;
Expand All @@ -40,10 +44,18 @@ describe(VideoConferenceService.name, () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
VideoConferenceService,
{
provide: BoardNodeAuthorizableService,
useValue: createMock<BoardNodeAuthorizableService>(),
},
{
provide: ConfigService,
useValue: createMock<ConfigService<VideoConferenceConfig, true>>(),
},
{
provide: ColumnBoardService,
useValue: createMock<ColumnBoardService>(),
},
{
provide: CourseService,
useValue: createMock<CourseService>(),
Expand Down Expand Up @@ -76,6 +88,8 @@ describe(VideoConferenceService.name, () => {
}).compile();

service = module.get(VideoConferenceService);
boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService);
columnBoardService = module.get(ColumnBoardService);
courseService = module.get(CourseService);
calendarService = module.get(CalendarService);
authorizationService = module.get(AuthorizationService);
Expand Down Expand Up @@ -324,6 +338,51 @@ describe(VideoConferenceService.name, () => {
});

describe('checkPermission', () => {
describe('when user has START_MEETING permission and is in board scope', () => {
const setup = () => {
const user = userFactory.buildWithId();
const usersWithRoles = [
{
userId: user.id,
roles: [BoardRoles.EDITOR],
},
];
const entity = columnBoardFactory.build();
const conferenceScope = VideoConferenceScope.BOARD;

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(
boardNodeAuthorizableFactory.build({ users: usersWithRoles })
);
columnBoardService.findById.mockResolvedValueOnce(entity);

return {
user,
userId: user.id,
usersWithRoles,
entity,
entityId: entity.id,
conferenceScope,
};
};

it('should call the correct authorization order', async () => {
const { entity, userId, conferenceScope, entityId } = setup();

await service.determineBbbRole(userId, entityId, conferenceScope);

expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledWith(entity);
});

it('should return BBBRole.MODERATOR', async () => {
const { userId, conferenceScope, entityId } = setup();

const result = await service.determineBbbRole(userId, entityId, conferenceScope);

expect(result).toBe(BBBRole.MODERATOR);
});
});

describe('when user has START_MEETING permission and is in course scope', () => {
const setup = () => {
const user = userFactory.buildWithId();
Expand Down Expand Up @@ -553,6 +612,24 @@ describe(VideoConferenceService.name, () => {
};
};

describe('when conference scope is VideoConferenceScope.BOARD', () => {
it('should return scope information for a board', async () => {
const { userId, scopeId } = setup();
const board: ColumnBoard = columnBoardFactory.build({ id: scopeId, title: 'Board' });
columnBoardService.findById.mockResolvedValue(board);

const result: ScopeInfo = await service.getScopeInfo(userId, scopeId, VideoConferenceScope.BOARD);

expect(result).toEqual({
scopeId,
scopeName: 'boards',
logoutUrl: `${service.hostUrl}/boards/${scopeId}`,
title: board.title,
});
expect(columnBoardService.findById).toHaveBeenCalledWith(scopeId);
});
});

describe('when conference scope is VideoConferenceScope.COURSE', () => {
it('should return scope information for a course', async () => {
const { userId, conferenceScope, scopeId } = setup();
Expand Down Expand Up @@ -602,7 +679,7 @@ describe(VideoConferenceService.name, () => {
});
});

describe('getUserRoleAndGuestStatusByUserId', () => {
describe('getUserRoleAndGuestStatusByUserIdForBbb', () => {
const setup = (conferenceScope: VideoConferenceScope) => {
const user: UserDO = userDoFactory.buildWithId();
const userId = user.id as EntityId;
Expand All @@ -622,6 +699,52 @@ describe(VideoConferenceService.name, () => {
};
};

/* describe('when conference scope is VideoConferenceScope.BOARD', () => {
it('should call columnBoardService.findById', async () => {
const user = userFactory.buildWithId();
const usersWithRoles = [
{
userId: user.id,
roles: [BoardRoles.EDITOR],
},
];
const entity = columnBoardFactory.build();
const conferenceScope = VideoConferenceScope.BOARD;
const scopeId = new ObjectId().toHexString();

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(
boardNodeAuthorizableFactory.build({ users: usersWithRoles })
);
columnBoardService.findById.mockResolvedValueOnce(entity);

configService.get.mockReturnValue('https://api.example.com');

await service.getUserRoleAndGuestStatusByUserIdForBbb(user.id, scopeId, conferenceScope);

expect(columnBoardService.findById).toHaveBeenCalledWith(scopeId);
});

it('should call userService.findById', async () => {
const { user, userId, conferenceScope, scopeId } = setup(VideoConferenceScope.BOARD);
userService.findById.mockResolvedValue(user);

await service.getUserRoleAndGuestStatusByUserIdForBbb(userId, scopeId, conferenceScope);

expect(userService.findById).toHaveBeenCalledWith(userId);
});

it('should return the user role and guest status for a board conference', async () => {
const { user, userId, conferenceScope, scopeId } = setup(VideoConferenceScope.BOARD);
columnBoardService.findById.mockResolvedValue(columnBoardFactory.build({ title: 'Board' }));
userService.findById.mockResolvedValue(user);

const result = await service.getUserRoleAndGuestStatusByUserIdForBbb(userId, scopeId, conferenceScope);

expect(result).toEqual({ role: BBBRole.MODERATOR, isGuest: false });
});
}); */

describe('when conference scope is VideoConferenceScope.COURSE', () => {
it('should call courseRepo.findById', async () => {
const { user, userId, conferenceScope, scopeId } = setup(VideoConferenceScope.COURSE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Course, TeamEntity, TeamUserEntity, User } from '@shared/domain/entity'
import { Permission, RoleName, VideoConferenceScope } from '@shared/domain/interface';
import { EntityId, SchoolFeature } from '@shared/domain/types';
import { TeamsRepo, VideoConferenceRepo } from '@shared/repo';
import { BoardNodeAuthorizableService, BoardRoles, ColumnBoard, ColumnBoardService } from '@src/modules/board';
import { BBBRole } from '../bbb';
import { ErrorStatus } from '../error';
import { VideoConferenceOptions } from '../interface';
Expand All @@ -19,6 +20,8 @@ import { VideoConferenceConfig } from '../video-conference-config';
@Injectable()
export class VideoConferenceService {
constructor(
private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService,
private readonly columnBoardService: ColumnBoardService,
private readonly configService: ConfigService<VideoConferenceConfig, true>,
private readonly courseService: CourseService,
private readonly calendarService: CalendarService,
Expand Down Expand Up @@ -51,6 +54,8 @@ export class VideoConferenceService {
): Promise<boolean> {
let isExpert = false;
switch (conferenceScope) {
// called "falling through" in switch case
case VideoConferenceScope.BOARD:
case VideoConferenceScope.COURSE: {
const user: UserDO = await this.userService.findById(userId);
isExpert = this.existsOnlyExpertRole(user.roles);
Expand Down Expand Up @@ -91,13 +96,15 @@ export class VideoConferenceService {
private async loadScopeRessources(
scopeId: EntityId,
scope: VideoConferenceScope
): Promise<Course | TeamEntity | null> {
let scopeRessource: Course | TeamEntity | null = null;
): Promise<ColumnBoard | Course | TeamEntity | null> {
let scopeRessource: ColumnBoard | Course | TeamEntity | null = null;

if (scope === VideoConferenceScope.COURSE) {
scopeRessource = await this.courseService.findById(scopeId);
} else if (scope === VideoConferenceScope.EVENT) {
scopeRessource = await this.teamsRepo.findById(scopeId);
} else if (scope === VideoConferenceScope.BOARD) {
scopeRessource = await this.columnBoardService.findById(scopeId);
MartinSchuhmacher marked this conversation as resolved.
Show resolved Hide resolved
} else {
// Need to be solve the null with throw by it self.
}
Expand All @@ -109,14 +116,40 @@ export class VideoConferenceService {
return !value;
}

private hasStartMeetingAndCanRead(authorizableUser: User, entity: Course | TeamEntity): boolean {
private async hasStartMeetingAndCanRead(
authorizableUser: User,
entity: ColumnBoard | Course | TeamEntity
): Promise<boolean> {
if (entity instanceof ColumnBoard) {
const boardDoAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(entity);
const boardAuthorisedUser = boardDoAuthorizable.users.find((user) => user.userId === authorizableUser.id);

if (boardAuthorisedUser) {
return boardAuthorisedUser?.roles.includes(BoardRoles.EDITOR);
}

return false;
}
const context = AuthorizationContextBuilder.read([Permission.START_MEETING]);
const hasPermission = this.authorizationService.hasPermission(authorizableUser, entity, context);

return hasPermission;
}

private hasJoinMeetingAndCanRead(authorizableUser: User, entity: Course | TeamEntity): boolean {
private async hasJoinMeetingAndCanRead(
authorizableUser: User,
entity: ColumnBoard | Course | TeamEntity
): Promise<boolean> {
if (entity instanceof ColumnBoard) {
const boardDoAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(entity);
const boardAuthorisedUser = boardDoAuthorizable.users.find((user) => user.userId === authorizableUser.id);

if (boardAuthorisedUser) {
return boardAuthorisedUser?.roles.includes(BoardRoles.READER);
}

return false;
}
const context = AuthorizationContextBuilder.read([Permission.JOIN_MEETING]);
const hasPermission = this.authorizationService.hasPermission(authorizableUser, entity, context);

Expand All @@ -125,16 +158,16 @@ export class VideoConferenceService {

async determineBbbRole(userId: EntityId, scopeId: EntityId, scope: VideoConferenceScope): Promise<BBBRole> {
// ressource loading need to be move to uc
const [authorizableUser, scopeRessource]: [User, TeamEntity | Course | null] = await Promise.all([
const [authorizableUser, scopeRessource]: [User, TeamEntity | Course | ColumnBoard | null] = await Promise.all([
this.authorizationService.getUserWithPermissions(userId),
this.loadScopeRessources(scopeId, scope),
]);

if (!this.isNullOrUndefined(scopeRessource)) {
if (this.hasStartMeetingAndCanRead(authorizableUser, scopeRessource)) {
if (await this.hasStartMeetingAndCanRead(authorizableUser, scopeRessource)) {
return BBBRole.MODERATOR;
}
if (this.hasJoinMeetingAndCanRead(authorizableUser, scopeRessource)) {
if (await this.hasJoinMeetingAndCanRead(authorizableUser, scopeRessource)) {
return BBBRole.VIEWER;
}
}
Expand Down Expand Up @@ -162,6 +195,16 @@ export class VideoConferenceService {

public async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise<ScopeInfo> {
switch (scope) {
case VideoConferenceScope.BOARD: {
const board: ColumnBoard = await this.columnBoardService.findById(scopeId);

return {
scopeId,
scopeName: 'boards',
logoutUrl: `${this.hostUrl}/boards/${scopeId}`,
title: board.title,
};
}
case VideoConferenceScope.COURSE: {
const course: Course = await this.courseService.findById(scopeId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Module } from '@nestjs/common';
import { TeamsRepo } from '@shared/repo';
import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo';
import { LoggerModule } from '@src/core/logger';
import { BoardModule } from '../board';
import { LearnroomModule } from '../learnroom';
import { BBBService } from './bbb';
import { VideoConferenceDeprecatedController } from './controller';
Expand All @@ -18,6 +19,7 @@ import { VideoConferenceDeprecatedUc } from './uc';
imports: [
AuthorizationModule,
AuthorizationReferenceModule, // can be removed wenn video-conference-deprecated is removed
BoardModule,
CalendarModule,
HttpModule,
LegacySchoolModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum VideoConferenceScope {
BOARD = 'board',
MartinSchuhmacher marked this conversation as resolved.
Show resolved Hide resolved
COURSE = 'course',
EVENT = 'event',
}
Loading