/* * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Test, TestingModule } from '@nestjs/testing'; import { Author } from '../authors/author.entity'; import { LoggerModule } from '../logger/logger.module'; import { Session } from '../users/session.entity'; import { HistoryService } from './history.service'; import { UsersModule } from '../users/users.module'; import { NotesModule } from '../notes/notes.module'; import { getConnectionToken, getRepositoryToken } from '@nestjs/typeorm'; import { Identity } from '../users/identity.entity'; import { User } from '../users/user.entity'; import { Edit } from '../revisions/edit.entity'; import { HistoryEntry } from './history-entry.entity'; import { Note } from '../notes/note.entity'; import { Tag } from '../notes/tag.entity'; import { AuthToken } from '../auth/auth-token.entity'; import { Revision } from '../revisions/revision.entity'; import { Connection, 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'; import { ConfigModule } from '@nestjs/config'; import appConfigMock from '../config/mock/app.config.mock'; import { HistoryEntryImportDto } from './history-entry-import.dto'; describe('HistoryService', () => { let service: HistoryService; let historyRepo: Repository; let connection; let noteRepo: Repository; type MockConnection = { transaction: () => void; }; function mockConnection(): MockConnection { return { transaction: jest.fn(), }; } beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ HistoryService, { provide: getConnectionToken(), useFactory: mockConnection, }, { provide: getRepositoryToken(HistoryEntry), useClass: Repository, }, ], imports: [ LoggerModule, UsersModule, NotesModule, ConfigModule.forRoot({ isGlobal: true, load: [appConfigMock], }), ], }) .overrideProvider(getRepositoryToken(User)) .useValue({}) .overrideProvider(getRepositoryToken(AuthToken)) .useValue({}) .overrideProvider(getRepositoryToken(Identity)) .useValue({}) .overrideProvider(getRepositoryToken(Edit)) .useValue({}) .overrideProvider(getRepositoryToken(Revision)) .useValue({}) .overrideProvider(getRepositoryToken(Note)) .useClass(Repository) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) .overrideProvider(getRepositoryToken(NoteGroupPermission)) .useValue({}) .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) .overrideProvider(getRepositoryToken(Group)) .useValue({}) .overrideProvider(getRepositoryToken(Session)) .useValue({}) .overrideProvider(getRepositoryToken(Author)) .useValue({}) .compile(); service = module.get(HistoryService); historyRepo = module.get>( getRepositoryToken(HistoryEntry), ); connection = module.get(Connection); noteRepo = module.get>(getRepositoryToken(Note)); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('getEntriesByUser', () => { describe('works', () => { it('with an empty list', async () => { jest.spyOn(historyRepo, 'find').mockResolvedValueOnce([]); expect(await service.getEntriesByUser({} as User)).toEqual([]); }); it('with an one element list', async () => { const historyEntry = new HistoryEntry(); jest.spyOn(historyRepo, 'find').mockResolvedValueOnce([historyEntry]); expect(await service.getEntriesByUser({} as User)).toEqual([ historyEntry, ]); }); it('with an multiple element list', async () => { const historyEntry = new HistoryEntry(); const historyEntry2 = new HistoryEntry(); jest .spyOn(historyRepo, 'find') .mockResolvedValueOnce([historyEntry, historyEntry2]); expect(await service.getEntriesByUser({} as User)).toEqual([ historyEntry, historyEntry2, ]); }); }); }); describe('getEntryByNoteIdOrAlias', () => { const user = {} as User; const alias = 'alias'; describe('works', () => { it('with history entry', async () => { const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); expect(await service.getEntryByNoteIdOrAlias(alias, user)).toEqual( historyEntry, ); }); }); describe('fails', () => { it('with an non-existing note', async () => { jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined); await expect( service.getEntryByNoteIdOrAlias(alias, {} as User), ).rejects.toThrow(NotInDBError); }); }); }); describe('createOrUpdateHistoryEntry', () => { describe('works', () => { const user = {} as User; const alias = 'alias'; const historyEntry = HistoryEntry.create(user, Note.create(user, alias)); it('without an preexisting entry', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest .spyOn(historyRepo, 'save') .mockImplementation( async (entry: HistoryEntry): Promise => entry, ); const createHistoryEntry = await service.createOrUpdateHistoryEntry( Note.create(user, alias), user, ); expect(createHistoryEntry.note.alias).toEqual(alias); expect(createHistoryEntry.note.owner).toEqual(user); expect(createHistoryEntry.user).toEqual(user); expect(createHistoryEntry.pinStatus).toEqual(false); }); it('with an preexisting entry', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest .spyOn(historyRepo, 'save') .mockImplementation( async (entry: HistoryEntry): Promise => entry, ); const createHistoryEntry = await service.createOrUpdateHistoryEntry( Note.create(user, alias), user, ); expect(createHistoryEntry.note.alias).toEqual(alias); expect(createHistoryEntry.note.owner).toEqual(user); expect(createHistoryEntry.user).toEqual(user); expect(createHistoryEntry.pinStatus).toEqual(false); expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual( historyEntry.updatedAt.getTime(), ); }); }); }); describe('updateHistoryEntry', () => { describe('works', () => { it('with an entry', async () => { const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); jest .spyOn(historyRepo, 'save') .mockImplementation( async (entry: HistoryEntry): Promise => entry, ); const updatedHistoryEntry = await service.updateHistoryEntry( alias, user, { pinStatus: true, }, ); expect(updatedHistoryEntry.note.alias).toEqual(alias); expect(updatedHistoryEntry.note.owner).toEqual(user); expect(updatedHistoryEntry.user).toEqual(user); expect(updatedHistoryEntry.pinStatus).toEqual(true); }); it('without an entry', async () => { const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); await expect( service.updateHistoryEntry(alias, user, { pinStatus: true, }), ).rejects.toThrow(NotInDBError); }); }); }); describe('deleteHistoryEntry', () => { describe('works', () => { const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); it('with an entry', async () => { jest.spyOn(historyRepo, 'find').mockResolvedValueOnce([historyEntry]); jest .spyOn(historyRepo, 'remove') .mockImplementationOnce( async (entry: HistoryEntry): Promise => { expect(entry).toEqual(historyEntry); return entry; }, ); await service.deleteHistory(user); }); it('with multiple entries', async () => { const alias2 = 'alias2'; const note2 = Note.create(user, alias2); const historyEntry2 = HistoryEntry.create(user, note2); jest .spyOn(historyRepo, 'find') .mockResolvedValueOnce([historyEntry, historyEntry2]); jest .spyOn(historyRepo, 'remove') .mockImplementationOnce( async (entry: HistoryEntry): Promise => { expect(entry).toEqual(historyEntry); return entry; }, ) .mockImplementationOnce( async (entry: HistoryEntry): Promise => { expect(entry).toEqual(historyEntry2); return entry; }, ); await service.deleteHistory(user); }); it('without an entry', async () => { jest.spyOn(historyRepo, 'find').mockResolvedValueOnce([]); await service.deleteHistory(user); expect(true).toBeTruthy(); }); }); }); describe('deleteHistory', () => { describe('works', () => { it('with an entry', async () => { const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); jest .spyOn(historyRepo, 'remove') .mockImplementation( async (entry: HistoryEntry): Promise => { expect(entry).toEqual(historyEntry); return entry; }, ); await service.deleteHistoryEntry(alias, user); }); }); describe('fails', () => { const user = {} as User; const alias = 'alias'; it('without an entry', async () => { const note = Note.create(user, alias); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); await expect(service.deleteHistoryEntry(alias, user)).rejects.toThrow( NotInDBError, ); }); it('without a note', async () => { jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined); await expect( service.getEntryByNoteIdOrAlias(alias, {} as User), ).rejects.toThrow(NotInDBError); }); }); }); describe('setHistory', () => { it('works', async () => { const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); const historyEntryImport: HistoryEntryImportDto = { lastVisited: new Date('2020-12-01 12:23:34'), note: alias, pinStatus: true, }; const newlyCreatedHistoryEntry: HistoryEntry = { ...historyEntry, pinStatus: historyEntryImport.pinStatus, updatedAt: historyEntryImport.lastVisited, }; const mockedManager = { find: jest.fn().mockResolvedValueOnce([historyEntry]), findOne: jest.fn().mockResolvedValueOnce(note), remove: jest.fn().mockImplementationOnce((entry: HistoryEntry) => { expect(entry.note.alias).toEqual(alias); expect(entry.pinStatus).toEqual(false); }), save: jest.fn().mockImplementationOnce((entry: HistoryEntry) => { expect(entry.note.alias).toEqual(newlyCreatedHistoryEntry.note.alias); expect(entry.pinStatus).toEqual(newlyCreatedHistoryEntry.pinStatus); expect(entry.updatedAt).toEqual(newlyCreatedHistoryEntry.updatedAt); }), }; // eslint-disable-next-line @typescript-eslint/no-unsafe-call connection.transaction.mockImplementation((cb) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call cb(mockedManager); }); await service.setHistory(user, [historyEntryImport]); }); }); describe('toHistoryEntryDto', () => { describe('works', () => { it('with aliased note', async () => { const user = {} as User; const alias = 'alias'; const title = 'title'; const tags = ['tag1', 'tag2']; const note = Note.create(user, alias); note.title = title; note.tags = tags.map((tag) => { const newTag = new Tag(); newTag.name = tag; return newTag; }); const historyEntry = HistoryEntry.create(user, note); historyEntry.pinStatus = true; jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); const historyEntryDto = service.toHistoryEntryDto(historyEntry); expect(historyEntryDto.pinStatus).toEqual(true); expect(historyEntryDto.identifier).toEqual(alias); expect(historyEntryDto.tags).toEqual(tags); expect(historyEntryDto.title).toEqual(title); }); it('with regular note', async () => { const user = {} as User; const title = 'title'; const id = 'id'; const tags = ['tag1', 'tag2']; const note = Note.create(user); note.title = title; note.id = id; note.tags = tags.map((tag) => { const newTag = new Tag(); newTag.name = tag; return newTag; }); const historyEntry = HistoryEntry.create(user, note); historyEntry.pinStatus = true; jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); const historyEntryDto = service.toHistoryEntryDto(historyEntry); expect(historyEntryDto.pinStatus).toEqual(true); expect(historyEntryDto.identifier).toEqual(id); expect(historyEntryDto.tags).toEqual(tags); expect(historyEntryDto.title).toEqual(title); }); }); }); });