From bb355feddc62b9f86a3502a9ec1af5c7cd61bbed Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 11 Jun 2023 12:59:04 +0200 Subject: [PATCH] fix: improve and adjust tests Signed-off-by: Tilman Vatteroth --- backend/src/history/history.service.spec.ts | 25 +- backend/src/notes/alias.service.spec.ts | 30 +- backend/src/notes/notes.service.spec.ts | 565 +++++++++--------- .../src/revisions/revisions.service.spec.ts | 212 +++++-- 4 files changed, 490 insertions(+), 342 deletions(-) diff --git a/backend/src/history/history.service.spec.ts b/backend/src/history/history.service.spec.ts index 33f77f9ba..3e2a3765c 100644 --- a/backend/src/history/history.service.spec.ts +++ b/backend/src/history/history.service.spec.ts @@ -30,6 +30,8 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { Edit } from '../revisions/edit.entity'; import { Revision } from '../revisions/revision.entity'; +import { RevisionsModule } from '../revisions/revisions.module'; +import { RevisionsService } from '../revisions/revisions.service'; import { Session } from '../users/session.entity'; import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; @@ -40,6 +42,7 @@ import { HistoryService } from './history.service'; describe('HistoryService', () => { let service: HistoryService; + let revisionsService: RevisionsService; let historyRepo: Repository; let noteRepo: Repository; let mockedTransaction: jest.Mock< @@ -94,6 +97,7 @@ describe('HistoryService', () => { LoggerModule, UsersModule, NotesModule, + RevisionsModule, ConfigModule.forRoot({ isGlobal: true, load: [ @@ -135,6 +139,7 @@ describe('HistoryService', () => { .compile(); service = module.get(HistoryService); + revisionsService = module.get(RevisionsService); historyRepo = module.get>( getRepositoryToken(HistoryEntry), ); @@ -419,8 +424,17 @@ describe('HistoryService', () => { const title = 'title'; const tags = ['tag1', 'tag2']; const note = Note.create(user, alias) as Note; - note.title = title; - note.tags = Promise.resolve( + const revision = Revision.create( + '', + '', + note, + null, + '', + '', + [], + ) as Revision; + revision.title = title; + revision.tags = Promise.resolve( tags.map((tag) => { const newTag = new Tag(); newTag.name = tag; @@ -431,6 +445,13 @@ describe('HistoryService', () => { historyEntry.pinStatus = true; mockSelectQueryBuilderInRepo(noteRepo, note); + jest + .spyOn(revisionsService, 'getLatestRevision') + .mockImplementation((requestedNote) => { + expect(note).toBe(requestedNote); + return Promise.resolve(revision); + }); + const historyEntryDto = await service.toHistoryEntryDto(historyEntry); expect(historyEntryDto.pinStatus).toEqual(true); expect(historyEntryDto.identifier).toEqual(alias); diff --git a/backend/src/notes/alias.service.spec.ts b/backend/src/notes/alias.service.spec.ts index 3ef295647..5dd3fe574 100644 --- a/backend/src/notes/alias.service.spec.ts +++ b/backend/src/notes/alias.service.spec.ts @@ -7,6 +7,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { Mock } from 'ts-mockery'; import { DataSource, EntityManager, Repository } from 'typeorm'; import { AuthToken } from '../auth/auth-token.entity'; @@ -35,6 +36,7 @@ import { RevisionsModule } from '../revisions/revisions.module'; import { Session } from '../users/session.entity'; import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; +import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder'; import { Alias } from './alias.entity'; import { AliasService } from './alias.service'; import { Note } from './note.entity'; @@ -259,15 +261,13 @@ describe('AliasService', () => { .spyOn(aliasRepo, 'save') .mockImplementationOnce(async (alias: Alias): Promise => alias) .mockImplementationOnce(async (alias: Alias): Promise => alias); - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - orWhere: () => createQueryBuilder, - setParameter: () => createQueryBuilder, - getOne: async () => { - return { - ...note, - aliases: (await note.aliases).map((anAlias) => { + + mockSelectQueryBuilderInRepo( + noteRepo, + Mock.of({ + ...note, + aliases: Promise.resolve( + (await note.aliases).map((anAlias) => { if (anAlias.primary) { anAlias.primary = false; } @@ -276,14 +276,10 @@ describe('AliasService', () => { } return anAlias; }), - }; - }, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + ), + }), + ); + const savedAlias = await service.makeAliasPrimary(note, alias2.name); expect(savedAlias.name).toEqual(alias2.name); expect(savedAlias.primary).toBeTruthy(); diff --git a/backend/src/notes/notes.service.spec.ts b/backend/src/notes/notes.service.spec.ts index e12ccd1ea..d56436c85 100644 --- a/backend/src/notes/notes.service.spec.ts +++ b/backend/src/notes/notes.service.spec.ts @@ -7,6 +7,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { Mock } from 'ts-mockery'; import { DataSource, EntityManager, @@ -43,20 +44,24 @@ import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.modu import { Edit } from '../revisions/edit.entity'; import { Revision } from '../revisions/revision.entity'; import { RevisionsModule } from '../revisions/revisions.module'; +import { RevisionsService } from '../revisions/revisions.service'; import { Session } from '../users/session.entity'; import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; +import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder'; import { Alias } from './alias.entity'; import { AliasService } from './alias.service'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; import { Tag } from './tag.entity'; +jest.mock('../revisions/revisions.service'); + describe('NotesService', () => { let service: NotesService; + let revisionsService: RevisionsService; const noteMockConfig: NoteConfig = createDefaultMockNoteConfig(); let noteRepo: Repository; - let revisionRepo: Repository; let userRepo: Repository; let groupRepo: Repository; let forbiddenNoteId: string; @@ -74,99 +79,10 @@ describe('NotesService', () => { true, ); - /** - * Creates a Note and a corresponding User and Group for testing. - * The Note does not have any aliases. - */ - async function getMockData(): Promise<[Note, User, Group]> { - const user = User.create('hardcoded', 'Testy') as User; - const author = Author.create(1); - author.user = Promise.resolve(user); - const group = Group.create('testGroup', 'testGroup', false) as Group; - const content = 'testContent'; - jest - .spyOn(noteRepo, 'save') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(async (note: Note): Promise => note); - mockGroupRepo(); - const note = await service.createNote(content, null); - const revisions = await note.revisions; - revisions[0].edits = Promise.resolve([ - { - revisions: Promise.resolve(revisions), - startPos: 0, - endPos: 1, - updatedAt: new Date(1549312452000), - author: Promise.resolve(author), - } as Edit, - { - revisions: Promise.resolve(revisions), - startPos: 0, - endPos: 1, - updatedAt: new Date(1549312452001), - author: Promise.resolve(author), - } as Edit, - ]); - revisions[0].createdAt = new Date(1549312452000); - jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]); - const createQueryBuilder = { - innerJoin: () => createQueryBuilder, - where: () => createQueryBuilder, - getMany: () => [user], - }; - jest - .spyOn(userRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); - note.publicId = 'testId'; - note.title = 'testTitle'; - note.description = 'testDescription'; - note.owner = Promise.resolve(user); - note.userPermissions = Promise.resolve([ - { - id: 1, - note: Promise.resolve(note), - user: Promise.resolve(user), - canEdit: true, - }, - ]); - note.groupPermissions = Promise.resolve([ - { - id: 1, - note: Promise.resolve(note), - group: Promise.resolve(group), - canEdit: true, - }, - ]); - note.tags = Promise.resolve([ - { - id: 1, - name: 'testTag', - notes: Promise.resolve([note]), - }, - ]); - note.viewCount = 1337; - - return [note, user, group]; - } - - function mockGroupRepo() { - jest.spyOn(groupRepo, 'findOne').mockReset(); - jest.spyOn(groupRepo, 'findOne').mockImplementation((args) => { - const groupName = (args.where as FindOptionsWhere).name; - if (groupName === loggedin.name) { - return Promise.resolve(loggedin as Group); - } else if (groupName === everyone.name) { - return Promise.resolve(everyone as Group); - } else { - return Promise.resolve(null); - } - }); - } - beforeEach(async () => { + jest.resetAllMocks(); + jest.resetModules(); + /** * We need to have *one* userRepo for both the providers array and * the overrideProvider call, as otherwise we have two instances @@ -202,9 +118,19 @@ describe('NotesService', () => { ), undefined, ); + + revisionsService = Mock.of({ + getLatestRevision: jest.fn(), + createRevision: jest.fn(), + }); + const module: TestingModule = await Test.createTestingModule({ providers: [ NotesService, + { + provide: RevisionsService, + useValue: revisionsService, + }, AliasService, { provide: getRepositoryToken(Note), @@ -214,6 +140,10 @@ describe('NotesService', () => { provide: getRepositoryToken(Tag), useClass: Repository, }, + { + provide: getRepositoryToken(Revision), + useClass: Repository, + }, { provide: getRepositoryToken(Alias), useClass: Repository, @@ -280,12 +210,106 @@ describe('NotesService', () => { loggedinDefaultAccessPermission = noteConfig.permissions.default.loggedIn; service = module.get(NotesService); noteRepo = module.get>(getRepositoryToken(Note)); - revisionRepo = module.get>( - getRepositoryToken(Revision), - ); eventEmitter = module.get(EventEmitter2); }); + /** + * Creates a Note and a corresponding User and Group for testing. + * The Note does not have any aliases. + */ + async function getMockData(): Promise<[Note, User, Group, Revision]> { + const user = User.create('hardcoded', 'Testy') as User; + const author = Author.create(1); + author.user = Promise.resolve(user); + const group = Group.create('testGroup', 'testGroup', false) as Group; + jest + .spyOn(noteRepo, 'save') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .mockImplementation(async (note: Note): Promise => note); + mockGroupRepo(); + + const revision = Mock.of({ + edits: Promise.resolve([ + { + startPos: 0, + endPos: 1, + updatedAt: new Date(1549312452000), + author: Promise.resolve(author), + } as Edit, + { + startPos: 0, + endPos: 1, + updatedAt: new Date(1549312452001), + author: Promise.resolve(author), + } as Edit, + ]), + createdAt: new Date(1549312452000), + tags: Promise.resolve([ + { + id: 0, + name: 'tag1', + } as Tag, + ]), + content: 'mockContent', + description: 'mockDescription', + title: 'mockTitle', + }); + + const note = Mock.of({ + revisions: Promise.resolve([revision]), + aliases: Promise.resolve([]), + }); + + mockRevisionService(note, revision); + + mockSelectQueryBuilderInRepo(userRepo, user); + note.publicId = 'testId'; + note.owner = Promise.resolve(user); + note.userPermissions = Promise.resolve([ + { + id: 1, + note: Promise.resolve(note), + user: Promise.resolve(user), + canEdit: true, + }, + ]); + note.groupPermissions = Promise.resolve([ + { + id: 1, + note: Promise.resolve(note), + group: Promise.resolve(group), + canEdit: true, + }, + ]); + note.viewCount = 1337; + + return [note, user, group, revision]; + } + + function mockRevisionService(note: Note, revision: Revision) { + jest + .spyOn(revisionsService, 'getLatestRevision') + .mockImplementation((requestedNote) => { + expect(requestedNote).toBe(note); + return Promise.resolve(revision); + }); + } + + function mockGroupRepo() { + jest.spyOn(groupRepo, 'findOne').mockReset(); + jest.spyOn(groupRepo, 'findOne').mockImplementation((args) => { + const groupName = (args.where as FindOptionsWhere).name; + if (groupName === loggedin.name) { + return Promise.resolve(loggedin as Group); + } else if (groupName === everyone.name) { + return Promise.resolve(everyone as Group); + } else { + return Promise.resolve(null); + } + }); + } + it('should be defined', () => { expect(service).toBeDefined(); }); @@ -297,52 +321,19 @@ describe('NotesService', () => { const note = Note.create(user, alias) as Note; it('with no note', async () => { - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - getMany: async () => { - return null; - }, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + mockSelectQueryBuilderInRepo(noteRepo, null); const notes = await service.getUserNotes(user); expect(notes).toEqual([]); }); it('with one note', async () => { - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - getMany: async () => { - return [note]; - }, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + mockSelectQueryBuilderInRepo(noteRepo, note); const notes = await service.getUserNotes(user); expect(notes).toEqual([note]); }); it('with multiple note', async () => { - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - getMany: async () => { - return [note, note]; - }, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + mockSelectQueryBuilderInRepo(noteRepo, [note, note]); const notes = await service.getUserNotes(user); expect(notes).toEqual([note, note]); }); @@ -353,6 +344,9 @@ describe('NotesService', () => { const user = User.create('hardcoded', 'Testy') as User; const alias = 'alias'; const content = 'testContent'; + const newRevision = Mock.of({}); + let createRevisionSpy: jest.SpyInstance; + describe('works', () => { beforeEach(() => { jest @@ -361,12 +355,16 @@ describe('NotesService', () => { // @ts-ignore .mockImplementation(async (note: Note): Promise => note); mockGroupRepo(); + + createRevisionSpy = jest + .spyOn(revisionsService, 'createRevision') + .mockResolvedValue(newRevision); }); it('without alias, without owner', async () => { const newNote = await service.createNote(content, null); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0); const groupPermissions = await newNote.groupPermissions; @@ -383,15 +381,13 @@ describe('NotesService', () => { expect((await groupPermissions[1].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toBeNull(); expect(await newNote.aliases).toHaveLength(0); }); it('without alias, with owner', async () => { const newNote = await service.createNote(content, user); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(1); expect(await (await newNote.historyEntries)[0].user).toEqual(user); expect(await newNote.userPermissions).toHaveLength(0); @@ -409,15 +405,13 @@ describe('NotesService', () => { expect((await groupPermissions[1].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toEqual(user); expect(await newNote.aliases).toHaveLength(0); }); it('with alias, without owner', async () => { const newNote = await service.createNote(content, null, alias); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0); const groupPermissions = await newNote.groupPermissions; @@ -434,15 +428,14 @@ describe('NotesService', () => { expect((await groupPermissions[1].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toBeNull(); expect(await newNote.aliases).toHaveLength(1); }); it('with alias, with owner', async () => { const newNote = await service.createNote(content, user, alias); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(1); expect(await (await newNote.historyEntries)[0].user).toEqual(user); expect(await newNote.userPermissions).toHaveLength(0); @@ -460,7 +453,6 @@ describe('NotesService', () => { expect((await groupPermissions[1].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toEqual(user); expect(await newNote.aliases).toHaveLength(1); expect((await newNote.aliases)[0].name).toEqual(alias); @@ -470,9 +462,9 @@ describe('NotesService', () => { it('and content has length maxDocumentLength', async () => { const content = 'x'.repeat(noteMockConfig.maxDocumentLength); const newNote = await service.createNote(content, user, alias); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(1); expect(await (await newNote.historyEntries)[0].user).toEqual(user); expect(await newNote.userPermissions).toHaveLength(0); @@ -490,7 +482,6 @@ describe('NotesService', () => { expect((await groupPermissions[1].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toEqual(user); expect(await newNote.aliases).toHaveLength(1); expect((await newNote.aliases)[0].name).toEqual(alias); @@ -505,9 +496,9 @@ describe('NotesService', () => { it('default permissions', async () => { mockGroupRepo(); const newNote = await service.createNote(content, user, alias); - const revisions = await newNote.revisions; - expect(revisions).toHaveLength(1); - expect(revisions[0].content).toEqual(content); + + expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content); + expect(await newNote.revisions).toStrictEqual([newRevision]); expect(await newNote.historyEntries).toHaveLength(1); expect(await (await newNote.historyEntries)[0].user).toEqual(user); expect(await newNote.userPermissions).toHaveLength(0); @@ -519,7 +510,6 @@ describe('NotesService', () => { expect((await groupPermissions[0].group).name).toEqual( SpecialGroup.LOGGED_IN, ); - expect(await newNote.tags).toHaveLength(0); expect(await newNote.owner).toEqual(user); expect(await newNote.aliases).toHaveLength(1); expect((await newNote.aliases)[0].name).toEqual(alias); @@ -561,54 +551,25 @@ describe('NotesService', () => { describe('getNoteContent', () => { it('works', async () => { const content = 'testContent'; - jest - .spyOn(noteRepo, 'save') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(async (note: Note): Promise => note); - mockGroupRepo(); - const newNote = await service.createNote(content, null); - const revisions = await newNote.revisions; - jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revisions[0]); - await service.getNoteContent(newNote).then((result) => { - expect(result).toEqual(content); - }); + const revision = Mock.of({ content: content }); + const newNote = Mock.of(); + mockRevisionService(newNote, revision); + const result = await service.getNoteContent(newNote); + expect(result).toEqual(content); }); }); describe('getNoteByIdOrAlias', () => { it('works', async () => { const user = User.create('hardcoded', 'Testy') as User; - const note = Note.create(user); - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - orWhere: () => createQueryBuilder, - setParameter: () => createQueryBuilder, - getOne: () => note, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + const note = Note.create(user) as Note; + mockSelectQueryBuilderInRepo(noteRepo, note); const foundNote = await service.getNoteByIdOrAlias('noteThatExists'); expect(foundNote).toEqual(note); }); describe('fails:', () => { it('no note found', async () => { - const createQueryBuilder = { - leftJoinAndSelect: () => createQueryBuilder, - where: () => createQueryBuilder, - orWhere: () => createQueryBuilder, - setParameter: () => createQueryBuilder, - getOne: () => null, - }; - jest - .spyOn(noteRepo, 'createQueryBuilder') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .mockImplementation(() => createQueryBuilder); + mockSelectQueryBuilderInRepo(noteRepo, null); await expect( service.getNoteByIdOrAlias('noteThatDoesNoteExist'), ).rejects.toThrow(NotInDBError); @@ -644,81 +605,111 @@ describe('NotesService', () => { }); describe('updateNote', () => { - it('works', async () => { - const [note, ,] = await getMockData(); - const revisionLength = (await note.revisions).length; - const updatedNote = await service.updateNote(note, 'newContent'); - expect(await updatedNote.revisions).toHaveLength(revisionLength + 1); - }); - }); + it('adds a new revision if content is different', async () => { + const [note, , , revision] = await getMockData(); - describe('toTagList', () => { - it('works', async () => { - const note = {} as Note; - note.tags = Promise.resolve([ - { - id: 1, - name: 'testTag', - notes: Promise.resolve([note]), - }, + const mockRevision = Mock.of({}); + const createRevisionSpy = jest + .spyOn(revisionsService, 'createRevision') + .mockReturnValue(Promise.resolve(mockRevision)); + + const newContent = 'newContent'; + const updatedNote = await service.updateNote(note, newContent); + expect(await updatedNote.revisions).toStrictEqual([ + revision, + mockRevision, ]); - const tagList = await service.toTagList(note); - expect(tagList).toHaveLength(1); - expect(tagList[0]).toEqual((await note.tags)[0].name); + expect(createRevisionSpy).toHaveBeenCalledWith(note, newContent); + }); + + it("won't create a new revision if content is same", async () => { + const [note, , , revision] = await getMockData(); + const createRevisionSpy = jest + .spyOn(revisionsService, 'createRevision') + .mockReturnValue(Promise.resolve(undefined)); + + const newContent = 'newContent'; + const updatedNote = await service.updateNote(note, newContent); + expect(await updatedNote.revisions).toStrictEqual([revision]); + expect(createRevisionSpy).toHaveBeenCalledWith(note, newContent); }); }); describe('toNotePermissionsDto', () => { it('works', async () => { - const [note, user, group] = await getMockData(); + const [note] = await getMockData(); const permissions = await service.toNotePermissionsDto(note); - expect(permissions.owner).toEqual(user.username); - expect(permissions.sharedToUsers).toHaveLength(1); - expect(permissions.sharedToUsers[0].username).toEqual(user.username); - expect(permissions.sharedToUsers[0].canEdit).toEqual(true); - expect(permissions.sharedToGroups).toHaveLength(1); - expect(permissions.sharedToGroups[0].groupName).toEqual( - group.displayName, - ); - expect(permissions.sharedToGroups[0].canEdit).toEqual(true); + expect(permissions).toMatchInlineSnapshot(` + { + "owner": "hardcoded", + "sharedToGroups": [ + { + "canEdit": true, + "groupName": "testGroup", + }, + ], + "sharedToUsers": [ + { + "canEdit": true, + "username": "hardcoded", + }, + ], + } + `); }); }); describe('toNoteMetadataDto', () => { it('works', async () => { - const [note, user, group] = await getMockData(); + const [note] = await getMockData(); note.aliases = Promise.resolve([ Alias.create('testAlias', note, true) as Alias, ]); const metadataDto = await service.toNoteMetadataDto(note); - expect(metadataDto.id).toEqual(note.publicId); - expect(metadataDto.aliases).toHaveLength(1); - expect(metadataDto.aliases[0].name).toEqual((await note.aliases)[0].name); - expect(metadataDto.primaryAddress).toEqual('testAlias'); - expect(metadataDto.title).toEqual(note.title); - expect(metadataDto.description).toEqual(note.description); - expect(metadataDto.editedBy).toHaveLength(1); - expect(metadataDto.editedBy[0]).toEqual(user.username); - expect(metadataDto.permissions.owner).toEqual(user.username); - expect(metadataDto.permissions.sharedToUsers).toHaveLength(1); - expect(metadataDto.permissions.sharedToUsers[0].username).toEqual( - user.username, - ); - expect(metadataDto.permissions.sharedToUsers[0].canEdit).toEqual(true); - expect(metadataDto.permissions.sharedToGroups).toHaveLength(1); - expect(metadataDto.permissions.sharedToGroups[0].groupName).toEqual( - group.displayName, - ); - expect(metadataDto.permissions.sharedToGroups[0].canEdit).toEqual(true); - expect(metadataDto.tags).toHaveLength(1); - expect(metadataDto.tags[0]).toEqual((await note.tags)[0].name); - expect(metadataDto.updatedAt).toEqual( - (await note.revisions)[0].createdAt, - ); - expect(metadataDto.updateUsername).toEqual(user.username); - expect(metadataDto.viewCount).toEqual(note.viewCount); + expect(metadataDto).toMatchInlineSnapshot(` + { + "aliases": [ + { + "name": "testAlias", + "noteId": "testId", + "primaryAlias": true, + }, + ], + "createdAt": undefined, + "description": "mockDescription", + "editedBy": [ + "hardcoded", + ], + "id": "testId", + "permissions": { + "owner": "hardcoded", + "sharedToGroups": [ + { + "canEdit": true, + "groupName": "testGroup", + }, + ], + "sharedToUsers": [ + { + "canEdit": true, + "username": "hardcoded", + }, + ], + }, + "primaryAddress": "testAlias", + "tags": [ + "tag1", + ], + "title": "mockTitle", + "updateUsername": "hardcoded", + "updatedAt": 2019-02-04T20:34:12.000Z, + "version": undefined, + "viewCount": 1337, + } + `); }); + it('returns publicId if no alias exists', async () => { const [note, ,] = await getMockData(); const metadataDto = await service.toNoteMetadataDto(note); @@ -728,41 +719,57 @@ describe('NotesService', () => { describe('toNoteDto', () => { it('works', async () => { - const [note, user, group] = await getMockData(); + const [note] = await getMockData(); note.aliases = Promise.resolve([ Alias.create('testAlias', note, true) as Alias, ]); const noteDto = await service.toNoteDto(note); - expect(noteDto.metadata.id).toEqual(note.publicId); - expect(noteDto.metadata.aliases).toHaveLength(1); - expect(noteDto.metadata.aliases[0].name).toEqual( - (await note.aliases)[0].name, - ); - expect(noteDto.metadata.title).toEqual(note.title); - 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).toEqual(user.username); - expect(noteDto.metadata.permissions.sharedToUsers).toHaveLength(1); - expect(noteDto.metadata.permissions.sharedToUsers[0].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].groupName).toEqual( - group.displayName, - ); - expect(noteDto.metadata.permissions.sharedToGroups[0].canEdit).toEqual( - true, - ); - expect(noteDto.metadata.tags).toHaveLength(1); - expect(noteDto.metadata.tags[0]).toEqual((await note.tags)[0].name); - expect(noteDto.metadata.updateUsername).toEqual(user.username); - expect(noteDto.metadata.viewCount).toEqual(note.viewCount); - expect(noteDto.content).toEqual('testContent'); + expect(noteDto).toMatchInlineSnapshot(` + { + "content": "mockContent", + "editedByAtPosition": [], + "metadata": { + "aliases": [ + { + "name": "testAlias", + "noteId": "testId", + "primaryAlias": true, + }, + ], + "createdAt": undefined, + "description": "mockDescription", + "editedBy": [ + "hardcoded", + ], + "id": "testId", + "permissions": { + "owner": "hardcoded", + "sharedToGroups": [ + { + "canEdit": true, + "groupName": "testGroup", + }, + ], + "sharedToUsers": [ + { + "canEdit": true, + "username": "hardcoded", + }, + ], + }, + "primaryAddress": "testAlias", + "tags": [ + "tag1", + ], + "title": "mockTitle", + "updateUsername": "hardcoded", + "updatedAt": 2019-02-04T20:34:12.000Z, + "version": undefined, + "viewCount": 1337, + }, + } + `); }); }); }); diff --git a/backend/src/revisions/revisions.service.spec.ts b/backend/src/revisions/revisions.service.spec.ts index 0f734f156..da0183adf 100644 --- a/backend/src/revisions/revisions.service.spec.ts +++ b/backend/src/revisions/revisions.service.spec.ts @@ -104,9 +104,9 @@ describe('RevisionsService', () => { describe('getRevision', () => { it('returns a revision', async () => { const note = Mock.of({}); - const revision = Revision.create('', '', note) as Revision; + const revision = Mock.of({}); jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(revision); - expect(await service.getRevision({} as Note, 1)).toEqual(revision); + expect(await service.getRevision(note, 1)).toBe(revision); }); it('throws if the revision is not in the databse', async () => { jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(null); @@ -117,54 +117,55 @@ describe('RevisionsService', () => { }); describe('purgeRevisions', () => { + let revisions: Revision[]; + let note: Note; + + beforeEach(() => { + note = Mock.of({}); + revisions = []; + + jest + .spyOn(revisionRepo, 'remove') + .mockImplementation( + (deleteEntities: T): Promise => { + const newRevisions = revisions.filter((item: Revision) => + Array.isArray(deleteEntities) + ? !deleteEntities.includes(item) + : deleteEntities !== item, + ); + revisions = newRevisions; + note.revisions = Promise.resolve(newRevisions); + return Promise.resolve(deleteEntities); + }, + ); + }); + it('purges the revision history', async () => { - const note = {} as Note; - note.id = 4711; - let revisions: Revision[] = []; - const revision1 = Revision.create('a', 'a', note) as Revision; - revision1.id = 1; - const revision2 = Revision.create('b', 'b', note) as Revision; - revision2.id = 2; - const revision3 = Revision.create('c', 'c', note) as Revision; - revision3.id = 3; - revisions.push(revision1, revision2, revision3); + const revision1 = Mock.of({ id: 1 }); + const revision2 = Mock.of({ id: 2 }); + const revision3 = Mock.of({ id: 3 }); + revisions = [revision1, revision2, revision3]; note.revisions = Promise.resolve(revisions); + jest.spyOn(revisionRepo, 'find').mockResolvedValueOnce(revisions); jest.spyOn(service, 'getLatestRevision').mockResolvedValueOnce(revision3); - revisionRepo.remove = jest - .fn() - .mockImplementation((deleteList: Revision[]) => { - revisions = revisions.filter( - (item: Revision) => !deleteList.includes(item), - ); - return Promise.resolve(deleteList); - }); // expected to return all the purged revisions - expect(await service.purgeRevisions(note)).toHaveLength(2); + expect(await service.purgeRevisions(note)).toStrictEqual([ + revision1, + revision2, + ]); // expected to have only the latest revision - const updatedRevisions: Revision[] = [revision3]; - expect(revisions).toEqual(updatedRevisions); + expect(revisions).toStrictEqual([revision3]); }); it('has no effect on revision history when a single revision is present', async () => { - const note = {} as Note; - note.id = 4711; - let revisions: Revision[] = []; - const revision1 = Revision.create('a', 'a', note) as Revision; - revision1.id = 1; - revisions.push(revision1); + const revision1 = Mock.of({ id: 1 }); + revisions = [revision1]; note.revisions = Promise.resolve(revisions); + jest.spyOn(revisionRepo, 'find').mockResolvedValueOnce(revisions); jest.spyOn(service, 'getLatestRevision').mockResolvedValueOnce(revision1); - revisionRepo.remove = jest - .fn() - .mockImplementation((deleteList: Revision[]) => { - revisions = revisions.filter( - (item: Revision) => !deleteList.includes(item), - ); - return Promise.resolve(deleteList); - }); // expected to return all the purged revisions expect(await service.purgeRevisions(note)).toHaveLength(0); @@ -188,7 +189,7 @@ describe('RevisionsService', () => { edits.push(Edit.create(anonAuthor, 29, 20) as Edit); edits.push(Edit.create(anonAuthor, 29, 20) as Edit); edits.push(Edit.create(anonAuthor2, 29, 20) as Edit); - const revision = Revision.create('', '', {} as Note) as Revision; + const revision = Mock.of({}); revision.edits = Promise.resolve(edits); const userInfo = await service.getRevisionUserInfo(revision); @@ -197,13 +198,122 @@ describe('RevisionsService', () => { }); }); + describe('toRevisionMetadataDto', () => { + it('converts a revision', async () => { + const revision = Mock.of({ + id: 3246, + content: 'mockContent', + length: 1854, + createdAt: new Date('2020-05-20T09:58:00.000Z'), + title: 'mockTitle', + tags: Promise.resolve([Mock.of({ name: 'mockTag' })]), + description: 'mockDescription', + patch: 'mockPatch', + edits: Promise.resolve([ + Mock.of({ + endPos: 93, + startPos: 34, + createdAt: new Date('2020-03-04T20:12:00.000Z'), + updatedAt: new Date('2021-12-10T09:45:00.000Z'), + author: Promise.resolve( + Mock.of({ + user: Promise.resolve( + Mock.of({ + username: 'mockusername', + }), + ), + }), + ), + }), + ]), + }); + expect(await service.toRevisionMetadataDto(revision)) + .toMatchInlineSnapshot(` + { + "anonymousAuthorCount": 0, + "authorUsernames": [ + "mockusername", + ], + "createdAt": 2020-05-20T09:58:00.000Z, + "description": "mockDescription", + "id": 3246, + "length": 1854, + "tags": [ + "mockTag", + ], + "title": "mockTitle", + } + `); + }); + }); + + describe('toRevisionDto', () => { + it('converts a revision', async () => { + const revision = Mock.of({ + id: 3246, + content: 'mockContent', + length: 1854, + createdAt: new Date('2020-05-20T09:58:00.000Z'), + title: 'mockTitle', + tags: Promise.resolve([Mock.of({ name: 'mockTag' })]), + description: 'mockDescription', + patch: 'mockPatch', + edits: Promise.resolve([ + Mock.of({ + endPos: 93, + startPos: 34, + createdAt: new Date('2020-03-04T22:32:00.000Z'), + updatedAt: new Date('2021-02-10T12:23:00.000Z'), + author: Promise.resolve( + Mock.of({ + user: Promise.resolve( + Mock.of({ + username: 'mockusername', + }), + ), + }), + ), + }), + ]), + }); + expect(await service.toRevisionDto(revision)).toMatchInlineSnapshot(` + { + "anonymousAuthorCount": 0, + "authorUsernames": [ + "mockusername", + ], + "content": "mockContent", + "createdAt": 2020-05-20T09:58:00.000Z, + "description": "mockDescription", + "edits": [ + { + "createdAt": 2020-03-04T22:32:00.000Z, + "endPos": 93, + "startPos": 34, + "updatedAt": 2021-02-10T12:23:00.000Z, + "username": "mockusername", + }, + ], + "id": 3246, + "length": 1854, + "patch": "mockPatch", + "tags": [ + "mockTag", + ], + "title": "mockTitle", + } + `); + }); + }); + describe('createRevision', () => { it('creates a new revision', async () => { - const note = Mock.of({ publicId: 'test-note' }); + const note = Mock.of({ publicId: 'test-note', id: 1 }); const oldContent = 'old content\n'; - const newContent = 'new content\n'; + const newContent = + '---\ntitle: new title\ndescription: new description\ntags: [ "tag1" ]\n---\nnew content\n'; - const oldRevision = Mock.of({ content: oldContent }); + const oldRevision = Mock.of({ content: oldContent, id: 1 }); jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(oldRevision); jest .spyOn(revisionRepo, 'save') @@ -214,24 +324,38 @@ describe('RevisionsService', () => { const createdRevision = await service.createRevision(note, newContent); expect(createdRevision).not.toBeUndefined(); expect(createdRevision?.content).toBe(newContent); + await expect(createdRevision?.tags).resolves.toMatchInlineSnapshot(` + [ + Tag { + "name": "tag1", + }, + ] + `); + expect(createdRevision?.title).toBe('new title'); + expect(createdRevision?.description).toBe('new description'); await expect(createdRevision?.note).resolves.toBe(note); expect(createdRevision?.patch).toMatchInlineSnapshot(` "Index: test-note =================================================================== --- test-note +++ test-note - @@ -1,1 +1,1 @@ + @@ -1,1 +1,6 @@ -old content + +--- + +title: new title + +description: new description + +tags: [ "tag1" ] + +--- +new content " `); }); it("won't create a revision if content is unchanged", async () => { - const note = Mock.of({}); + const note = Mock.of({ id: 1 }); const oldContent = 'old content\n'; - const oldRevision = Mock.of({ content: oldContent }); + const oldRevision = Mock.of({ content: oldContent, id: 1 }); jest.spyOn(revisionRepo, 'findOne').mockResolvedValueOnce(oldRevision); const saveSpy = jest.spyOn(revisionRepo, 'save').mockImplementation();