Merge pull request #848 from hedgedoc/feature/noteService

This commit is contained in:
David Mehren 2021-02-20 21:46:17 +01:00 committed by GitHub
commit 3f05fa4852
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1273 additions and 157 deletions

View file

@ -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>(MeController);

View file

@ -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>(MediaController);

View file

@ -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>(NotesController);

View file

@ -5,6 +5,7 @@
*/
import {
BadRequestException,
Body,
Controller,
Delete,
@ -18,7 +19,11 @@ import {
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { NotInDBError } from '../../../errors/errors';
import {
AlreadyInDBError,
NotInDBError,
PermissionsUpdateInconsistentError,
} from '../../../errors/errors';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import {
NotePermissionsDto,
@ -100,9 +105,16 @@ export class NotesController {
throw new UnauthorizedException('Creating note denied!');
}
this.logger.debug('Got raw markdown:\n' + text, 'createNamedNote');
return this.noteService.toNoteDto(
await this.noteService.createNote(text, noteAlias, req.user),
);
try {
return this.noteService.toNoteDto(
await this.noteService.createNote(text, noteAlias, req.user),
);
} catch (e) {
if (e instanceof AlreadyInDBError) {
throw new BadRequestException(e.message);
}
throw e;
}
}
@UseGuards(TokenAuthGuard)
@ -117,7 +129,7 @@ export class NotesController {
throw new UnauthorizedException('Deleting note denied!');
}
this.logger.debug('Deleting note: ' + noteIdOrAlias, 'deleteNote');
await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias);
await this.noteService.deleteNote(note);
this.logger.debug('Successfully deleted ' + noteIdOrAlias, 'deleteNote');
return;
} catch (e) {
@ -142,7 +154,7 @@ export class NotesController {
}
this.logger.debug('Got raw markdown:\n' + text, 'updateNote');
return this.noteService.toNoteDto(
await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text),
await this.noteService.updateNote(note, text),
);
} catch (e) {
if (e instanceof NotInDBError) {
@ -164,7 +176,7 @@ export class NotesController {
if (!this.permissionsService.mayRead(req.user, note)) {
throw new UnauthorizedException('Reading note denied!');
}
return await this.noteService.getNoteContent(noteIdOrAlias);
return await this.noteService.getNoteContentByNote(note);
} catch (e) {
if (e instanceof NotInDBError) {
throw new NotFoundException(e.message);
@ -184,13 +196,14 @@ export class NotesController {
if (!this.permissionsService.mayRead(req.user, note)) {
throw new UnauthorizedException('Reading note denied!');
}
return this.noteService.toNoteMetadataDto(
await this.noteService.getNoteByIdOrAlias(noteIdOrAlias),
);
return this.noteService.toNoteMetadataDto(note);
} catch (e) {
if (e instanceof NotInDBError) {
throw new NotFoundException(e.message);
}
if (e instanceof PermissionsUpdateInconsistentError) {
throw new BadRequestException(e.message);
}
throw e;
}
}
@ -208,7 +221,7 @@ export class NotesController {
throw new UnauthorizedException('Updating note denied!');
}
return this.noteService.toNotePermissionsDto(
await this.noteService.updateNotePermissions(noteIdOrAlias, updateDto),
await this.noteService.updateNotePermissions(note, updateDto),
);
} catch (e) {
if (e instanceof NotInDBError) {
@ -229,9 +242,7 @@ export class NotesController {
if (!this.permissionsService.mayRead(req.user, note)) {
throw new UnauthorizedException('Reading note denied!');
}
const revisions = await this.revisionsService.getAllRevisions(
noteIdOrAlias,
);
const revisions = await this.revisionsService.getAllRevisions(note);
return Promise.all(
revisions.map((revision) =>
this.revisionsService.toRevisionMetadataDto(revision),
@ -258,7 +269,7 @@ export class NotesController {
throw new UnauthorizedException('Reading note denied!');
}
return this.revisionsService.toRevisionDto(
await this.revisionsService.getRevision(noteIdOrAlias, revisionId),
await this.revisionsService.getRevision(note, revisionId),
);
} catch (e) {
if (e instanceof NotInDBError) {

View file

@ -8,6 +8,10 @@ export class NotInDBError extends Error {
name = 'NotInDBError';
}
export class AlreadyInDBError extends Error {
name = 'AlreadyInDBError';
}
export class ClientError extends Error {
name = 'ClientError';
}
@ -23,3 +27,7 @@ export class TokenNotValidError extends Error {
export class TooManyTokensError extends Error {
name = 'TooManyTokensError';
}
export class PermissionsUpdateInconsistentError extends Error {
name = 'PermissionsUpdateInconsistentError';
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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 {}

View file

@ -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<Group>;
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>(GroupsService);
groupRepo = module.get<Repository<Group>>(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();
});
});
});

View file

@ -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<Group>,
) {
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<Group> {
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,
};
}
}

View file

@ -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>(HistoryService);

View file

@ -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>(MediaService);

View file

@ -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

View file

@ -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: [],

View file

@ -17,12 +17,27 @@ 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 {
NotInDBError,
PermissionsUpdateInconsistentError,
} from '../errors/errors';
import {
NoteGroupPermissionUpdateDto,
NoteUserPermissionUpdateDto,
} from './note-permissions.dto';
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;
let noteRepo: Repository<Note>;
let revisionRepo: Repository<Revision>;
let userRepo: Repository<User>;
let groupRepo: Repository<Group>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -30,17 +45,21 @@ describe('NotesService', () => {
NotesService,
{
provide: getRepositoryToken(Note),
useValue: {},
useClass: Repository,
},
{
provide: getRepositoryToken(Tag),
useValue: {},
useClass: Repository,
},
],
imports: [UsersModule, RevisionsModule, LoggerModule],
imports: [LoggerModule, UsersModule, GroupsModule, RevisionsModule],
})
.overrideProvider(getRepositoryToken(Note))
.useClass(Repository)
.overrideProvider(getRepositoryToken(Tag))
.useClass(Repository)
.overrideProvider(getRepositoryToken(User))
.useValue({})
.useClass(Repository)
.overrideProvider(getRepositoryToken(AuthToken))
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
@ -50,20 +69,764 @@ describe('NotesService', () => {
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Note))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.useClass(Repository)
.overrideProvider(getRepositoryToken(NoteGroupPermission))
.useValue({})
.overrideProvider(getRepositoryToken(NoteUserPermission))
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useClass(Repository)
.compile();
service = module.get<NotesService>(NotesService);
noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note));
revisionRepo = module.get<Repository<Revision>>(
getRepositoryToken(Revision),
);
userRepo = module.get<Repository<User>>(getRepositoryToken(User));
groupRepo = module.get<Repository<Group>>(getRepositoryToken(Group));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('getUserNotes', () => {
describe('works', () => {
const user = User.create('hardcoded', 'Testy') as User;
const alias = 'alias';
const note = Note.create(user, alias);
it('with no note', async () => {
jest.spyOn(noteRepo, 'find').mockResolvedValueOnce(undefined);
const notes = await service.getUserNotes(user);
expect(notes).toEqual([]);
});
it('with one note', async () => {
jest.spyOn(noteRepo, 'find').mockResolvedValueOnce([note]);
const notes = await service.getUserNotes(user);
expect(notes).toEqual([note]);
});
it('with multiple note', async () => {
jest.spyOn(noteRepo, 'find').mockResolvedValueOnce([note, note]);
const notes = await service.getUserNotes(user);
expect(notes).toEqual([note, note]);
});
});
});
describe('createNote', () => {
describe('works', () => {
const user = User.create('hardcoded', 'Testy') as User;
const alias = 'alias';
const content = 'testContent';
beforeEach(() => {
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
});
it('without alias, without owner', async () => {
const newNote = await service.createNote(content);
const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toBeUndefined();
expect(newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0);
expect(newNote.owner).toBeUndefined();
expect(newNote.alias).toBeUndefined();
});
it('without alias, with owner', async () => {
const newNote = await service.createNote(content, undefined, user);
const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(1);
expect(newNote.historyEntries[0].user).toEqual(user);
expect(newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0);
expect(newNote.owner).toEqual(user);
expect(newNote.alias).toBeUndefined();
});
it('with alias, without owner', async () => {
const newNote = await service.createNote(content, alias);
const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toBeUndefined();
expect(newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0);
expect(newNote.owner).toBeUndefined();
expect(newNote.alias).toEqual(alias);
});
it('with alias, with owner', async () => {
const newNote = await service.createNote(content, alias, user);
const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(1);
expect(newNote.historyEntries[0].user).toEqual(user);
expect(newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0);
expect(newNote.owner).toEqual(user);
expect(newNote.alias).toEqual(alias);
});
});
});
describe('getNoteContentByNote', () => {
it('works', async () => {
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const newNote = await service.createNote(content);
const revisions = await newNote.revisions;
jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revisions[0]);
service.getNoteContentByNote(newNote).then((result) => {
expect(result).toEqual(content);
});
});
});
describe('getLatestRevision', () => {
it('works', async () => {
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const newNote = await service.createNote(content);
const revisions = await newNote.revisions;
jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revisions[0]);
service.getLatestRevision(newNote).then((result) => {
expect(result).toEqual(revisions[0]);
});
});
});
describe('getFirstRevision', () => {
it('works', async () => {
const user = {} as User;
user.userName = 'hardcoded';
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const newNote = await service.createNote(content);
const revisions = await newNote.revisions;
jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revisions[0]);
service.getLatestRevision(newNote).then((result) => {
expect(result).toEqual(revisions[0]);
});
});
});
describe('getNoteByIdOrAlias', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const note = Note.create(user);
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note);
const foundNote = await service.getNoteByIdOrAlias('noteThatExists');
expect(foundNote).toEqual(note);
});
it('fails: no note found', async () => {
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined);
try {
await service.getNoteByIdOrAlias('noteThatDoesNoteExist');
} catch (e) {
expect(e).toBeInstanceOf(NotInDBError);
}
});
});
describe('deleteNote', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const note = Note.create(user);
jest
.spyOn(noteRepo, 'remove')
.mockImplementationOnce(async (entry, _) => {
expect(entry).toEqual(note);
return entry;
});
await service.deleteNote(note);
});
});
describe('updateNote', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const note = Note.create(user);
const revisionLength = (await note.revisions).length;
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
const updatedNote = await service.updateNote(note, 'newContent');
expect(await updatedNote.revisions).toHaveLength(revisionLength + 1);
});
});
describe('updateNotePermissions', () => {
const userPermissionUpdate = new NoteUserPermissionUpdateDto();
userPermissionUpdate.username = 'hardcoded';
userPermissionUpdate.canEdit = true;
const groupPermissionUpate = new NoteGroupPermissionUpdateDto();
groupPermissionUpate.groupname = 'testGroup';
groupPermissionUpate.canEdit = false;
const user = User.create(userPermissionUpdate.username, 'Testy') as User;
const group = Group.create(
groupPermissionUpate.groupname,
groupPermissionUpate.groupname,
);
const note = Note.create(user);
describe('works', () => {
it('with empty GroupPermissions and with empty UserPermissions', async () => {
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
const savedNote = await service.updateNotePermissions(note, {
sharedToUsers: [],
sharedToGroups: [],
});
expect(savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions).toHaveLength(0);
});
it('with empty GroupPermissions and with new UserPermissions', async () => {
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
const savedNote = await service.updateNotePermissions(note, {
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [],
});
expect(savedNote.userPermissions).toHaveLength(1);
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions).toHaveLength(0);
});
it('with empty GroupPermissions and with existing UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.userPermissions = [
{
note: noteWithPreexistingPermissions,
user: user,
canEdit: !userPermissionUpdate.canEdit,
},
];
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
const savedNote = await service.updateNotePermissions(note, {
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [],
});
expect(savedNote.userPermissions).toHaveLength(1);
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions).toHaveLength(0);
});
it('with new GroupPermissions and with empty UserPermissions', async () => {
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
const savedNote = await service.updateNotePermissions(note, {
sharedToUsers: [],
sharedToGroups: [groupPermissionUpate],
});
expect(savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
it('with new GroupPermissions and with new UserPermissions', async () => {
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
const savedNote = await service.updateNotePermissions(note, {
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [groupPermissionUpate],
});
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
it('with new GroupPermissions and with existing UserPermissions', async () => {
const noteWithUserPermission: Note = { ...note };
noteWithUserPermission.userPermissions = [
{
note: noteWithUserPermission,
user: user,
canEdit: !userPermissionUpdate.canEdit,
},
];
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
const savedNote = await service.updateNotePermissions(
noteWithUserPermission,
{
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [groupPermissionUpate],
},
);
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
it('with existing GroupPermissions and with empty UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [
{
note: noteWithPreexistingPermissions,
group: group,
canEdit: !groupPermissionUpate.canEdit,
},
];
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
const savedNote = await service.updateNotePermissions(
noteWithPreexistingPermissions,
{
sharedToUsers: [],
sharedToGroups: [groupPermissionUpate],
},
);
expect(savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
it('with existing GroupPermissions and with new UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [
{
note: noteWithPreexistingPermissions,
group: group,
canEdit: !groupPermissionUpate.canEdit,
},
];
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
const savedNote = await service.updateNotePermissions(
noteWithPreexistingPermissions,
{
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [groupPermissionUpate],
},
);
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
it('with existing GroupPermissions and with existing UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [
{
note: noteWithPreexistingPermissions,
group: group,
canEdit: !groupPermissionUpate.canEdit,
},
];
noteWithPreexistingPermissions.userPermissions = [
{
note: noteWithPreexistingPermissions,
user: user,
canEdit: !userPermissionUpdate.canEdit,
},
];
jest
.spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => {
return entry;
});
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
const savedNote = await service.updateNotePermissions(
noteWithPreexistingPermissions,
{
sharedToUsers: [userPermissionUpdate],
sharedToGroups: [groupPermissionUpate],
},
);
expect(savedNote.userPermissions[0].user.userName).toEqual(
userPermissionUpdate.username,
);
expect(savedNote.userPermissions[0].canEdit).toEqual(
userPermissionUpdate.canEdit,
);
expect(savedNote.groupPermissions[0].group.name).toEqual(
groupPermissionUpate.groupname,
);
expect(savedNote.groupPermissions[0].canEdit).toEqual(
groupPermissionUpate.canEdit,
);
});
});
describe('fails:', () => {
it('userPermissions has duplicate entries', async () => {
try {
await service.updateNotePermissions(note, {
sharedToUsers: [userPermissionUpdate, userPermissionUpdate],
sharedToGroups: [],
});
} catch (e) {
expect(e).toBeInstanceOf(PermissionsUpdateInconsistentError);
}
});
it('groupPermissions has duplicate entries', async () => {
try {
await service.updateNotePermissions(note, {
sharedToUsers: [],
sharedToGroups: [groupPermissionUpate, groupPermissionUpate],
});
} catch (e) {
expect(e).toBeInstanceOf(PermissionsUpdateInconsistentError);
}
});
it('userPermissions and groupPermissions have duplicate entries', async () => {
try {
await service.updateNotePermissions(note, {
sharedToUsers: [userPermissionUpdate, userPermissionUpdate],
sharedToGroups: [groupPermissionUpate, groupPermissionUpate],
});
} catch (e) {
expect(e).toBeInstanceOf(PermissionsUpdateInconsistentError);
}
});
});
});
describe('getNoteContentByIdOrAlias', () => {
it('works', async () => {
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const newNote = await service.createNote(content);
const revisions = await newNote.revisions;
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(newNote);
jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revisions[0]);
service.getNoteContentByIdOrAlias('noteThatExists').then((result) => {
expect(result).toEqual(content);
});
});
});
describe('toTagList', () => {
it('works', async () => {
const note = {} as Note;
note.tags = [
{
id: 1,
name: 'testTag',
notes: [note],
},
];
const tagList = service.toTagList(note);
expect(tagList).toHaveLength(1);
expect(tagList[0]).toEqual(note.tags[0].name);
});
});
describe('toNotePermissionsDto', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const group = Group.create('testGroup', 'testGroup') as Group;
const note = Note.create(user);
note.userPermissions = [
{
note: note,
user: user,
canEdit: true,
},
];
note.groupPermissions = [
{
note: note,
group: group,
canEdit: true,
},
];
const permissions = await service.toNotePermissionsDto(note);
expect(permissions.owner.userName).toEqual(user.userName);
expect(permissions.sharedToUsers).toHaveLength(1);
expect(permissions.sharedToUsers[0].user.userName).toEqual(user.userName);
expect(permissions.sharedToUsers[0].canEdit).toEqual(true);
expect(permissions.sharedToGroups).toHaveLength(1);
expect(permissions.sharedToGroups[0].group.displayName).toEqual(
group.displayName,
);
expect(permissions.sharedToGroups[0].canEdit).toEqual(true);
});
});
describe('toNoteMetadataDto', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const otherUser = User.create('other hardcoded', 'Testy2') as User;
const group = Group.create('testGroup', 'testGroup') as Group;
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const note = await service.createNote(content);
const revisions = await note.revisions;
revisions[0].authorships = [
{
user: otherUser,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452000),
} as Authorship,
{
user: user,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452001),
} as Authorship,
];
revisions[0].createdAt = new Date(1549312452000);
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]);
note.id = 'testId';
note.alias = 'testAlias';
note.title = 'testTitle';
note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user;
note.userPermissions = [
{
note: note,
user: user,
canEdit: true,
},
];
note.groupPermissions = [
{
note: note,
group: group,
canEdit: true,
},
];
note.tags = [
{
id: 1,
name: 'testTag',
notes: [note],
},
];
note.viewcount = 1337;
const metadataDto = await service.toNoteMetadataDto(note);
expect(metadataDto.id).toEqual(note.id);
expect(metadataDto.alias).toEqual(note.alias);
expect(metadataDto.title).toEqual(note.title);
expect(metadataDto.createTime).toEqual(revisions[0].createdAt);
expect(metadataDto.description).toEqual(note.description);
expect(metadataDto.editedBy).toHaveLength(1);
expect(metadataDto.editedBy[0]).toEqual(user.userName);
expect(metadataDto.permissions.owner.userName).toEqual(user.userName);
expect(metadataDto.permissions.sharedToUsers).toHaveLength(1);
expect(metadataDto.permissions.sharedToUsers[0].user.userName).toEqual(
user.userName,
);
expect(metadataDto.permissions.sharedToUsers[0].canEdit).toEqual(true);
expect(metadataDto.permissions.sharedToGroups).toHaveLength(1);
expect(
metadataDto.permissions.sharedToGroups[0].group.displayName,
).toEqual(group.displayName);
expect(metadataDto.permissions.sharedToGroups[0].canEdit).toEqual(true);
expect(metadataDto.tags).toHaveLength(1);
expect(metadataDto.tags[0]).toEqual(note.tags[0].name);
expect(metadataDto.updateTime).toEqual(revisions[0].createdAt);
expect(metadataDto.updateUser.userName).toEqual(user.userName);
expect(metadataDto.viewCount).toEqual(note.viewcount);
});
});
describe('toNoteDto', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const otherUser = User.create('other hardcoded', 'Testy2') as User;
otherUser.userName = 'other hardcoded user';
const group = Group.create('testGroup', 'testGroup') as Group;
const content = 'testContent';
jest
.spyOn(noteRepo, 'save')
.mockImplementation(async (note: Note): Promise<Note> => note);
const note = await service.createNote(content);
const revisions = await note.revisions;
revisions[0].authorships = [
{
user: otherUser,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452000),
} as Authorship,
{
user: user,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452001),
} as Authorship,
];
revisions[0].createdAt = new Date(1549312452000);
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(revisions[0])
.mockResolvedValue(revisions[0]);
note.id = 'testId';
note.alias = 'testAlias';
note.title = 'testTitle';
note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user;
note.userPermissions = [
{
note: note,
user: user,
canEdit: true,
},
];
note.groupPermissions = [
{
note: note,
group: group,
canEdit: true,
},
];
note.tags = [
{
id: 1,
name: 'testTag',
notes: [note],
},
];
note.viewcount = 1337;
const noteDto = await service.toNoteDto(note);
expect(noteDto.metadata.id).toEqual(note.id);
expect(noteDto.metadata.alias).toEqual(note.alias);
expect(noteDto.metadata.title).toEqual(note.title);
expect(noteDto.metadata.createTime).toEqual(revisions[0].createdAt);
expect(noteDto.metadata.description).toEqual(note.description);
expect(noteDto.metadata.editedBy).toHaveLength(1);
expect(noteDto.metadata.editedBy[0]).toEqual(user.userName);
expect(noteDto.metadata.permissions.owner.userName).toEqual(
user.userName,
);
expect(noteDto.metadata.permissions.sharedToUsers).toHaveLength(1);
expect(
noteDto.metadata.permissions.sharedToUsers[0].user.userName,
).toEqual(user.userName);
expect(noteDto.metadata.permissions.sharedToUsers[0].canEdit).toEqual(
true,
);
expect(noteDto.metadata.permissions.sharedToGroups).toHaveLength(1);
expect(
noteDto.metadata.permissions.sharedToGroups[0].group.displayName,
).toEqual(group.displayName);
expect(noteDto.metadata.permissions.sharedToGroups[0].canEdit).toEqual(
true,
);
expect(noteDto.metadata.tags).toHaveLength(1);
expect(noteDto.metadata.tags[0]).toEqual(note.tags[0].name);
expect(noteDto.metadata.updateTime).toEqual(revisions[0].createdAt);
expect(noteDto.metadata.updateUser.userName).toEqual(user.userName);
expect(noteDto.metadata.viewCount).toEqual(note.viewcount);
expect(noteDto.content).toEqual(content);
});
});
});

