diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index 43110f49c..5b8d6f7ff 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -22,6 +22,7 @@ import { MeController } from './me.controller'; import { HistoryEntry } from '../../../history/history-entry.entity'; import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; +import { Group } from '../../../groups/group.entity'; describe('Me Controller', () => { let controller: MeController; @@ -53,6 +54,8 @@ describe('Me Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); controller = module.get(MeController); diff --git a/src/api/public/media/media.controller.spec.ts b/src/api/public/media/media.controller.spec.ts index 7da3ca771..5e99c2b5f 100644 --- a/src/api/public/media/media.controller.spec.ts +++ b/src/api/public/media/media.controller.spec.ts @@ -24,6 +24,7 @@ import { User } from '../../../users/user.entity'; import { MediaController } from './media.controller'; import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; +import { Group } from '../../../groups/group.entity'; describe('Media Controller', () => { let controller: MediaController; @@ -63,6 +64,8 @@ describe('Media Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); controller = module.get(MediaController); diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index 18f28a708..187e5ef1b 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -24,6 +24,8 @@ import { HistoryModule } from '../../../history/history.module'; import { HistoryEntry } from '../../../history/history-entry.entity'; import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; +import { Group } from '../../../groups/group.entity'; +import { GroupsModule } from '../../../groups/groups.module'; describe('Notes Controller', () => { let controller: NotesController; @@ -45,6 +47,7 @@ describe('Notes Controller', () => { imports: [ RevisionsModule, UsersModule, + GroupsModule, LoggerModule, PermissionsModule, HistoryModule, @@ -74,6 +77,8 @@ describe('Notes Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); controller = module.get(NotesController); diff --git a/src/groups/group-info.dto.ts b/src/groups/group-info.dto.ts new file mode 100644 index 000000000..40ea64cb1 --- /dev/null +++ b/src/groups/group-info.dto.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { IsBoolean, IsString } from 'class-validator'; + +export class GroupInfoDto { + /** + * Name of the group + * @example "superheroes" + */ + @IsString() + name: string; + + /** + * Display name of this group + * @example "Superheroes" + */ + @IsString() + displayName: string; + + /** + * True if this group must be specially handled + * Used for e.g. "everybody", "all logged in users" + * @example false + */ + @IsBoolean() + special: boolean; +} diff --git a/src/groups/group.entity.ts b/src/groups/group.entity.ts index 0574e243e..f3cdc6ad4 100644 --- a/src/groups/group.entity.ts +++ b/src/groups/group.entity.ts @@ -40,4 +40,15 @@ export class Group { }) @JoinTable() members: User[]; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static create(name: string, displayName: string): Group { + const newGroup = new Group(); + newGroup.special = false; // this attribute should only be true for the two special groups + newGroup.name = name; + newGroup.displayName = displayName; + return newGroup; + } } diff --git a/src/groups/groups.module.ts b/src/groups/groups.module.ts index 7f3fa62a6..d4d6c2932 100644 --- a/src/groups/groups.module.ts +++ b/src/groups/groups.module.ts @@ -7,8 +7,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Group } from './group.entity'; +import { GroupsService } from './groups.service'; +import { LoggerModule } from '../logger/logger.module'; @Module({ - imports: [TypeOrmModule.forFeature([Group])], + imports: [TypeOrmModule.forFeature([Group]), LoggerModule], + providers: [GroupsService], + exports: [GroupsService], }) export class GroupsModule {} diff --git a/src/groups/groups.service.spec.ts b/src/groups/groups.service.spec.ts new file mode 100644 index 000000000..827d2d1f0 --- /dev/null +++ b/src/groups/groups.service.spec.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { GroupsService } from './groups.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Group } from './group.entity'; +import { NotInDBError } from '../errors/errors'; +import { LoggerModule } from '../logger/logger.module'; + +describe('GroupsService', () => { + let service: GroupsService; + let groupRepo: Repository; + let group: Group; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GroupsService, + { + provide: getRepositoryToken(Group), + useClass: Repository, + }, + ], + imports: [LoggerModule], + }).compile(); + + service = module.get(GroupsService); + groupRepo = module.get>(getRepositoryToken(Group)); + group = Group.create('testGroup', 'Superheros') as Group; + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getGroupByName', () => { + it('works', async () => { + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const foundGroup = await service.getGroupByName(group.name); + expect(foundGroup.name).toEqual(group.name); + expect(foundGroup.displayName).toEqual(group.displayName); + expect(foundGroup.special).toEqual(group.special); + }); + it('fails with non-existing group', async () => { + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(undefined); + try { + await service.getGroupByName('i_dont_exist'); + } catch (e) { + expect(e).toBeInstanceOf(NotInDBError); + } + }); + }); + + describe('toGroupDto', () => { + it('works', () => { + const groupDto = service.toGroupDto(group); + expect(groupDto.displayName).toEqual(group.displayName); + expect(groupDto.name).toEqual(group.name); + expect(groupDto.special).toBeFalsy(); + }); + it('fails with null parameter', () => { + const groupDto = service.toGroupDto(null); + expect(groupDto).toBeNull(); + }); + it('fails with undefined parameter', () => { + const groupDto = service.toGroupDto(undefined); + expect(groupDto).toBeNull(); + }); + }); +}); diff --git a/src/groups/groups.service.ts b/src/groups/groups.service.ts new file mode 100644 index 000000000..042ef0996 --- /dev/null +++ b/src/groups/groups.service.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { ConsoleLoggerService } from '../logger/console-logger.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Group } from './group.entity'; +import { NotInDBError } from '../errors/errors'; +import { GroupInfoDto } from './group-info.dto'; + +@Injectable() +export class GroupsService { + constructor( + private readonly logger: ConsoleLoggerService, + @InjectRepository(Group) private groupRepository: Repository, + ) { + this.logger.setContext(GroupsService.name); + } + + /** + * @async + * Get a group by their name. + * @param {string} name - the groups name + * @return {Group} the group + * @throws {NotInDBError} there is no group with this name + */ + async getGroupByName(name: string): Promise { + const group = await this.groupRepository.findOne({ + where: { name: name }, + }); + if (group === undefined) { + throw new NotInDBError(`Group with name '${name}' not found`); + } + return group; + } + + /** + * Build GroupInfoDto from a group. + * @param {Group} group - the group to use + * @return {GroupInfoDto} the built GroupInfoDto + */ + toGroupDto(group: Group | null | undefined): GroupInfoDto | null { + if (!group) { + this.logger.warn(`Recieved ${group} argument!`, 'toGroupDto'); + return null; + } + return { + name: group.name, + displayName: group.displayName, + special: group.special, + }; + } +} diff --git a/src/history/history.service.spec.ts b/src/history/history.service.spec.ts index caa177c36..af4613dc1 100644 --- a/src/history/history.service.spec.ts +++ b/src/history/history.service.spec.ts @@ -23,6 +23,7 @@ import { Repository } from 'typeorm'; import { NotInDBError } from '../errors/errors'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { Group } from '../groups/group.entity'; describe('HistoryService', () => { let service: HistoryService; @@ -60,6 +61,8 @@ describe('HistoryService', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); service = module.get(HistoryService); diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index 9c2c0535e..8773be3da 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -27,6 +27,7 @@ import { promises as fs } from 'fs'; import { ClientError, NotInDBError, PermissionError } from '../errors/errors'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { Group } from '../groups/group.entity'; describe('MediaService', () => { let service: MediaService; @@ -78,6 +79,8 @@ describe('MediaService', () => { .useValue({}) .overrideProvider(getRepositoryToken(MediaUpload)) .useClass(Repository) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); service = module.get(MediaService); diff --git a/src/notes/note-permissions.dto.ts b/src/notes/note-permissions.dto.ts index 55bb1c7b9..42da78129 100644 --- a/src/notes/note-permissions.dto.ts +++ b/src/notes/note-permissions.dto.ts @@ -6,6 +6,7 @@ import { IsArray, IsBoolean, IsString, ValidateNested } from 'class-validator'; import { UserInfoDto } from '../users/user-info.dto'; +import { GroupInfoDto } from '../groups/group-info.dto'; export class NoteUserPermissionEntryDto { /** @@ -38,30 +39,6 @@ export class NoteUserPermissionUpdateDto { canEdit: boolean; } -export class GroupInfoDto { - /** - * Name of the group - * @example "superheroes" - */ - @IsString() - name: string; - - /** - * Display name of this group - * @example "Superheroes" - */ - @IsString() - displayName: string; - - /** - * True if this group must be specially handled - * Used for e.g. "everybody", "all logged in users" - * @example false - */ - @IsBoolean() - special: boolean; -} - export class NoteGroupPermissionEntryDto { /** * Group this permission applies to diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index cba204cf6..609489ac3 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -15,6 +15,7 @@ import { NotesService } from './notes.service'; import { Tag } from './tag.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { GroupsModule } from '../groups/groups.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { NoteUserPermission } from '../permissions/note-user-permission.entity'; ]), forwardRef(() => RevisionsModule), UsersModule, + GroupsModule, LoggerModule, ], controllers: [], diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index d08cc8f03..12a596ef7 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -17,9 +17,12 @@ import { UsersModule } from '../users/users.module'; import { AuthorColor } from './author-color.entity'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; +import { Repository } from 'typeorm'; import { Tag } from './tag.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { GroupsModule } from '../groups/groups.module'; +import { Group } from '../groups/group.entity'; describe('NotesService', () => { let service: NotesService; @@ -37,7 +40,7 @@ describe('NotesService', () => { useValue: {}, }, ], - imports: [UsersModule, RevisionsModule, LoggerModule], + imports: [LoggerModule, UsersModule, GroupsModule, RevisionsModule], }) .overrideProvider(getRepositoryToken(User)) .useValue({}) @@ -59,7 +62,10 @@ describe('NotesService', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useClass(Repository) .compile(); + service = module.get(NotesService); }); diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index 6bc0fca7d..d05a2159b 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -22,6 +22,9 @@ import { NoteDto } from './note.dto'; import { Note } from './note.entity'; import { Tag } from './tag.entity'; import { HistoryEntry } from '../history/history-entry.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { GroupsService } from '../groups/groups.service'; @Injectable() export class NotesService { @@ -30,6 +33,7 @@ export class NotesService { @InjectRepository(Note) private noteRepository: Repository, @InjectRepository(Tag) private tagRepository: Repository, @Inject(UsersService) private usersService: UsersService, + @Inject(GroupsService) private groupsService: GroupsService, @Inject(forwardRef(() => RevisionsService)) private revisionsService: RevisionsService, ) { diff --git a/src/permissions/permissions.service.spec.ts b/src/permissions/permissions.service.spec.ts index da27e14a4..6396eb09c 100644 --- a/src/permissions/permissions.service.spec.ts +++ b/src/permissions/permissions.service.spec.ts @@ -51,6 +51,8 @@ describe('PermissionsService', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); permissionsService = module.get(PermissionsService); }); @@ -241,33 +243,27 @@ describe('PermissionsService', () => { function createGroups(): { [id: string]: Group } { const result: { [id: string]: Group } = {}; - const everybody: Group = new Group(); + const everybody: Group = Group.create('everybody', 'Everybody'); everybody.special = true; - everybody.name = 'everybody'; result['everybody'] = everybody; - const loggedIn = new Group(); + const loggedIn = Group.create('loggedIn', 'loggedIn'); loggedIn.special = true; - loggedIn.name = 'loggedIn'; result['loggedIn'] = loggedIn; - const user1group = new Group(); - user1group.name = 'user1group'; + const user1group = Group.create('user1group', 'user1group'); user1group.members = [user1]; result['user1group'] = user1group; - const user2group = new Group(); - user2group.name = 'user2group'; + const user2group = Group.create('user2group', 'user2group'); user2group.members = [user2]; result['user2group'] = user2group; - const user1and2group = new Group(); - user1and2group.name = 'user1and2group'; + const user1and2group = Group.create('user1and2group', 'user1and2group'); user1and2group.members = [user1, user2]; result['user1and2group'] = user1and2group; - const user2and1group = new Group(); - user2and1group.name = 'user2and1group'; + const user2and1group = Group.create('user2and1group', 'user2and1group'); user2and1group.members = [user2, user1]; result['user2and1group'] = user2and1group; diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index 43799ae0d..fe82a346f 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -19,6 +19,7 @@ import { RevisionsService } from './revisions.service'; import { Tag } from '../notes/tag.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity'; +import { Group } from '../groups/group.entity'; describe('RevisionsService', () => { let service: RevisionsService; @@ -54,6 +55,8 @@ describe('RevisionsService', () => { .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) + .overrideProvider(getRepositoryToken(Group)) + .useValue({}) .compile(); service = module.get(RevisionsService);