From cf02c35b49c3168d3f82bff78dde9dcbc3167d73 Mon Sep 17 00:00:00 2001
From: Tilman Vatteroth <git@tilmanvatteroth.de>
Date: Wed, 21 Jun 2023 13:11:30 +0200
Subject: [PATCH] fix: save created revision on realtime note destroy

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
---
 .../realtime-note.service.spec.ts             |  6 +--
 .../realtime-note/realtime-note.service.ts    |  2 +-
 .../src/revisions/revisions.service.spec.ts   | 46 +++++++++++++++++++
 backend/src/revisions/revisions.service.ts    | 34 ++++++++++++++
 4 files changed, 84 insertions(+), 4 deletions(-)

diff --git a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
index 35f60e3d4..8232b4e11 100644
--- a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
+++ b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
@@ -78,7 +78,7 @@ describe('RealtimeNoteService', () => {
 
     revisionsService = Mock.of<RevisionsService>({
       getLatestRevision: jest.fn(),
-      createRevision: jest.fn(),
+      createAndSaveRevision: jest.fn(),
     });
 
     consoleLoggerService = Mock.of<ConsoleLoggerService>({
@@ -294,8 +294,8 @@ describe('RealtimeNoteService', () => {
     await realtimeNoteService.getOrCreateRealtimeNote(note);
 
     const createRevisionSpy = jest
-      .spyOn(revisionsService, 'createRevision')
-      .mockImplementation(() => Promise.resolve(Mock.of<Revision>()));
+      .spyOn(revisionsService, 'createAndSaveRevision')
+      .mockResolvedValue();
 
     realtimeNote.emit('beforeDestroy');
     expect(createRevisionSpy).toHaveBeenCalledWith(
diff --git a/backend/src/realtime/realtime-note/realtime-note.service.ts b/backend/src/realtime/realtime-note/realtime-note.service.ts
index d7900dd3b..19ea69b8f 100644
--- a/backend/src/realtime/realtime-note/realtime-note.service.ts
+++ b/backend/src/realtime/realtime-note/realtime-note.service.ts
@@ -44,7 +44,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
    */
   public saveRealtimeNote(realtimeNote: RealtimeNote): void {
     this.revisionsService
-      .createRevision(
+      .createAndSaveRevision(
         realtimeNote.getNote(),
         realtimeNote.getRealtimeDoc().getCurrentContent(),
         realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
diff --git a/backend/src/revisions/revisions.service.spec.ts b/backend/src/revisions/revisions.service.spec.ts
index da0183adf..8c4dd3e2c 100644
--- a/backend/src/revisions/revisions.service.spec.ts
+++ b/backend/src/revisions/revisions.service.spec.ts
@@ -364,4 +364,50 @@ describe('RevisionsService', () => {
       expect(saveSpy).not.toHaveBeenCalled();
     });
   });
+
+  describe('createAndSaveRevision', () => {
+    it('creates and saves a new revision', async () => {
+      const newRevision = Mock.of<Revision>();
+      const createRevisionSpy = jest
+        .spyOn(service, 'createRevision')
+        .mockResolvedValue(newRevision);
+      const repoSaveSpy = jest
+        .spyOn(revisionRepo, 'save')
+        .mockResolvedValue(newRevision);
+
+      const note = Mock.of<Note>({});
+      const newContent = 'MockContent';
+
+      const yjsState = [0, 1, 2, 3, 4, 5];
+
+      await service.createAndSaveRevision(note, newContent, yjsState);
+      expect(createRevisionSpy).toHaveBeenCalledWith(
+        note,
+        newContent,
+        yjsState,
+      );
+      expect(repoSaveSpy).toHaveBeenCalledWith(newRevision);
+    });
+
+    it("doesn't save if no revision has been created", async () => {
+      const createRevisionSpy = jest
+        .spyOn(service, 'createRevision')
+        .mockResolvedValue(undefined);
+      const repoSaveSpy = jest
+        .spyOn(revisionRepo, 'save')
+        .mockRejectedValue(new Error("shouldn't have been called"));
+
+      const note = Mock.of<Note>({});
+      const newContent = 'MockContent';
+      const yjsState = [0, 1, 2, 3, 4, 5];
+
+      await service.createAndSaveRevision(note, newContent, yjsState);
+      expect(createRevisionSpy).toHaveBeenCalledWith(
+        note,
+        newContent,
+        yjsState,
+      );
+      expect(repoSaveSpy).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/backend/src/revisions/revisions.service.ts b/backend/src/revisions/revisions.service.ts
index 2607dce95..8e37d7541 100644
--- a/backend/src/revisions/revisions.service.ts
+++ b/backend/src/revisions/revisions.service.ts
@@ -150,6 +150,17 @@ export class RevisionsService {
     };
   }
 
+  /**
+   * Creates (but does not persist(!)) a new {@link Revision} for the given {@link Note}.
+   * Useful if the revision is saved together with the note in one action.
+   *
+   * @async
+   * @param note The note for which the revision should be created
+   * @param newContent The new note content
+   * @param yjsStateVector The yjs state vector that describes the new content
+   * @return {Revision} the created revision
+   * @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed
+   */
   async createRevision(
     note: Note,
     newContent: string,
@@ -185,4 +196,27 @@ export class RevisionsService {
       tagEntities,
     ) as Revision;
   }
+
+  /**
+   * Creates and saves a new {@link Revision} for the given {@link Note}.
+   *
+   * @async
+   * @param note The note for which the revision should be created
+   * @param newContent The new note content
+   * @param yjsStateVector The yjs state vector that describes the new content
+   */
+  async createAndSaveRevision(
+    note: Note,
+    newContent: string,
+    yjsStateVector?: number[],
+  ): Promise<void> {
+    const revision = await this.createRevision(
+      note,
+      newContent,
+      yjsStateVector,
+    );
+    if (revision) {
+      await this.revisionRepository.save(revision);
+    }
+  }
 }