View file

@ -7,7 +7,11 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotInDBError } from '../errors/errors';
import {
AlreadyInDBError,
NotInDBError,
PermissionsUpdateInconsistentError,
} from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Revision } from '../revisions/revision.entity';
import { RevisionsService } from '../revisions/revisions.service';
@ -22,6 +26,10 @@ 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';
import { checkArrayForDuplicates } from '../utils/arrayDuplicatCheck';
@Injectable()
export class NotesService {
@ -30,41 +38,52 @@ export class NotesService {
@InjectRepository(Note) private noteRepository: Repository<Note>,
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
@Inject(UsersService) private usersService: UsersService,
@Inject(GroupsService) private groupsService: GroupsService,
@Inject(forwardRef(() => RevisionsService))
private revisionsService: RevisionsService,
) {
this.logger.setContext(NotesService.name);
}
getUserNotes(user: User): Note[] {
this.logger.warn('Using hardcoded data!');
return [
{
id: 'foobar-barfoo',
alias: null,
shortid: 'abc',
owner: user,
description: 'Very descriptive text.',
userPermissions: [],
groupPermissions: [],
historyEntries: [],
tags: [],
revisions: Promise.resolve([]),
authorColors: [],
title: 'Title!',
viewcount: 42,
},
];
/**
* @async
* Get all notes owned by a user.
* @param {User} user - the user who owns the notes
* @return {Note[]} arary of notes owned by the user
*/
async getUserNotes(user: User): Promise<Note[]> {
const notes = await this.noteRepository.find({
where: { owner: user },
relations: [
'owner',
'userPermissions',
'groupPermissions',
'authorColors',
'tags',
],
});
if (notes === undefined) {
return [];
}
return notes;
}
/**
* @async
* Create a new note.
* @param {string} noteContent - the content the new note should have
* @param {string=} alias - a optional alias the note should have
* @param {User=} owner - the owner of the note
* @return {Note} the newly created note
*/
async createNote(
noteContent: string,
alias?: NoteMetadataDto['alias'],
owner?: User,
): Promise<Note> {
const newNote = Note.create();
//TODO: Calculate patch
newNote.revisions = Promise.resolve([
//TODO: Calculate patch
Revision.create(noteContent, noteContent),
]);
if (alias) {
@ -74,21 +93,56 @@ export class NotesService {
newNote.historyEntries = [HistoryEntry.create(owner)];
newNote.owner = owner;
}
return this.noteRepository.save(newNote);
try {
return await this.noteRepository.save(newNote);
} catch {
this.logger.debug(
`A note with the alias '${alias}' already exists.`,
'createNote',
);
throw new AlreadyInDBError(
`A note with the alias '${alias}' already exists.`,
);
}
}
async getCurrentContent(note: Note): Promise<string> {
/**
* @async
* Get the current content of the note.
* @param {Note} note - the note to use
* @return {string} the content of the note
*/
async getNoteContentByNote(note: Note): Promise<string> {
return (await this.getLatestRevision(note)).content;
}
/**
* @async
* Get the first revision of the note.
* @param {Note} note - the note to use
* @return {Revision} the first revision of the note
*/
async getLatestRevision(note: Note): Promise<Revision> {
return this.revisionsService.getLatestRevision(note.id);
return await this.revisionsService.getLatestRevision(note.id);
}
/**
* @async
* Get the last revision of the note.
* @param {Note} note - the note to use
* @return {Revision} the last revision of the note
*/
async getFirstRevision(note: Note): Promise<Revision> {
return this.revisionsService.getFirstRevision(note.id);
return await this.revisionsService.getFirstRevision(note.id);
}
/**
* @async
* Get a note by either their id or alias.
* @param {string} noteIdOrAlias - the notes id or alias
* @return {Note} the note
* @throws {NotInDBError} there is no note with this id or alias
*/
async getNoteByIdOrAlias(noteIdOrAlias: string): Promise<Note> {
this.logger.debug(
`Trying to find note '${noteIdOrAlias}'`,
@ -108,6 +162,7 @@ export class NotesService {
'owner',
'groupPermissions',
'userPermissions',
'tags',
],
});
if (note === undefined) {
@ -123,16 +178,26 @@ export class NotesService {
return note;
}
async deleteNoteByIdOrAlias(noteIdOrAlias: string) {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
/**
* @async
* Delete a note
* @param {Note} note - the note to delete
* @return {Note} the note, that was deleted
* @throws {NotInDBError} there is no note with this id or alias
*/
async deleteNote(note: Note): Promise<Note> {
return await this.noteRepository.remove(note);
}
async updateNoteByIdOrAlias(
noteIdOrAlias: string,
noteContent: string,
): Promise<Note> {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
/**
* @async
* Update a notes content.
* @param {Note} note - the note
* @param {string} noteContent - the new content
* @return {Note} the note with a new revision and new content
* @throws {NotInDBError} there is no note with this id or alias
*/
async updateNote(note: Note, noteContent: string): Promise<Note> {
const revisions = await note.revisions;
//TODO: Calculate patch
revisions.push(Revision.create(noteContent, noteContent));
@ -140,48 +205,112 @@ export class NotesService {
return this.noteRepository.save(note);
}
updateNotePermissions(
noteIdOrAlias: string,
/**
* @async
* Update a notes permissions.
* @param {Note} note - the note
* @param {NotePermissionsUpdateDto} newPermissions - the permissions the not should be set to
* @return {Note} the note with the new permissions
* @throws {NotInDBError} there is no note with this id or alias
* @throws {PermissionsUpdateInconsistentError} the new permissions specify a user or group twice.
*/
async updateNotePermissions(
note: Note,
newPermissions: NotePermissionsUpdateDto,
): Note {
this.logger.warn('Using hardcoded data!', 'updateNotePermissions');
return {
id: 'foobar-barfoo',
alias: null,
shortid: 'abc',
owner: {
authTokens: [],
createdAt: new Date(),
displayName: 'hardcoded',
id: '1',
identities: [],
ownedNotes: [],
historyEntries: [],
updatedAt: new Date(),
userName: 'Testy',
groups: [],
},
description: 'Very descriptive text.',
userPermissions: [],
groupPermissions: [],
historyEntries: [],
tags: [],
revisions: Promise.resolve([]),
authorColors: [],
title: 'Title!',
viewcount: 42,
};
): Promise<Note> {
const users = newPermissions.sharedToUsers.map(
(userPermission) => userPermission.username,
);
const groups = newPermissions.sharedToGroups.map(
(groupPermission) => groupPermission.groupname,
);
if (checkArrayForDuplicates(users) || checkArrayForDuplicates(groups)) {
this.logger.debug(
`The PermissionUpdate requested specifies the same user or group multiple times.`,
'updateNotePermissions',
);
throw new PermissionsUpdateInconsistentError(
'The PermissionUpdate requested specifies the same user or group multiple times.',
);
}
note.userPermissions = [];
note.groupPermissions = [];
// Create new userPermissions
for (const newUserPermission of newPermissions.sharedToUsers) {
const user = await this.usersService.getUserByUsername(
newUserPermission.username,
);
const createdPermission = NoteUserPermission.create(
user,
newUserPermission.canEdit,
);
note.userPermissions.push(createdPermission);
}
// Create groupPermissions
for (const newGroupPermission of newPermissions.sharedToGroups) {
const group = await this.groupsService.getGroupByName(
newGroupPermission.groupname,
);
const createdPermission = NoteGroupPermission.create(
group,
newGroupPermission.canEdit,
);
note.groupPermissions.push(createdPermission);
}
return await this.noteRepository.save(note);
}
async getNoteContent(noteIdOrAlias: string): Promise<string> {
/**
* @async
* Get the current content of the note by either their id or alias.
* @param {string} noteIdOrAlias - the notes id or alias
* @return {string} the content of the note
*/
async getNoteContentByIdOrAlias(noteIdOrAlias: string): Promise<string> {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
return this.getCurrentContent(note);
return this.getNoteContentByNote(note);
}
/**
* @async
* Calculate the updateUser (for the NoteDto) for a Note.
* @param {Note} note - the note to use
* @return {User} user to be used as updateUser in the NoteDto
*/
async calculateUpdateUser(note: Note): Promise<User> {
const lastRevision = await this.getLatestRevision(note);
if (lastRevision && lastRevision.authorships) {
// Sort the last Revisions Authorships by their updatedAt Date to get the latest one
// the user of that Authorship is the updateUser
return lastRevision.authorships.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
)[0].user;
}
// If there are no Authorships, the owner is the updateUser
return note.owner;
}
/**
* Map the tags of a note to a string array of the tags names.
* @param {Note} note - the note to use
* @return {string[]} string array of tags names
*/
toTagList(note: Note): string[] {
return note.tags.map((tag) => tag.name);
}
/**
* @async
* Build NotePermissionsDto from a note.
* @param {Note} note - the note to use
* @return {NotePermissionsDto} the built NotePermissionDto
*/
async toNotePermissionsDto(note: Note): Promise<NotePermissionsDto> {
return {
owner: this.usersService.toUserDto(note.owner),
@ -190,12 +319,18 @@ export class NotesService {
canEdit: noteUserPermission.canEdit,
})),
sharedToGroups: note.groupPermissions.map((noteGroupPermission) => ({
group: noteGroupPermission.group,
group: this.groupsService.toGroupDto(noteGroupPermission.group),
canEdit: noteGroupPermission.canEdit,
})),
};
}
/**
* @async
* Build NoteMetadataDto from a note.
* @param {Note} note - the note to use
* @return {NoteMetadataDto} the built NoteMetadataDto
*/
async toNoteMetadataDto(note: Note): Promise<NoteMetadataDto> {
return {
// TODO: Convert DB UUID to base64
@ -207,24 +342,25 @@ export class NotesService {
editedBy: note.authorColors.map(
(authorColor) => authorColor.user.userName,
),
// TODO: Extract into method
permissions: await this.toNotePermissionsDto(note),
tags: this.toTagList(note),
updateTime: (await this.getLatestRevision(note)).createdAt,
// TODO: Get actual updateUser
updateUser: {
displayName: 'Hardcoded User',
userName: 'hardcoded',
email: 'foo@example.com',
photo: '',
},
viewCount: 42,
updateUser: this.usersService.toUserDto(
await this.calculateUpdateUser(note),
),
viewCount: note.viewcount,
};
}
/**
* @async
* Build NoteDto from a note.
* @param {Note} note - the note to use
* @return {NoteDto} the built NoteDto
*/
async toNoteDto(note: Note): Promise<NoteDto> {
return {
content: await this.getCurrentContent(note),
content: await this.getNoteContentByNote(note),
metadata: await this.toNoteMetadataDto(note),
editedByAtPosition: [],
};

View file

@ -18,4 +18,14 @@ export class NoteGroupPermission {
@Column()
canEdit: boolean;
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static create(group: Group, canEdit: boolean): NoteGroupPermission {
const groupPermission = new NoteGroupPermission();
groupPermission.group = group;
groupPermission.canEdit = canEdit;
return groupPermission;
}
}

View file

@ -18,4 +18,14 @@ export class NoteUserPermission {
@Column()
canEdit: boolean;
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static create(user: User, canEdit: boolean): NoteUserPermission {
const userPermission = new NoteUserPermission();
userPermission.user = user;
userPermission.canEdit = canEdit;
return userPermission;
}
}

View file

@ -23,11 +23,6 @@ import { Revision } from '../revisions/revision.entity';
import { Tag } from '../notes/tag.entity';
import { Group } from '../groups/group.entity';
jest.mock('../permissions/note-group-permission.entity.ts');
jest.mock('../groups/group.entity.ts');
jest.mock('../notes/note.entity.ts');
jest.mock('../users/user.entity.ts');
describe('PermissionsService', () => {
let permissionsService: PermissionsService;
@ -56,6 +51,8 @@ describe('PermissionsService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(NoteUserPermission))
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.compile();
permissionsService = module.get<PermissionsService>(PermissionsService);
});
@ -246,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;
@ -292,10 +283,7 @@ describe('PermissionsService', () => {
group: Group,
write: boolean,
): NoteGroupPermission {
const noteGroupPermission = new NoteGroupPermission();
noteGroupPermission.canEdit = write;
noteGroupPermission.group = group;
return noteGroupPermission;
return NoteGroupPermission.create(group, write);
}
const everybodyRead = createNoteGroupPermission(groups['everybody'], false);

View file

@ -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>(RevisionsService);

View file

@ -12,6 +12,7 @@ import { NotesService } from '../notes/notes.service';
import { RevisionMetadataDto } from './revision-metadata.dto';
import { RevisionDto } from './revision.dto';
import { Revision } from './revision.entity';
import { Note } from '../notes/note.entity';
@Injectable()
export class RevisionsService {
@ -24,8 +25,7 @@ export class RevisionsService {
this.logger.setContext(RevisionsService.name);
}
async getAllRevisions(noteIdOrAlias: string): Promise<Revision[]> {
const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias);
async getAllRevisions(note: Note): Promise<Revision[]> {
return await this.revisionRepository.find({
where: {
note: note,
@ -33,11 +33,7 @@ export class RevisionsService {
});
}
async getRevision(
noteIdOrAlias: string,
revisionId: number,
): Promise<Revision> {
const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias);
async getRevision(note: Note, revisionId: number): Promise<Revision> {
return await this.revisionRepository.findOne({
where: {
id: revisionId,

View file

@ -63,7 +63,7 @@ export class UsersService {
userName: user.userName,
displayName: user.displayName,
photo: this.getPhotoUrl(user),
email: user.email,
email: user.email ?? '',
};
}
}

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function checkArrayForDuplicates<T>(array: Array<T>): boolean {
return new Set(array).size !== array.length;
}

View file

@ -74,7 +74,7 @@ describe('Notes', () => {
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
expect(
await notesService.getCurrentContent(
await notesService.getNoteContentByNote(
await notesService.getNoteByIdOrAlias(response.body.metadata.id),
),
).toEqual(content);
@ -109,11 +109,20 @@ describe('Notes', () => {
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
return expect(
await notesService.getCurrentContent(
await notesService.getNoteContentByNote(
await notesService.getNoteByIdOrAlias(response.body.metadata?.id),
),
).toEqual(content);
});
it('fails with a existing alias', async () => {
await request(app.getHttpServer())
.post('/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(400);
});
});
describe('DELETE /notes/{note}', () => {
@ -141,7 +150,7 @@ describe('Notes', () => {
.send(changedContent)
.expect(200);
await expect(
await notesService.getCurrentContent(
await notesService.getNoteContentByNote(
await notesService.getNoteByIdOrAlias('test4'),
),
).toEqual(changedContent);
@ -197,7 +206,7 @@ describe('Notes', () => {
// wait one second
await new Promise((r) => setTimeout(r, 1000));
// update the note
await notesService.updateNoteByIdOrAlias('test5a', 'More test content');
await notesService.updateNote(note, 'More test content');
const metadata = await request(app.getHttpServer())
.get('/notes/test5a/metadata')
.expect(200);