diff --git a/src/api/private/me/history/history.controller.ts b/src/api/private/me/history/history.controller.ts index 85203b78b..da00d7223 100644 --- a/src/api/private/me/history/history.controller.ts +++ b/src/api/private/me/history/history.controller.ts @@ -19,7 +19,7 @@ import { UsersService } from '../../../../users/users.service'; import { NotesService } from '../../../../notes/notes.service'; import { HistoryEntryDto } from '../../../../history/history-entry.dto'; import { NotInDBError } from '../../../../errors/errors'; -import { HistoryEntryCreationDto } from '../../../../history/history-entry-creation.dto'; +import { HistoryEntryImportDto } from '../../../../history/history-entry-import.dto'; import { HistoryEntryUpdateDto } from '../../../../history/history-entry-update.dto'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { HistoryService } from '../../../../history/history.service'; @@ -55,7 +55,7 @@ export class HistoryController { @Post() async setHistory( - @Body('history') history: HistoryEntryCreationDto[], + @Body('history') history: HistoryEntryImportDto[], ): Promise { try { // ToDo: use actual user here @@ -65,7 +65,12 @@ export class HistoryController { const note = await this.noteService.getNoteByIdOrAlias( historyEntry.note, ); - await this.historyService.createOrUpdateHistoryEntry(note, user); + await this.historyService.createOrUpdateHistoryEntry( + note, + user, + historyEntry.pinStatus, + historyEntry.lastVisited, + ); } } catch (e) { if (e instanceof NotInDBError) { diff --git a/src/history/history-entry-creation.dto.ts b/src/history/history-entry-creation.dto.ts deleted file mode 100644 index aa565ca1d..000000000 --- a/src/history/history-entry-creation.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { IsString } from 'class-validator'; - -export class HistoryEntryCreationDto { - /** - * ID or Alias of the note - */ - @IsString() - note: string; -} diff --git a/src/history/history-entry-import.dto.ts b/src/history/history-entry-import.dto.ts new file mode 100644 index 000000000..7e258c7f6 --- /dev/null +++ b/src/history/history-entry-import.dto.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IsBoolean, IsDate, IsString } from 'class-validator'; + +export class HistoryEntryImportDto { + /** + * ID or Alias of the note + */ + @IsString() + note: string; + /** + * True if the note should be pinned + * @example true + */ + @IsBoolean() + pinStatus: boolean; + /** + * Datestring of the last time this note was updated + * @example "2020-12-01 12:23:34" + */ + @IsDate() + lastVisited: Date; +} diff --git a/src/history/history.service.spec.ts b/src/history/history.service.spec.ts index b41361c86..6cc7d947c 100644 --- a/src/history/history.service.spec.ts +++ b/src/history/history.service.spec.ts @@ -141,9 +141,12 @@ describe('HistoryService', () => { describe('createOrUpdateHistoryEntry', () => { describe('works', () => { - it('without an preexisting entry', async () => { - const user = {} as User; - const alias = 'alias'; + const user = {} as User; + const alias = 'alias'; + const pinStatus = true; + const lastVisited = new Date('2020-12-01 12:23:34'); + const historyEntry = HistoryEntry.create(user, Note.create(user, alias)); + it('without an preexisting entry, without pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest .spyOn(historyRepo, 'save') @@ -160,13 +163,65 @@ describe('HistoryService', () => { expect(createHistoryEntry.pinStatus).toEqual(false); }); - it('with an preexisting entry', async () => { - const user = {} as User; - const alias = 'alias'; - const historyEntry = HistoryEntry.create( - user, + it('without an preexisting entry, with pinStatus and without lastVisited', 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, + pinStatus, ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + }); + + it('without an preexisting entry, without pinStatus and with lastVisited', 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, + undefined, + lastVisited, + ); + 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).toEqual(lastVisited); + }); + + it('without an preexisting entry, with pinStatus and with lastVisited', 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, + pinStatus, + lastVisited, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + expect(createHistoryEntry.updatedAt).toEqual(lastVisited); + }); + + it('with an preexisting entry, without pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest .spyOn(historyRepo, 'save') @@ -185,6 +240,67 @@ describe('HistoryService', () => { historyEntry.updatedAt.getTime(), ); }); + + it('with an preexisting entry, with pinStatus and without lastVisited', 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, + pinStatus, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).not.toEqual(pinStatus); + expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual( + historyEntry.updatedAt.getTime(), + ); + }); + + it('with an preexisting entry, without pinStatus and with lastVisited', 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, + undefined, + lastVisited, + ); + 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).not.toEqual(lastVisited); + }); + + it('with an preexisting entry, with pinStatus and with lastVisited', 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, + pinStatus, + lastVisited, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).not.toEqual(pinStatus); + expect(createHistoryEntry.updatedAt).not.toEqual(lastVisited); + }); }); }); diff --git a/src/history/history.service.ts b/src/history/history.service.ts index d2793ac04..276af8190 100644 --- a/src/history/history.service.ts +++ b/src/history/history.service.ts @@ -80,15 +80,25 @@ export class HistoryService { * Create or update a history entry by the user and note. If the entry is merely updated the updatedAt date is set to the current date. * @param {Note} note - the note that the history entry belongs to * @param {User} user - the user that the history entry belongs to + * @param {boolean} pinStatus - if the pinStatus of the history entry should be set + * @param {Date} lastVisited - the last time the associated note was accessed * @return {HistoryEntry} the requested history entry */ async createOrUpdateHistoryEntry( note: Note, user: User, + pinStatus?: boolean, + lastVisited?: Date, ): Promise { let entry = await this.getEntryByNote(note, user); if (!entry) { entry = HistoryEntry.create(user, note); + if (pinStatus !== undefined) { + entry.pinStatus = pinStatus; + } + if (lastVisited !== undefined) { + entry.updatedAt = lastVisited; + } } else { entry.updatedAt = new Date(); } diff --git a/test/private-api/history.e2e-spec.ts b/test/private-api/history.e2e-spec.ts index 74aee331a..192c8964d 100644 --- a/test/private-api/history.e2e-spec.ts +++ b/test/private-api/history.e2e-spec.ts @@ -26,7 +26,7 @@ import { UsersModule } from '../../src/users/users.module'; import { PrivateApiModule } from '../../src/api/private/private-api.module'; import { HistoryService } from '../../src/history/history.service'; import { Note } from '../../src/notes/note.entity'; -import { HistoryEntryCreationDto } from '../../src/history/history-entry-creation.dto'; +import { HistoryEntryImportDto } from '../../src/history/history-entry-import.dto'; describe('History', () => { let app: INestApplication; @@ -100,8 +100,13 @@ describe('History', () => { }); it('POST /me/history', async () => { - const postEntryDto = new HistoryEntryCreationDto(); + expect((await historyService.getEntriesByUser(user)).length).toEqual(1); + const pinStatus = true; + const lastVisited = new Date('2020-12-01 12:23:34'); + const postEntryDto = new HistoryEntryImportDto(); postEntryDto.note = note2.alias; + postEntryDto.pinStatus = pinStatus; + postEntryDto.lastVisited = lastVisited; await request(app.getHttpServer()) .post('/me/history') .set('Content-Type', 'application/json') @@ -110,6 +115,9 @@ describe('History', () => { const userEntries = await historyService.getEntriesByUser(user); expect(userEntries.length).toEqual(1); expect(userEntries[0].note.alias).toEqual(note2.alias); + expect(userEntries[0].user.userName).toEqual(user.userName); + expect(userEntries[0].pinStatus).toEqual(pinStatus); + expect(userEntries[0].updatedAt).toEqual(lastVisited); }); it('DELETE /me/history', async () => {