refactor: replace TypeORM with knex.js

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-03-14 23:33:29 +01:00
parent 6e151c8a1b
commit c0ce00b3f9
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
242 changed files with 4601 additions and 6871 deletions

View file

@ -11,8 +11,7 @@ import {
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
import { Mock } from 'ts-mockery';
import { User } from '../../database/user.entity';
import { Note } from '../../notes/note.entity';
import { FieldNameUser, User } from '../../database/types';
import * as NameRandomizerModule from './random-word-lists/name-randomizer';
import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note';
@ -41,12 +40,15 @@ describe('websocket connection', () => {
const mockedUserName: string = 'mocked-user-name';
const mockedDisplayName = 'mockedDisplayName';
const mockedAuthorStyle = 42;
beforeEach(() => {
mockedRealtimeNote = new RealtimeNote(Mock.of<Note>({}), '');
mockedRealtimeNote = new RealtimeNote(1, '');
mockedUser = Mock.of<User>({
username: mockedUserName,
displayName: mockedDisplayName,
[FieldNameUser.id]: 0,
[FieldNameUser.username]: mockedUserName,
[FieldNameUser.displayName]: mockedDisplayName,
[FieldNameUser.authorStyle]: mockedAuthorStyle,
});
mockedMessageTransporter = new MessageTransporter();
@ -61,7 +63,10 @@ describe('websocket connection', () => {
it('returns the correct transporter', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
@ -71,7 +76,10 @@ describe('websocket connection', () => {
it('returns the correct realtime note', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
@ -107,12 +115,14 @@ describe('websocket connection', () => {
(
username,
displayName,
authorStyle,
otherAdapterCollector: OtherAdapterCollector,
messageTransporter,
acceptCursorUpdateProvider,
) => {
expect(username).toBe(mockedUserName);
expect(displayName).toBe(mockedDisplayName);
expect(authorStyle).toBe(mockedAuthorStyle);
expect(otherAdapterCollector()).toStrictEqual([
realtimeUserStatus1,
realtimeUserStatus2,
@ -126,7 +136,10 @@ describe('websocket connection', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
acceptEdits,
);
@ -157,7 +170,10 @@ describe('websocket connection', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
acceptEdits,
);
@ -169,7 +185,10 @@ describe('websocket connection', () => {
it('removes the client from the note on transporter disconnect', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
@ -181,46 +200,59 @@ describe('websocket connection', () => {
expect(removeClientSpy).toHaveBeenCalledWith(sut);
});
it('saves the correct user', () => {
it('correctly return user id', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
expect(sut.getUser()).toBe(mockedUser);
expect(sut.getUserId()).toBe(mockedUser[FieldNameUser.id]);
});
it('returns the correct username', () => {
const mockedUserWithUsername = Mock.of<User>({ displayName: 'MockUser' });
it('correctly return username', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUserWithUsername,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
expect(sut.getDisplayName()).toBe('MockUser');
expect(sut.getUsername()).toBe(mockedUser[FieldNameUser.username]);
});
it('returns a random fallback display name if the provided user has no display name', () => {
const randomName = 'I am a random name';
jest
.spyOn(NameRandomizerModule, 'generateRandomName')
.mockReturnValue(randomName);
mockedUser = Mock.of<User>({});
it('correctly return displayName', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
expect(sut.getDisplayName()).toBe(randomName);
expect(sut.getDisplayName()).toBe(mockedUser[FieldNameUser.displayName]);
});
it('correctly return authorStyle', () => {
const sut = new RealtimeConnection(
mockedMessageTransporter,
mockedUser[FieldNameUser.id],
mockedUser[FieldNameUser.username],
mockedUser[FieldNameUser.displayName],
mockedUser[FieldNameUser.authorStyle],
mockedRealtimeNote,
true,
);
expect(sut.getAuthorStyle()).toBe(mockedUser[FieldNameUser.authorStyle]);
});
});

View file

@ -6,8 +6,6 @@
import { MessageTransporter, YDocSyncServerAdapter } from '@hedgedoc/commons';
import { Logger } from '@nestjs/common';
import { User } from '../../database/user.entity';
import { generateRandomName } from './random-word-lists/name-randomizer';
import { RealtimeNote } from './realtime-note';
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
@ -19,24 +17,28 @@ export class RealtimeConnection {
private readonly transporter: MessageTransporter;
private readonly yDocSyncAdapter: YDocSyncServerAdapter;
private readonly realtimeUserStateAdapter: RealtimeUserStatusAdapter;
private readonly displayName: string;
/**
* Instantiates the connection wrapper.
*
* @param messageTransporter The message transporter that handles the communication with the client.
* @param user The user of the client
* @param userId The id of the user of the client
* @param username The username of the user of the client
* @param displayName The displayName of the user of the client
* @param authorStyle The authorStyle of the user of the client
* @param realtimeNote The {@link RealtimeNote} that the client connected to.
* @param acceptEdits If edits by this connection should be accepted.
* @throws Error if the socket is not open
*/
constructor(
messageTransporter: MessageTransporter,
private user: User | null,
private userId: number,
private username: string | null,
private displayName: string,
private authorStyle: number,
private realtimeNote: RealtimeNote,
public acceptEdits: boolean,
) {
this.displayName = user?.displayName ?? generateRandomName();
this.transporter = messageTransporter;
this.transporter.on('disconnected', () => {
@ -48,8 +50,9 @@ export class RealtimeConnection {
() => acceptEdits,
);
this.realtimeUserStateAdapter = new RealtimeUserStatusAdapter(
this.user?.username ?? null,
this.getDisplayName(),
this.username ?? null,
this.displayName,
this.authorStyle,
() =>
this.realtimeNote
.getConnections()
@ -67,18 +70,26 @@ export class RealtimeConnection {
return this.transporter;
}
public getUser(): User | null {
return this.user;
}
public getSyncAdapter(): YDocSyncServerAdapter {
return this.yDocSyncAdapter;
}
public getUserId(): number {
return this.userId;
}
public getDisplayName(): string {
return this.displayName;
}
public getUsername(): string | null {
return this.username;
}
public getAuthorStyle(): number {
return this.authorStyle;
}
public getRealtimeNote(): RealtimeNote {
return this.realtimeNote;
}

View file

@ -5,7 +5,6 @@
*/
import { Injectable } from '@nestjs/common';
import { Note } from '../../notes/note.entity';
import { RealtimeNote } from './realtime-note';
@Injectable()
@ -15,29 +14,29 @@ export class RealtimeNoteStore {
/**
* Creates a new {@link RealtimeNote} for the given {@link Note} and memorizes it.
*
* @param note The note for which the realtime note should be created
* @param noteId The note for which the realtime note should be created
* @param initialTextContent the initial text content of realtime doc
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
* @throws Error if there is already an realtime note for the given note.
* @return The created realtime note
*/
public create(
note: Note,
noteId: number,
initialTextContent: string,
initialYjsState?: number[],
initialYjsState?: ArrayBuffer,
): RealtimeNote {
if (this.noteIdToRealtimeNote.has(note.id)) {
throw new Error(`Realtime note for note ${note.id} already exists.`);
if (this.noteIdToRealtimeNote.has(noteId)) {
throw new Error(`Realtime note for note ${noteId} already exists.`);
}
const realtimeNote = new RealtimeNote(
note,
noteId,
initialTextContent,
initialYjsState,
);
realtimeNote.on('destroy', () => {
this.noteIdToRealtimeNote.delete(note.id);
this.noteIdToRealtimeNote.delete(noteId);
});
this.noteIdToRealtimeNote.set(note.id, realtimeNote);
this.noteIdToRealtimeNote.set(noteId, realtimeNote);
return realtimeNote;
}

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { LoggerModule } from '../../logger/logger.module';
@ -18,7 +18,7 @@ import { RealtimeNoteService } from './realtime-note.service';
imports: [
LoggerModule,
UsersModule,
PermissionsModule,
forwardRef(() => PermissionsModule),
SessionModule,
RevisionsModule,
ScheduleModule.forRoot(),

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -10,8 +10,8 @@ import { AppConfig } from '../../config/app.config';
import { User } from '../../database/user.entity';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { Note } from '../../notes/note.entity';
import { NotePermission } from '../../permissions/note-permission.enum';
import { PermissionsService } from '../../permissions/permissions.service';
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
import { PermissionService } from '../../permissions/permission.service';
import { Revision } from '../../revisions/revision.entity';
import { RevisionsService } from '../../revisions/revisions.service';
import { RealtimeConnection } from './realtime-connection';
@ -29,7 +29,7 @@ describe('RealtimeNoteService', () => {
let realtimeNoteService: RealtimeNoteService;
let revisionsService: RevisionsService;
let realtimeNoteStore: RealtimeNoteStore;
let mockedPermissionService: PermissionsService;
let mockedPermissionService: PermissionService;
let consoleLoggerService: ConsoleLoggerService;
let mockedAppConfig: AppConfig;
let addIntervalSpy: jest.SpyInstance;
@ -91,14 +91,14 @@ describe('RealtimeNoteService', () => {
});
mockedAppConfig = Mock.of<AppConfig>({ persistInterval: 0 });
mockedPermissionService = Mock.of<PermissionsService>({
mockedPermissionService = Mock.of<PermissionService>({
determinePermission: async (user: User | null) => {
if (user?.username === readWriteUsername) {
return NotePermission.WRITE;
return NotePermissionLevel.WRITE;
} else if (user?.username === onlyReadUsername) {
return NotePermission.READ;
return NotePermissionLevel.READ;
} else {
return NotePermission.DENY;
return NotePermissionLevel.DENY;
}
},
});

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -9,11 +9,11 @@ import { OnEvent } from '@nestjs/event-emitter';
import { SchedulerRegistry } from '@nestjs/schedule';
import appConfiguration, { AppConfig } from '../../config/app.config';
import { FieldNameRevision } from '../../database/types';
import { NoteEvent } from '../../events';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { Note } from '../../notes/note.entity';
import { NotePermission } from '../../permissions/note-permission.enum';
import { PermissionsService } from '../../permissions/permissions.service';
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
import { PermissionService } from '../../permissions/permission.service';
import { RevisionsService } from '../../revisions/revisions.service';
import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note';
@ -28,7 +28,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
private schedulerRegistry: SchedulerRegistry,
@Inject(appConfiguration.KEY)
private appConfig: AppConfig,
private permissionService: PermissionsService,
private permissionService: PermissionService,
) {}
beforeApplicationShutdown(): void {
@ -44,9 +44,10 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
*/
public saveRealtimeNote(realtimeNote: RealtimeNote): void {
this.revisionsService
.createAndSaveRevision(
realtimeNote.getNote(),
.createRevision(
realtimeNote.getNoteId(),
realtimeNote.getRealtimeDoc().getCurrentContent(),
undefined,
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
)
.then(() => {
@ -57,30 +58,30 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
/**
* Creates or reuses a {@link RealtimeNote} that is handling the real time editing of the {@link Note} which is identified by the given note id.
* @param note The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
* @param noteId The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
* @throws NotInDBError if note doesn't exist or has no revisions.
* @return A {@link RealtimeNote} that is linked to the given note.
*/
public async getOrCreateRealtimeNote(note: Note): Promise<RealtimeNote> {
public async getOrCreateRealtimeNote(noteId: number): Promise<RealtimeNote> {
return (
this.realtimeNoteStore.find(note.id) ??
(await this.createNewRealtimeNote(note))
this.realtimeNoteStore.find(noteId) ??
(await this.createNewRealtimeNote(noteId))
);
}
/**
* Creates a new {@link RealtimeNote} for the given {@link Note}.
*
* @param note The note for which the realtime note should be created
* @param noteId The note for which the realtime note should be created
* @throws NotInDBError if note doesn't exist or has no revisions.
* @return The created realtime note
*/
private async createNewRealtimeNote(note: Note): Promise<RealtimeNote> {
const lastRevision = await this.revisionsService.getLatestRevision(note);
private async createNewRealtimeNote(noteId: number): Promise<RealtimeNote> {
const lastRevision = await this.revisionsService.getLatestRevision(noteId);
const realtimeNote = this.realtimeNoteStore.create(
note,
noteId,
lastRevision.content,
lastRevision.yjsStateVector ?? undefined,
lastRevision[FieldNameRevision.yjsStateVector] ?? undefined,
);
realtimeNote.on('beforeDestroy', () => {
this.saveRealtimeNote(realtimeNote);
@ -103,47 +104,47 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
persistInterval * 60 * 1000,
);
this.schedulerRegistry.addInterval(
`periodic-persist-${realtimeNote.getNote().id}`,
`periodic-persist-${realtimeNote.getNoteId()}`,
intervalId,
);
realtimeNote.on('destroy', () => {
clearInterval(intervalId);
this.schedulerRegistry.deleteInterval(
`periodic-persist-${realtimeNote.getNote().id}`,
`periodic-persist-${realtimeNote.getNoteId()}`,
);
});
});
}
@OnEvent(NoteEvent.PERMISSION_CHANGE)
public async handleNotePermissionChanged(note: Note): Promise<void> {
const realtimeNote = this.realtimeNoteStore.find(note.id);
public async handleNotePermissionChanged(noteId: number): Promise<void> {
const realtimeNote = this.realtimeNoteStore.find(noteId);
if (!realtimeNote) return;
realtimeNote.announceMetadataUpdate();
const allConnections = realtimeNote.getConnections();
await this.updateOrCloseConnection(allConnections, note);
await this.updateOrCloseConnection(allConnections, noteId);
}
private async updateOrCloseConnection(
connections: RealtimeConnection[],
note: Note,
noteId: number,
): Promise<void> {
for (const connection of connections) {
const permission = await this.permissionService.determinePermission(
connection.getUser(),
note,
connection.getUserId(),
noteId,
);
if (permission === NotePermission.DENY) {
if (permission === NotePermissionLevel.DENY) {
connection.getTransporter().disconnect();
} else {
connection.acceptEdits = permission > NotePermission.READ;
connection.acceptEdits = permission > NotePermissionLevel.READ;
}
}
}
@OnEvent(NoteEvent.DELETION)
public handleNoteDeleted(noteId: Note['id']): void {
public handleNoteDeleted(noteId: number): void {
const realtimeNote = this.realtimeNoteStore.find(noteId);
if (realtimeNote) {
realtimeNote.announceNoteDeletion();

View file

@ -29,7 +29,7 @@ describe('realtime note', () => {
it('can return the given note', () => {
const sut = new RealtimeNote(mockedNote, 'nothing');
expect(sut.getNote()).toBe(mockedNote);
expect(sut.getNoteId()).toBe(mockedNote);
});
it('can connect and disconnect clients', () => {

View file

@ -7,7 +7,6 @@ import { Message, MessageType, RealtimeDoc } from '@hedgedoc/commons';
import { Logger } from '@nestjs/common';
import { EventEmitter2, EventMap, Listener } from 'eventemitter2';
import { Note } from '../../notes/note.entity';
import { RealtimeConnection } from './realtime-connection';
export interface RealtimeNoteEventMap extends EventMap {
@ -33,16 +32,16 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
private destroyEventTimer: NodeJS.Timeout | null = null;
constructor(
private readonly note: Note,
private readonly noteId: number,
initialTextContent: string,
initialYjsState?: number[],
initialYjsState?: ArrayBuffer,
) {
super();
this.logger = new Logger(`${RealtimeNote.name} ${note.id}`);
this.logger = new Logger(`${RealtimeNote.name} ${noteId}`);
this.doc = new RealtimeDoc(initialTextContent, initialYjsState);
const length = this.doc.getCurrentContent().length;
this.logger.debug(
`New realtime session for note ${note.id} created. Length of initial content: ${length} characters`,
`New realtime session for note ${noteId} created. Length of initial content: ${length} characters`,
);
this.clientAddedListener = this.on(
'clientAdded',
@ -74,7 +73,7 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
/**
* Disconnects the given websocket client while cleaning-up if it was the last user in the realtime note.
*
* @param {WebSocket} client The websocket client that disconnects.
* @param client The websocket client that disconnects.
*/
public removeClient(client: RealtimeConnection): void {
this.clients.delete(client);
@ -144,8 +143,8 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
*
* @return the {@link Note note}
*/
public getNote(): Note {
return this.note;
public getNoteId(): number {
return this.noteId;
}
/**

View file

@ -22,6 +22,7 @@ export class RealtimeUserStatusAdapter {
constructor(
private readonly username: string | null,
private readonly displayName: string,
private readonly authorStyle: number,
private collectOtherAdapters: OtherAdapterCollector,
private messageTransporter: MessageTransporter,
private acceptCursorUpdateProvider: () => boolean,
@ -35,9 +36,7 @@ export class RealtimeUserStatusAdapter {
username: this.username,
displayName: this.displayName,
active: true,
styleIndex: this.findLeastUsedStyleIndex(
this.createStyleIndexToCountMap(),
),
styleIndex: this.authorStyle,
cursor: !this.acceptCursorUpdateProvider()
? null
: {

View file

@ -9,7 +9,7 @@ import {
} from '@hedgedoc/commons';
import { Mock } from 'ts-mockery';
import { User } from '../../../database/user.entity';
import { FieldNameUser, User } from '../../../database/types';
import { RealtimeConnection } from '../realtime-connection';
import { RealtimeNote } from '../realtime-note';
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
@ -21,14 +21,14 @@ enum RealtimeUserState {
WITH_READONLY,
}
const MOCK_FALLBACK_USERNAME: string = 'mock';
/**
* Creates a mocked {@link RealtimeConnection realtime connection}.
*/
export class MockConnectionBuilder {
private userId: number;
private username: string | null;
private displayName: string | undefined;
private displayName: string;
private authorStyle: number;
private includeRealtimeUserStatus: RealtimeUserState =
RealtimeUserState.WITHOUT;
@ -42,6 +42,8 @@ export class MockConnectionBuilder {
public withGuestUser(displayName: string): this {
this.username = null;
this.displayName = displayName;
this.authorStyle = 8;
this.userId = 1000;
return this;
}
@ -50,10 +52,11 @@ export class MockConnectionBuilder {
*
* @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;
public withLoggedInUser(username: string): this {
this.username = username;
this.displayName = username;
this.userId = 1001;
this.authorStyle = 1;
return this;
}
@ -80,16 +83,15 @@ export class MockConnectionBuilder {
* @throws Error if neither withGuestUser nor withLoggedInUser has been called.
*/
public build(): RealtimeConnection {
const displayName = this.deriveDisplayName();
const transporter = new MockMessageTransporter();
transporter.setAdapter(new MockedBackendTransportAdapter(''));
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
? Mock.of<RealtimeUserStatusAdapter>({})
: new RealtimeUserStatusAdapter(
this.username ?? null,
displayName,
this.username,
this.displayName,
this.authorStyle,
() =>
this.realtimeNote
.getConnections()
@ -100,18 +102,19 @@ export class MockConnectionBuilder {
RealtimeUserState.WITH_READWRITE,
);
const mockUser =
this.username === null
? null
: Mock.of<User>({
username: this.username,
displayName: this.displayName,
});
const mockUser = Mock.of<User>({
[FieldNameUser.username]: this.username,
[FieldNameUser.displayName]: this.displayName,
[FieldNameUser.id]: this.userId,
[FieldNameUser.authorStyle]: this.authorStyle,
});
const yDocSyncServerAdapter = Mock.of<YDocSyncServerAdapter>({});
const connection = Mock.of<RealtimeConnection>({
getUser: jest.fn(() => mockUser),
getDisplayName: jest.fn(() => displayName),
getUserId: jest.fn(() => mockUser[FieldNameUser.id]),
getUsername: jest.fn(() => mockUser[FieldNameUser.username]),
getAuthorStyle: jest.fn(() => mockUser[FieldNameUser.authorStyle]),
getDisplayName: jest.fn(() => mockUser[FieldNameUser.displayName]),
getSyncAdapter: jest.fn(() => yDocSyncServerAdapter),
getTransporter: jest.fn(() => transporter),
getRealtimeUserStateAdapter: () => realtimeUserStateAdapter,
@ -129,14 +132,4 @@ 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;
}
}