From 2a2d3756adfee4147c11119420ca095b7238e197 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Fri, 31 Mar 2023 21:42:11 +0200 Subject: [PATCH] refactor: test code of realtime Signed-off-by: Tilman Vatteroth --- .../realtime-note/realtime-note.spec.ts | 12 +- .../realtime-user-status-adapter.spec.ts | 195 ++++++++++-------- .../test-utils/mock-connection.ts | 82 ++++++-- 3 files changed, 183 insertions(+), 106 deletions(-) diff --git a/backend/src/realtime/realtime-note/realtime-note.spec.ts b/backend/src/realtime/realtime-note/realtime-note.spec.ts index f1807473a..8a7cf7225 100644 --- a/backend/src/realtime/realtime-note/realtime-note.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-note.spec.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,7 +29,7 @@ describe('realtime note', () => { it('can connect and disconnect clients', () => { const sut = new RealtimeNote(mockedNote, 'nothing'); - const client1 = new MockConnectionBuilder(sut).build(); + const client1 = new MockConnectionBuilder(sut).withLoggedInUser().build(); expect(sut.getConnections()).toStrictEqual([client1]); expect(sut.hasConnections()).toBeTruthy(); sut.removeClient(client1); @@ -70,8 +70,8 @@ describe('realtime note', () => { it('announcePermissionChange to all clients', () => { const sut = new RealtimeNote(mockedNote, 'nothing'); - const client1 = new MockConnectionBuilder(sut).build(); - const client2 = new MockConnectionBuilder(sut).build(); + const client1 = new MockConnectionBuilder(sut).withLoggedInUser().build(); + const client2 = new MockConnectionBuilder(sut).withLoggedInUser().build(); const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); @@ -88,8 +88,8 @@ describe('realtime note', () => { it('announceNoteDeletion to all clients', () => { const sut = new RealtimeNote(mockedNote, 'nothing'); - const client1 = new MockConnectionBuilder(sut).build(); - const client2 = new MockConnectionBuilder(sut).build(); + const client1 = new MockConnectionBuilder(sut).withLoggedInUser().build(); + const client2 = new MockConnectionBuilder(sut).withLoggedInUser().build(); const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts index 7ebe9884f..94bef2c59 100644 --- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts @@ -17,70 +17,76 @@ type SendMessageSpy = jest.SpyInstance< >; describe('realtime user status adapter', () => { - let client1: RealtimeConnection; - let client2: RealtimeConnection; - let client3: RealtimeConnection; - let client4: RealtimeConnection; + let clientLoggedIn1: RealtimeConnection; + let clientLoggedIn2: RealtimeConnection; + let clientGuest: RealtimeConnection; + let clientNotReady: RealtimeConnection; - let sendMessage1Spy: SendMessageSpy; - let sendMessage2Spy: SendMessageSpy; - let sendMessage3Spy: SendMessageSpy; - let sendMessage4Spy: SendMessageSpy; + let clientLoggedIn1SendMessageSpy: SendMessageSpy; + let clientLoggedIn2SendMessageSpy: SendMessageSpy; + let clientGuestSendMessageSpy: SendMessageSpy; + let clientNotReadySendMessageSpy: SendMessageSpy; let realtimeNote: RealtimeNote; - const username1 = 'mock1'; - const username2 = 'mock2'; - const username3 = 'mock3'; - const username4 = 'mock4'; + const clientLoggedIn1Username = 'logged.in1'; + const clientLoggedIn2Username = 'logged.in2'; + const clientNotReadyUsername = 'not.ready'; + + const guestDisplayName = 'Virtuous Mockingbird'; + + function spyOnSendMessage(connection: RealtimeConnection): jest.SpyInstance { + return jest.spyOn(connection.getTransporter(), 'sendMessage'); + } beforeEach(() => { realtimeNote = new RealtimeNote( Mock.of({ id: 9876 }), 'mockedContent', ); - client1 = new MockConnectionBuilder(realtimeNote) - .withRealtimeUserState() - .withUsername(username1) + clientLoggedIn1 = new MockConnectionBuilder(realtimeNote) + .withRealtimeUserStatus() + .withLoggedInUser(clientLoggedIn1Username) .build(); - client2 = new MockConnectionBuilder(realtimeNote) - .withRealtimeUserState() - .withUsername(username2) + clientLoggedIn2 = new MockConnectionBuilder(realtimeNote) + .withRealtimeUserStatus() + .withLoggedInUser(clientLoggedIn2Username) .build(); - client3 = new MockConnectionBuilder(realtimeNote) - .withRealtimeUserState() - .withUsername(username3) + clientGuest = new MockConnectionBuilder(realtimeNote) + .withRealtimeUserStatus() + .withGuestUser(guestDisplayName) .build(); - client4 = new MockConnectionBuilder(realtimeNote) - .withRealtimeUserState() - .withUsername(username4) + clientNotReady = new MockConnectionBuilder(realtimeNote) + .withRealtimeUserStatus() + .withLoggedInUser(clientNotReadyUsername) .build(); - sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); - sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); - sendMessage3Spy = jest.spyOn(client3.getTransporter(), 'sendMessage'); - sendMessage4Spy = jest.spyOn(client4.getTransporter(), 'sendMessage'); + clientLoggedIn1SendMessageSpy = spyOnSendMessage(clientLoggedIn1); + clientLoggedIn2SendMessageSpy = spyOnSendMessage(clientLoggedIn2); + clientGuestSendMessageSpy = spyOnSendMessage(clientGuest); + clientNotReadySendMessageSpy = spyOnSendMessage(clientNotReady); - client1.getTransporter().sendReady(); - client2.getTransporter().sendReady(); - client3.getTransporter().sendReady(); - //client 4 shouldn't be ready on purpose + clientLoggedIn1.getTransporter().sendReady(); + clientLoggedIn2.getTransporter().sendReady(); + clientGuest.getTransporter().sendReady(); }); it('can answer a state request', () => { - expect(sendMessage1Spy).toHaveBeenCalledTimes(0); - expect(sendMessage2Spy).toHaveBeenCalledTimes(0); - expect(sendMessage3Spy).toHaveBeenCalledTimes(0); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); - client1.getTransporter().emit(MessageType.REALTIME_USER_STATE_REQUEST); + clientLoggedIn1 + .getTransporter() + .emit(MessageType.REALTIME_USER_STATE_REQUEST); const expectedMessage1: Message = { type: MessageType.REALTIME_USER_STATE_SET, payload: { ownUser: { styleIndex: 0, - displayName: username1, + displayName: clientLoggedIn1Username, }, users: [ { @@ -90,8 +96,8 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 1, - username: username2, - displayName: username2, + username: clientLoggedIn2Username, + displayName: clientLoggedIn2Username, }, { active: true, @@ -100,41 +106,46 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 2, - username: username3, - displayName: username3, + username: null, + displayName: guestDisplayName, }, ], }, }; - expect(sendMessage1Spy).toHaveBeenNthCalledWith(1, expectedMessage1); - expect(sendMessage2Spy).toHaveBeenCalledTimes(0); - expect(sendMessage3Spy).toHaveBeenCalledTimes(0); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith( + 1, + expectedMessage1, + ); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); }); it('can save an cursor update', () => { - expect(sendMessage1Spy).toHaveBeenCalledTimes(0); - expect(sendMessage2Spy).toHaveBeenCalledTimes(0); - expect(sendMessage3Spy).toHaveBeenCalledTimes(0); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); const newFrom = Math.floor(Math.random() * 100); const newTo = Math.floor(Math.random() * 100); - client1.getTransporter().emit(MessageType.REALTIME_USER_SINGLE_UPDATE, { - type: MessageType.REALTIME_USER_SINGLE_UPDATE, - payload: { - from: newFrom, - to: newTo, - }, - }); + clientLoggedIn1 + .getTransporter() + .emit(MessageType.REALTIME_USER_SINGLE_UPDATE, { + type: MessageType.REALTIME_USER_SINGLE_UPDATE, + payload: { + from: newFrom, + to: newTo, + }, + }); const expectedMessage2: Message = { type: MessageType.REALTIME_USER_STATE_SET, payload: { ownUser: { styleIndex: 1, - displayName: username2, + displayName: clientLoggedIn2Username, }, users: [ { @@ -144,8 +155,8 @@ describe('realtime user status adapter', () => { to: newTo, }, styleIndex: 0, - username: username1, - displayName: username1, + username: clientLoggedIn1Username, + displayName: clientLoggedIn1Username, }, { active: true, @@ -154,8 +165,8 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 2, - username: username3, - displayName: username3, + username: null, + displayName: guestDisplayName, }, ], }, @@ -166,7 +177,7 @@ describe('realtime user status adapter', () => { payload: { ownUser: { styleIndex: 2, - displayName: username3, + displayName: guestDisplayName, }, users: [ { @@ -176,8 +187,8 @@ describe('realtime user status adapter', () => { to: newTo, }, styleIndex: 0, - username: username1, - displayName: username1, + username: clientLoggedIn1Username, + displayName: clientLoggedIn1Username, }, { active: true, @@ -186,33 +197,39 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 1, - username: username2, - displayName: username2, + username: clientLoggedIn2Username, + displayName: clientLoggedIn2Username, }, ], }, }; - expect(sendMessage1Spy).toHaveBeenCalledTimes(0); - expect(sendMessage2Spy).toHaveBeenNthCalledWith(1, expectedMessage2); - expect(sendMessage3Spy).toHaveBeenNthCalledWith(1, expectedMessage3); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith( + 1, + expectedMessage2, + ); + expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith( + 1, + expectedMessage3, + ); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); }); it('will inform other clients about removed client', () => { - expect(sendMessage1Spy).toHaveBeenCalledTimes(0); - expect(sendMessage2Spy).toHaveBeenCalledTimes(0); - expect(sendMessage3Spy).toHaveBeenCalledTimes(0); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); - client2.getTransporter().disconnect(); + clientLoggedIn2.getTransporter().disconnect(); const expectedMessage1: Message = { type: MessageType.REALTIME_USER_STATE_SET, payload: { ownUser: { styleIndex: 0, - displayName: username1, + displayName: clientLoggedIn1Username, }, users: [ { @@ -222,8 +239,8 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 2, - username: username3, - displayName: username3, + username: null, + displayName: guestDisplayName, }, ], }, @@ -234,7 +251,7 @@ describe('realtime user status adapter', () => { payload: { ownUser: { styleIndex: 2, - displayName: username3, + displayName: guestDisplayName, }, users: [ { @@ -244,16 +261,22 @@ describe('realtime user status adapter', () => { to: 0, }, styleIndex: 0, - username: username1, - displayName: username1, + username: clientLoggedIn1Username, + displayName: clientLoggedIn1Username, }, ], }, }; - expect(sendMessage1Spy).toHaveBeenNthCalledWith(1, expectedMessage1); - expect(sendMessage2Spy).toHaveBeenCalledTimes(0); - expect(sendMessage3Spy).toHaveBeenNthCalledWith(1, expectedMessage3); - expect(sendMessage4Spy).toHaveBeenCalledTimes(0); + expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith( + 1, + expectedMessage1, + ); + expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0); + expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith( + 1, + expectedMessage3, + ); + expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); }); }); diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts index b64758af0..47a2203c4 100644 --- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts +++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,31 +14,75 @@ import { RealtimeConnection } from '../realtime-connection'; import { RealtimeNote } from '../realtime-note'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; +const MOCK_FALLBACK_USERNAME = 'mock'; + +/** + * Creates a mocked {@link RealtimeConnection realtime connection}. + */ export class MockConnectionBuilder { - private username = 'mock'; - private includeRealtimeUserState = false; + private username: string | null; + private displayName: string | undefined; + private includeRealtimeUserStatus = false; constructor(private readonly realtimeNote: RealtimeNote) {} - public withUsername(username: string): this { - this.username = username; + /** + * Defines that the user who belongs to the connection is a guest. + * + * @param displayName the display name of the guest user + */ + public withGuestUser(displayName: string): this { + this.username = null; + this.displayName = displayName; return this; } - public withRealtimeUserState(): this { - this.includeRealtimeUserState = true; + /** + * Defines that the user who belongs to this connection is a logged-in user. + * + * @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}. + */ + public withLoggedInUser(username?: string): this { + const newUsername = username ?? MOCK_FALLBACK_USERNAME; + this.username = newUsername; + this.displayName = newUsername; return this; } + /** + * Defines that the connection should contain a {@link RealtimeUserStatusAdapter}. + */ + public withRealtimeUserStatus(): this { + this.includeRealtimeUserStatus = true; + return this; + } + + /** + * Creates a new connection based on the given configuration. + * + * @return {RealtimeConnection} The constructed mocked connection + * @throws Error if neither withGuestUser nor withLoggedInUser has been called. + */ public build(): RealtimeConnection { + const displayName = this.deriveDisplayName(); + const transporter = new MockedBackendMessageTransporter(''); let realtimeUserStateAdapter: RealtimeUserStatusAdapter = - Mock.of(); + Mock.of({}); + + const mockUser = + this.username === null + ? null + : Mock.of({ + username: this.username, + displayName: this.displayName, + }); + const yDocSyncServerAdapter = Mock.of({}); const connection = Mock.of({ - getUser: jest.fn(() => Mock.of({ username: this.username })), - getDisplayName: jest.fn(() => this.username), - getSyncAdapter: jest.fn(() => Mock.of({})), + getUser: jest.fn(() => mockUser), + getDisplayName: jest.fn(() => displayName), + getSyncAdapter: jest.fn(() => yDocSyncServerAdapter), getTransporter: jest.fn(() => transporter), getRealtimeUserStateAdapter: () => realtimeUserStateAdapter, getRealtimeNote: () => this.realtimeNote, @@ -48,10 +92,10 @@ export class MockConnectionBuilder { this.realtimeNote.removeClient(connection), ); - if (this.includeRealtimeUserState) { + if (this.includeRealtimeUserStatus) { realtimeUserStateAdapter = new RealtimeUserStatusAdapter( - this.username, - this.username, + this.username ?? null, + displayName, connection, ); } @@ -60,4 +104,14 @@ export class MockConnectionBuilder { return connection; } + + private deriveDisplayName(): string { + if (this.displayName === undefined) { + throw new Error( + 'Neither withGuestUser nor withLoggedInUser has been called.', + ); + } + + return this.displayName; + } }