refactor: test code of realtime

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-03-31 21:42:11 +02:00
parent 3b2ded6e46
commit 2a2d3756ad
3 changed files with 183 additions and 106 deletions

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -29,7 +29,7 @@ describe('realtime note', () => {
it('can connect and disconnect clients', () => { it('can connect and disconnect clients', () => {
const sut = new RealtimeNote(mockedNote, 'nothing'); 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.getConnections()).toStrictEqual([client1]);
expect(sut.hasConnections()).toBeTruthy(); expect(sut.hasConnections()).toBeTruthy();
sut.removeClient(client1); sut.removeClient(client1);
@ -70,8 +70,8 @@ describe('realtime note', () => {
it('announcePermissionChange to all clients', () => { it('announcePermissionChange to all clients', () => {
const sut = new RealtimeNote(mockedNote, 'nothing'); const sut = new RealtimeNote(mockedNote, 'nothing');
const client1 = new MockConnectionBuilder(sut).build(); const client1 = new MockConnectionBuilder(sut).withLoggedInUser().build();
const client2 = new MockConnectionBuilder(sut).build(); const client2 = new MockConnectionBuilder(sut).withLoggedInUser().build();
const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage');
const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage');
@ -88,8 +88,8 @@ describe('realtime note', () => {
it('announceNoteDeletion to all clients', () => { it('announceNoteDeletion to all clients', () => {
const sut = new RealtimeNote(mockedNote, 'nothing'); const sut = new RealtimeNote(mockedNote, 'nothing');
const client1 = new MockConnectionBuilder(sut).build(); const client1 = new MockConnectionBuilder(sut).withLoggedInUser().build();
const client2 = new MockConnectionBuilder(sut).build(); const client2 = new MockConnectionBuilder(sut).withLoggedInUser().build();
const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); const sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage');
const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); const sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage');

View file

@ -17,70 +17,76 @@ type SendMessageSpy = jest.SpyInstance<
>; >;
describe('realtime user status adapter', () => { describe('realtime user status adapter', () => {
let client1: RealtimeConnection; let clientLoggedIn1: RealtimeConnection;
let client2: RealtimeConnection; let clientLoggedIn2: RealtimeConnection;
let client3: RealtimeConnection; let clientGuest: RealtimeConnection;
let client4: RealtimeConnection; let clientNotReady: RealtimeConnection;
let sendMessage1Spy: SendMessageSpy; let clientLoggedIn1SendMessageSpy: SendMessageSpy;
let sendMessage2Spy: SendMessageSpy; let clientLoggedIn2SendMessageSpy: SendMessageSpy;
let sendMessage3Spy: SendMessageSpy; let clientGuestSendMessageSpy: SendMessageSpy;
let sendMessage4Spy: SendMessageSpy; let clientNotReadySendMessageSpy: SendMessageSpy;
let realtimeNote: RealtimeNote; let realtimeNote: RealtimeNote;
const username1 = 'mock1'; const clientLoggedIn1Username = 'logged.in1';
const username2 = 'mock2'; const clientLoggedIn2Username = 'logged.in2';
const username3 = 'mock3'; const clientNotReadyUsername = 'not.ready';
const username4 = 'mock4';
const guestDisplayName = 'Virtuous Mockingbird';
function spyOnSendMessage(connection: RealtimeConnection): jest.SpyInstance {
return jest.spyOn(connection.getTransporter(), 'sendMessage');
}
beforeEach(() => { beforeEach(() => {
realtimeNote = new RealtimeNote( realtimeNote = new RealtimeNote(
Mock.of<Note>({ id: 9876 }), Mock.of<Note>({ id: 9876 }),
'mockedContent', 'mockedContent',
); );
client1 = new MockConnectionBuilder(realtimeNote) clientLoggedIn1 = new MockConnectionBuilder(realtimeNote)
.withRealtimeUserState() .withRealtimeUserStatus()
.withUsername(username1) .withLoggedInUser(clientLoggedIn1Username)
.build(); .build();
client2 = new MockConnectionBuilder(realtimeNote) clientLoggedIn2 = new MockConnectionBuilder(realtimeNote)
.withRealtimeUserState() .withRealtimeUserStatus()
.withUsername(username2) .withLoggedInUser(clientLoggedIn2Username)
.build(); .build();
client3 = new MockConnectionBuilder(realtimeNote) clientGuest = new MockConnectionBuilder(realtimeNote)
.withRealtimeUserState() .withRealtimeUserStatus()
.withUsername(username3) .withGuestUser(guestDisplayName)
.build(); .build();
client4 = new MockConnectionBuilder(realtimeNote) clientNotReady = new MockConnectionBuilder(realtimeNote)
.withRealtimeUserState() .withRealtimeUserStatus()
.withUsername(username4) .withLoggedInUser(clientNotReadyUsername)
.build(); .build();
sendMessage1Spy = jest.spyOn(client1.getTransporter(), 'sendMessage'); clientLoggedIn1SendMessageSpy = spyOnSendMessage(clientLoggedIn1);
sendMessage2Spy = jest.spyOn(client2.getTransporter(), 'sendMessage'); clientLoggedIn2SendMessageSpy = spyOnSendMessage(clientLoggedIn2);
sendMessage3Spy = jest.spyOn(client3.getTransporter(), 'sendMessage'); clientGuestSendMessageSpy = spyOnSendMessage(clientGuest);
sendMessage4Spy = jest.spyOn(client4.getTransporter(), 'sendMessage'); clientNotReadySendMessageSpy = spyOnSendMessage(clientNotReady);
client1.getTransporter().sendReady(); clientLoggedIn1.getTransporter().sendReady();
client2.getTransporter().sendReady(); clientLoggedIn2.getTransporter().sendReady();
client3.getTransporter().sendReady(); clientGuest.getTransporter().sendReady();
//client 4 shouldn't be ready on purpose
}); });
it('can answer a state request', () => { it('can answer a state request', () => {
expect(sendMessage1Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage2Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage3Spy).toHaveBeenCalledTimes(0); expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage4Spy).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<MessageType.REALTIME_USER_STATE_SET> = { const expectedMessage1: Message<MessageType.REALTIME_USER_STATE_SET> = {
type: MessageType.REALTIME_USER_STATE_SET, type: MessageType.REALTIME_USER_STATE_SET,
payload: { payload: {
ownUser: { ownUser: {
styleIndex: 0, styleIndex: 0,
displayName: username1, displayName: clientLoggedIn1Username,
}, },
users: [ users: [
{ {
@ -90,8 +96,8 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 1, styleIndex: 1,
username: username2, username: clientLoggedIn2Username,
displayName: username2, displayName: clientLoggedIn2Username,
}, },
{ {
active: true, active: true,
@ -100,41 +106,46 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 2, styleIndex: 2,
username: username3, username: null,
displayName: username3, displayName: guestDisplayName,
}, },
], ],
}, },
}; };
expect(sendMessage1Spy).toHaveBeenNthCalledWith(1, expectedMessage1); expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
expect(sendMessage2Spy).toHaveBeenCalledTimes(0); 1,
expect(sendMessage3Spy).toHaveBeenCalledTimes(0); expectedMessage1,
expect(sendMessage4Spy).toHaveBeenCalledTimes(0); );
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
}); });
it('can save an cursor update', () => { it('can save an cursor update', () => {
expect(sendMessage1Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage2Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage3Spy).toHaveBeenCalledTimes(0); expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage4Spy).toHaveBeenCalledTimes(0); expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
const newFrom = Math.floor(Math.random() * 100); const newFrom = Math.floor(Math.random() * 100);
const newTo = Math.floor(Math.random() * 100); const newTo = Math.floor(Math.random() * 100);
client1.getTransporter().emit(MessageType.REALTIME_USER_SINGLE_UPDATE, { clientLoggedIn1
type: MessageType.REALTIME_USER_SINGLE_UPDATE, .getTransporter()
payload: { .emit(MessageType.REALTIME_USER_SINGLE_UPDATE, {
from: newFrom, type: MessageType.REALTIME_USER_SINGLE_UPDATE,
to: newTo, payload: {
}, from: newFrom,
}); to: newTo,
},
});
const expectedMessage2: Message<MessageType.REALTIME_USER_STATE_SET> = { const expectedMessage2: Message<MessageType.REALTIME_USER_STATE_SET> = {
type: MessageType.REALTIME_USER_STATE_SET, type: MessageType.REALTIME_USER_STATE_SET,
payload: { payload: {
ownUser: { ownUser: {
styleIndex: 1, styleIndex: 1,
displayName: username2, displayName: clientLoggedIn2Username,
}, },
users: [ users: [
{ {
@ -144,8 +155,8 @@ describe('realtime user status adapter', () => {
to: newTo, to: newTo,
}, },
styleIndex: 0, styleIndex: 0,
username: username1, username: clientLoggedIn1Username,
displayName: username1, displayName: clientLoggedIn1Username,
}, },
{ {
active: true, active: true,
@ -154,8 +165,8 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 2, styleIndex: 2,
username: username3, username: null,
displayName: username3, displayName: guestDisplayName,
}, },
], ],
}, },
@ -166,7 +177,7 @@ describe('realtime user status adapter', () => {
payload: { payload: {
ownUser: { ownUser: {
styleIndex: 2, styleIndex: 2,
displayName: username3, displayName: guestDisplayName,
}, },
users: [ users: [
{ {
@ -176,8 +187,8 @@ describe('realtime user status adapter', () => {
to: newTo, to: newTo,
}, },
styleIndex: 0, styleIndex: 0,
username: username1, username: clientLoggedIn1Username,
displayName: username1, displayName: clientLoggedIn1Username,
}, },
{ {
active: true, active: true,
@ -186,33 +197,39 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 1, styleIndex: 1,
username: username2, username: clientLoggedIn2Username,
displayName: username2, displayName: clientLoggedIn2Username,
}, },
], ],
}, },
}; };
expect(sendMessage1Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage2Spy).toHaveBeenNthCalledWith(1, expectedMessage2); expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
expect(sendMessage3Spy).toHaveBeenNthCalledWith(1, expectedMessage3); 1,
expect(sendMessage4Spy).toHaveBeenCalledTimes(0); expectedMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
expectedMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
}); });
it('will inform other clients about removed client', () => { it('will inform other clients about removed client', () => {
expect(sendMessage1Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage2Spy).toHaveBeenCalledTimes(0); expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage3Spy).toHaveBeenCalledTimes(0); expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(sendMessage4Spy).toHaveBeenCalledTimes(0); expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
client2.getTransporter().disconnect(); clientLoggedIn2.getTransporter().disconnect();
const expectedMessage1: Message<MessageType.REALTIME_USER_STATE_SET> = { const expectedMessage1: Message<MessageType.REALTIME_USER_STATE_SET> = {
type: MessageType.REALTIME_USER_STATE_SET, type: MessageType.REALTIME_USER_STATE_SET,
payload: { payload: {
ownUser: { ownUser: {
styleIndex: 0, styleIndex: 0,
displayName: username1, displayName: clientLoggedIn1Username,
}, },
users: [ users: [
{ {
@ -222,8 +239,8 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 2, styleIndex: 2,
username: username3, username: null,
displayName: username3, displayName: guestDisplayName,
}, },
], ],
}, },
@ -234,7 +251,7 @@ describe('realtime user status adapter', () => {
payload: { payload: {
ownUser: { ownUser: {
styleIndex: 2, styleIndex: 2,
displayName: username3, displayName: guestDisplayName,
}, },
users: [ users: [
{ {
@ -244,16 +261,22 @@ describe('realtime user status adapter', () => {
to: 0, to: 0,
}, },
styleIndex: 0, styleIndex: 0,
username: username1, username: clientLoggedIn1Username,
displayName: username1, displayName: clientLoggedIn1Username,
}, },
], ],
}, },
}; };
expect(sendMessage1Spy).toHaveBeenNthCalledWith(1, expectedMessage1); expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
expect(sendMessage2Spy).toHaveBeenCalledTimes(0); 1,
expect(sendMessage3Spy).toHaveBeenNthCalledWith(1, expectedMessage3); expectedMessage1,
expect(sendMessage4Spy).toHaveBeenCalledTimes(0); );
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
expectedMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
}); });
}); });

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -14,31 +14,75 @@ import { RealtimeConnection } from '../realtime-connection';
import { RealtimeNote } from '../realtime-note'; import { RealtimeNote } from '../realtime-note';
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter'; import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
const MOCK_FALLBACK_USERNAME = 'mock';
/**
* Creates a mocked {@link RealtimeConnection realtime connection}.
*/
export class MockConnectionBuilder { export class MockConnectionBuilder {
private username = 'mock'; private username: string | null;
private includeRealtimeUserState = false; private displayName: string | undefined;
private includeRealtimeUserStatus = false;
constructor(private readonly realtimeNote: RealtimeNote) {} 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; 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; 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 { public build(): RealtimeConnection {
const displayName = this.deriveDisplayName();
const transporter = new MockedBackendMessageTransporter(''); const transporter = new MockedBackendMessageTransporter('');
let realtimeUserStateAdapter: RealtimeUserStatusAdapter = let realtimeUserStateAdapter: RealtimeUserStatusAdapter =
Mock.of<RealtimeUserStatusAdapter>(); Mock.of<RealtimeUserStatusAdapter>({});
const mockUser =
this.username === null
? null
: Mock.of<User>({
username: this.username,
displayName: this.displayName,
});
const yDocSyncServerAdapter = Mock.of<YDocSyncServerAdapter>({});
const connection = Mock.of<RealtimeConnection>({ const connection = Mock.of<RealtimeConnection>({
getUser: jest.fn(() => Mock.of<User>({ username: this.username })), getUser: jest.fn(() => mockUser),
getDisplayName: jest.fn(() => this.username), getDisplayName: jest.fn(() => displayName),
getSyncAdapter: jest.fn(() => Mock.of<YDocSyncServerAdapter>({})), getSyncAdapter: jest.fn(() => yDocSyncServerAdapter),
getTransporter: jest.fn(() => transporter), getTransporter: jest.fn(() => transporter),
getRealtimeUserStateAdapter: () => realtimeUserStateAdapter, getRealtimeUserStateAdapter: () => realtimeUserStateAdapter,
getRealtimeNote: () => this.realtimeNote, getRealtimeNote: () => this.realtimeNote,
@ -48,10 +92,10 @@ export class MockConnectionBuilder {
this.realtimeNote.removeClient(connection), this.realtimeNote.removeClient(connection),
); );
if (this.includeRealtimeUserState) { if (this.includeRealtimeUserStatus) {
realtimeUserStateAdapter = new RealtimeUserStatusAdapter( realtimeUserStateAdapter = new RealtimeUserStatusAdapter(
this.username, this.username ?? null,
this.username, displayName,
connection, connection,
); );
} }
@ -60,4 +104,14 @@ export class MockConnectionBuilder {
return connection; return connection;
} }
private deriveDisplayName(): string {
if (this.displayName === undefined) {
throw new Error(
'Neither withGuestUser nor withLoggedInUser has been called.',
);
}
return this.displayName;
}
} }