711 lines
22 KiB
TypeScript
711 lines
22 KiB
TypeScript
import { Injector, signal } from '@angular/core';
|
|
import { Store } from '@ngrx/store';
|
|
import { Subject } from 'rxjs';
|
|
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
|
import { DatabaseService } from '../../../../infrastructure/persistence';
|
|
import { ServerDirectoryFacade } from '../../../server-directory';
|
|
import { AttachmentFacade } from '../../../attachment';
|
|
import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade';
|
|
import type {
|
|
Channel,
|
|
Message,
|
|
Room,
|
|
TojuPluginManifest,
|
|
User
|
|
} from '../../../../shared-kernel';
|
|
import { MessagesActions } from '../../../../store/messages/messages.actions';
|
|
import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors';
|
|
import {
|
|
selectActiveChannelId,
|
|
selectCurrentRoom,
|
|
selectCurrentRoomChannels,
|
|
selectCurrentRoomId
|
|
} from '../../../../store/rooms/rooms.selectors';
|
|
import { UsersActions } from '../../../../store/users/users.actions';
|
|
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
|
|
import {
|
|
PLUGIN_CLIENT_API_CAPABILITY_REQUIREMENTS,
|
|
assertPluginApiSurfaceImplemented,
|
|
collectPluginApiMethodPaths,
|
|
type PluginClientApiMethodPath
|
|
} from '../../domain/logic/plugin-client-api-surface.rules';
|
|
import { PluginCapabilityError, PluginCapabilityService } from './plugin-capability.service';
|
|
import { MessageRevisionService } from '../../../chat/application/services/message-revision.service';
|
|
import { MessageSigningService } from '../../../authentication/application/services/message-signing.service';
|
|
import { PluginClientApiService } from './plugin-client-api.service';
|
|
import { PluginDesktopStateService } from './plugin-desktop-state.service';
|
|
import { PluginLoggerService } from './plugin-logger.service';
|
|
import { PluginMessageBusService } from './plugin-message-bus.service';
|
|
import { PluginStorageService } from './plugin-storage.service';
|
|
import { PluginUiRegistryService } from './plugin-ui-registry.service';
|
|
|
|
const TEST_MANIFEST = createTestManifest();
|
|
|
|
describe('PluginClientApiService', () => {
|
|
let context: ServiceTestContext;
|
|
|
|
beforeEach(() => {
|
|
context = createServiceTestContext();
|
|
context.capabilities.grantAll(TEST_MANIFEST);
|
|
});
|
|
|
|
it('implements the full public plugin API surface', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
expect(() => assertPluginApiSurfaceImplemented(api as Record<string, Record<string, unknown>>)).not.toThrow();
|
|
});
|
|
|
|
it('freezes the API object returned to plugins', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
expect(Object.isFrozen(api)).toBe(true);
|
|
expect(Object.isFrozen(api.commands)).toBe(true);
|
|
expect(Object.isFrozen(api.messages)).toBe(true);
|
|
});
|
|
|
|
it('exposes current interaction context without capability checks', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
expect(api.context.getCurrent()).toEqual({
|
|
server: context.room(),
|
|
source: 'manual',
|
|
textChannel: context.channels().find((channel) => channel.id === 'general') ?? null,
|
|
user: context.currentUser(),
|
|
voiceChannel: null
|
|
});
|
|
});
|
|
|
|
it('routes logger calls to the plugin logger service', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
api.logger.debug('debug');
|
|
api.logger.error('error');
|
|
api.logger.info('info');
|
|
api.logger.warn('warn');
|
|
|
|
expect(context.logger.debug).toHaveBeenCalledWith(TEST_MANIFEST.id, 'debug', undefined);
|
|
expect(context.logger.error).toHaveBeenCalledWith(TEST_MANIFEST.id, 'error', undefined);
|
|
expect(context.logger.info).toHaveBeenCalledWith(TEST_MANIFEST.id, 'info', undefined);
|
|
expect(context.logger.warn).toHaveBeenCalledWith(TEST_MANIFEST.id, 'warn', undefined);
|
|
});
|
|
|
|
it('dispatches profile updates through the users store', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
api.profile.update({ displayName: 'Plugin User' });
|
|
api.profile.updateAvatar({
|
|
avatarHash: 'hash',
|
|
avatarMime: 'image/png',
|
|
avatarUrl: '/avatar.png'
|
|
});
|
|
|
|
expect(context.store.dispatch).toHaveBeenCalledWith(UsersActions.updateCurrentUserProfile({
|
|
profile: expect.objectContaining({ displayName: 'Plugin User' })
|
|
}));
|
|
|
|
expect(context.store.dispatch).toHaveBeenCalledWith(UsersActions.updateCurrentUserAvatar({
|
|
avatar: expect.objectContaining({ avatarUrl: '/avatar.png' })
|
|
}));
|
|
});
|
|
|
|
it('sends plugin messages and broadcasts them to peers', async () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
const message = api.messages.send('hello plugin');
|
|
|
|
expect(message.content).toBe('hello plugin');
|
|
expect(message.roomId).toBe('room-1');
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
expect(context.store.dispatch).toHaveBeenCalledWith(
|
|
MessagesActions.sendMessageSuccess({
|
|
message: expect.objectContaining({
|
|
content: 'hello plugin',
|
|
roomId: 'room-1'
|
|
})
|
|
})
|
|
);
|
|
expect(context.voice.broadcastMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'chat-message',
|
|
message: expect.objectContaining({
|
|
content: 'hello plugin',
|
|
roomId: 'room-1'
|
|
})
|
|
}));
|
|
expect(context.messageRevisions.broadcastRevision).toHaveBeenCalled();
|
|
});
|
|
|
|
it('publishes typing state through the realtime facade', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
api.messages.setTyping(true, 'general');
|
|
|
|
expect(context.realtime.sendRawMessage).toHaveBeenCalledWith({
|
|
type: 'typing',
|
|
serverId: 'room-1',
|
|
channelId: 'general',
|
|
isTyping: true
|
|
});
|
|
});
|
|
|
|
it('forwards slash command registration to the UI registry', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
const contribution = {
|
|
name: 'echo',
|
|
run: () => {}
|
|
};
|
|
|
|
api.commands.register('echo', contribution);
|
|
|
|
expect(context.uiRegistry.registerSlashCommand).toHaveBeenCalledWith(
|
|
TEST_MANIFEST.id,
|
|
'echo',
|
|
contribution
|
|
);
|
|
});
|
|
|
|
it('lists slash commands from the UI registry', () => {
|
|
const commands = [{ name: 'echo', run: () => {} }];
|
|
|
|
context.uiRegistry.slashCommands.mockReturnValue(commands);
|
|
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
expect(api.commands.list()).toBe(commands);
|
|
});
|
|
|
|
it('routes storage APIs through the plugin storage service', async () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
api.storage.set('key', { ok: true });
|
|
api.storage.get('key');
|
|
api.storage.remove('key');
|
|
await api.clientData.write('client-key', { ok: true });
|
|
await api.clientData.read('client-key');
|
|
await api.clientData.remove('client-key');
|
|
await api.serverData.write('server-key', { ok: true });
|
|
await api.serverData.read('server-key');
|
|
await api.serverData.remove('server-key');
|
|
|
|
expect(context.storage.setLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key', { ok: true });
|
|
expect(context.storage.getLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key');
|
|
expect(context.storage.removeLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key');
|
|
expect(context.storage.writeClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key', { ok: true });
|
|
expect(context.storage.readClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key');
|
|
expect(context.storage.removeClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key');
|
|
expect(context.storage.writeServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key', { ok: true });
|
|
expect(context.storage.readServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key');
|
|
expect(context.storage.removeServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key');
|
|
});
|
|
|
|
it('publishes declared server events through the realtime facade', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
api.events.publishServer('e2e:server', { ok: true });
|
|
|
|
expect(context.realtime.sendRawMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: 'plugin_event',
|
|
eventName: 'e2e:server',
|
|
payload: { ok: true },
|
|
pluginId: TEST_MANIFEST.id,
|
|
serverId: 'room-1'
|
|
}));
|
|
});
|
|
|
|
it('rejects undeclared plugin events', () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
expect(() => api.events.publishServer('missing:event', {})).toThrow(/did not declare event/);
|
|
});
|
|
|
|
it('enforces capability grants for privileged API methods', async () => {
|
|
const api = context.service.createApi(TEST_MANIFEST);
|
|
|
|
context.capabilities.revokeAll(TEST_MANIFEST.id);
|
|
|
|
for (const path of collectPluginApiMethodPaths()) {
|
|
const capability = PLUGIN_CLIENT_API_CAPABILITY_REQUIREMENTS[path as PluginClientApiMethodPath];
|
|
|
|
if (!capability) {
|
|
continue;
|
|
}
|
|
|
|
const [namespace, method] = path.split('.') as [keyof typeof api, string];
|
|
const target = api[namespace] as Record<string, (...args: unknown[]) => unknown>;
|
|
|
|
await expect(invokeApiMethod(target[method], path)).rejects.toThrow(PluginCapabilityError);
|
|
}
|
|
});
|
|
});
|
|
|
|
interface ServiceTestContext {
|
|
capabilities: PluginCapabilityService;
|
|
channels: ReturnType<typeof signal<Channel[]>>;
|
|
currentUser: ReturnType<typeof signal<User | null>>;
|
|
logger: {
|
|
debug: ReturnType<typeof vi.fn>;
|
|
error: ReturnType<typeof vi.fn>;
|
|
info: ReturnType<typeof vi.fn>;
|
|
warn: ReturnType<typeof vi.fn>;
|
|
};
|
|
messages: ReturnType<typeof signal<Message[]>>;
|
|
realtime: {
|
|
onSignalingMessage: Subject<unknown>;
|
|
sendRawMessage: ReturnType<typeof vi.fn>;
|
|
};
|
|
room: ReturnType<typeof signal<Room | null>>;
|
|
service: PluginClientApiService;
|
|
storage: {
|
|
getLocal: ReturnType<typeof vi.fn>;
|
|
readClientData: ReturnType<typeof vi.fn>;
|
|
readServerData: ReturnType<typeof vi.fn>;
|
|
removeClientData: ReturnType<typeof vi.fn>;
|
|
removeLocal: ReturnType<typeof vi.fn>;
|
|
removeServerData: ReturnType<typeof vi.fn>;
|
|
setLocal: ReturnType<typeof vi.fn>;
|
|
writeClientData: ReturnType<typeof vi.fn>;
|
|
writeServerData: ReturnType<typeof vi.fn>;
|
|
};
|
|
store: {
|
|
dispatch: ReturnType<typeof vi.fn>;
|
|
};
|
|
uiRegistry: {
|
|
registerSlashCommand: ReturnType<typeof vi.fn>;
|
|
slashCommands: ReturnType<typeof vi.fn>;
|
|
};
|
|
voice: {
|
|
broadcastMessage: ReturnType<typeof vi.fn>;
|
|
getConnectedPeers: ReturnType<typeof vi.fn>;
|
|
setInputVolume: ReturnType<typeof vi.fn>;
|
|
setLocalStream: ReturnType<typeof vi.fn>;
|
|
setOutputVolume: ReturnType<typeof vi.fn>;
|
|
};
|
|
messageRevisions: {
|
|
broadcastRevision: ReturnType<typeof vi.fn>;
|
|
createSignedRevision: ReturnType<typeof vi.fn>;
|
|
persistRevision: ReturnType<typeof vi.fn>;
|
|
};
|
|
}
|
|
|
|
function createServiceTestContext(): ServiceTestContext {
|
|
installLocalStorageMock();
|
|
installBrowserMediaMocks();
|
|
|
|
const currentUser = signal<User | null>(createUser());
|
|
const users = signal<User[]>(currentUser() ? [currentUser() as User] : []);
|
|
const room = signal<Room | null>(createRoom());
|
|
const channels = signal<Channel[]>(room()?.channels ?? []);
|
|
const messages = signal<Message[]>([]);
|
|
const activeChannelId = signal('general');
|
|
const roomId = signal('room-1');
|
|
const logger = {
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn()
|
|
};
|
|
const storage = {
|
|
getLocal: vi.fn(),
|
|
setLocal: vi.fn(),
|
|
removeLocal: vi.fn(),
|
|
readClientData: vi.fn(async () => null),
|
|
writeClientData: vi.fn(async () => undefined),
|
|
removeClientData: vi.fn(async () => undefined),
|
|
readServerData: vi.fn(async () => null),
|
|
writeServerData: vi.fn(async () => undefined),
|
|
removeServerData: vi.fn(async () => undefined)
|
|
};
|
|
const uiRegistry = {
|
|
registerSlashCommand: vi.fn(() => ({ dispose: vi.fn() })),
|
|
slashCommands: vi.fn(() => [])
|
|
};
|
|
const voice = {
|
|
broadcastMessage: vi.fn(),
|
|
getConnectedPeers: vi.fn(() => []),
|
|
setInputVolume: vi.fn(),
|
|
setLocalStream: vi.fn(async () => undefined),
|
|
setOutputVolume: vi.fn()
|
|
};
|
|
const messageRevisions = {
|
|
createSignedRevision: vi.fn(async (input: Parameters<MessageRevisionService['createSignedRevision']>[0]) => ({
|
|
messageId: input.message.id,
|
|
revision: input.type === 'create' ? 0 : (input.message.revision ?? 0) + 1,
|
|
prevRevisionHash: input.type === 'create' ? '' : (input.message.headHash ?? ''),
|
|
headHash: 'test-head-hash',
|
|
type: input.type,
|
|
actorId: input.actorId,
|
|
senderId: input.message.senderId,
|
|
roomId: input.message.roomId,
|
|
channelId: input.message.channelId,
|
|
senderName: input.message.senderName,
|
|
content: input.content ?? input.message.content,
|
|
editedAt: input.editedAt,
|
|
isDeleted: input.isDeleted ?? false,
|
|
replyToId: input.message.replyToId,
|
|
pluginId: input.pluginId,
|
|
signature: input.sign === false ? undefined : 'test-signature'
|
|
})),
|
|
persistRevision: vi.fn(async () => undefined),
|
|
broadcastRevision: vi.fn()
|
|
};
|
|
const realtime = {
|
|
onSignalingMessage: new Subject<unknown>(),
|
|
sendRawMessage: vi.fn()
|
|
};
|
|
const store = {
|
|
dispatch: vi.fn(),
|
|
selectSignal: vi.fn((selector: unknown) => {
|
|
if (selector === selectCurrentUser) {
|
|
return currentUser;
|
|
}
|
|
|
|
if (selector === selectAllUsers) {
|
|
return users;
|
|
}
|
|
|
|
if (selector === selectCurrentRoom) {
|
|
return room;
|
|
}
|
|
|
|
if (selector === selectCurrentRoomChannels) {
|
|
return channels;
|
|
}
|
|
|
|
if (selector === selectCurrentRoomMessages) {
|
|
return messages;
|
|
}
|
|
|
|
if (selector === selectActiveChannelId) {
|
|
return activeChannelId;
|
|
}
|
|
|
|
if (selector === selectCurrentRoomId) {
|
|
return roomId;
|
|
}
|
|
|
|
throw new Error(`Unexpected selector in PluginClientApiService test: ${String(selector)}`);
|
|
})
|
|
};
|
|
const injector = Injector.create({
|
|
providers: [
|
|
PluginClientApiService,
|
|
PluginCapabilityService,
|
|
{
|
|
provide: AttachmentFacade,
|
|
useValue: {
|
|
publishAttachments: vi.fn(async () => undefined),
|
|
rememberMessageRoom: vi.fn()
|
|
}
|
|
},
|
|
{
|
|
provide: DatabaseService,
|
|
useValue: {
|
|
getMessageById: vi.fn(async () => null),
|
|
saveMessage: vi.fn(async () => undefined),
|
|
updateMessage: vi.fn(async () => undefined),
|
|
updateRoom: vi.fn(async () => undefined)
|
|
}
|
|
},
|
|
{
|
|
provide: MessageRevisionService,
|
|
useValue: messageRevisions
|
|
},
|
|
{
|
|
provide: MessageSigningService,
|
|
useValue: {
|
|
signRevision: vi.fn(async () => 'test-signature'),
|
|
fetchSigningPublicKey: vi.fn(async () => null),
|
|
verifyRevisionSignature: vi.fn(async () => true)
|
|
}
|
|
},
|
|
{
|
|
provide: PluginDesktopStateService,
|
|
useValue: {
|
|
readJson: vi.fn(async () => null),
|
|
writeJson: vi.fn(async () => undefined)
|
|
}
|
|
},
|
|
{
|
|
provide: PluginLoggerService,
|
|
useValue: logger
|
|
},
|
|
{
|
|
provide: PluginMessageBusService,
|
|
useValue: {
|
|
publish: vi.fn(() => ({ topic: 'test' })),
|
|
sendLatestMessages: vi.fn(() => ({ topic: 'test' })),
|
|
subscribe: vi.fn(() => ({ dispose: vi.fn() }))
|
|
}
|
|
},
|
|
{
|
|
provide: PluginStorageService,
|
|
useValue: storage
|
|
},
|
|
{
|
|
provide: PluginUiRegistryService,
|
|
useValue: uiRegistry
|
|
},
|
|
{
|
|
provide: RealtimeSessionFacade,
|
|
useValue: {
|
|
broadcastMessage: vi.fn(),
|
|
onSignalingMessage: realtime.onSignalingMessage.asObservable(),
|
|
sendRawMessage: realtime.sendRawMessage
|
|
}
|
|
},
|
|
{
|
|
provide: ServerDirectoryFacade,
|
|
useValue: {
|
|
updateServer: vi.fn(() => ({ subscribe: vi.fn() }))
|
|
}
|
|
},
|
|
{
|
|
provide: Store,
|
|
useValue: store
|
|
},
|
|
{
|
|
provide: VoiceConnectionFacade,
|
|
useValue: voice
|
|
}
|
|
]
|
|
});
|
|
|
|
return {
|
|
capabilities: injector.get(PluginCapabilityService),
|
|
channels,
|
|
currentUser,
|
|
logger,
|
|
messages,
|
|
realtime,
|
|
room,
|
|
service: injector.get(PluginClientApiService),
|
|
storage,
|
|
store,
|
|
uiRegistry,
|
|
voice,
|
|
messageRevisions
|
|
};
|
|
}
|
|
|
|
function createTestManifest(): TojuPluginManifest {
|
|
return {
|
|
apiVersion: '1.0.0',
|
|
capabilities: [
|
|
'profile.read',
|
|
'profile.write',
|
|
'users.read',
|
|
'users.manage',
|
|
'roles.read',
|
|
'roles.manage',
|
|
'messages.read',
|
|
'messages.send',
|
|
'messages.editOwn',
|
|
'messages.deleteOwn',
|
|
'messages.moderate',
|
|
'messages.sync',
|
|
'channels.read',
|
|
'channels.manage',
|
|
'server.read',
|
|
'server.manage',
|
|
'p2p.data',
|
|
'media.playAudio',
|
|
'media.addAudioStream',
|
|
'media.addVideoStream',
|
|
'audio.volume',
|
|
'ui.settings',
|
|
'ui.pages',
|
|
'ui.sidePanel',
|
|
'ui.channelsSection',
|
|
'ui.embeds',
|
|
'ui.dom',
|
|
'ui.commands',
|
|
'storage.local',
|
|
'storage.serverData.read',
|
|
'storage.serverData.write',
|
|
'events.server.publish',
|
|
'events.server.subscribe',
|
|
'events.p2p.publish',
|
|
'events.p2p.subscribe'
|
|
],
|
|
compatibility: {
|
|
minimumTojuVersion: '1.0.0'
|
|
},
|
|
description: 'Plugin API service test fixture',
|
|
events: [
|
|
{
|
|
direction: 'serverRelay',
|
|
eventName: 'e2e:server',
|
|
scope: 'server'
|
|
}
|
|
],
|
|
id: 'test.plugin-api',
|
|
kind: 'client',
|
|
schemaVersion: 1,
|
|
title: 'Plugin API Service Fixture',
|
|
version: '1.0.0'
|
|
};
|
|
}
|
|
|
|
function createUser(): User {
|
|
return {
|
|
displayName: 'Alice',
|
|
id: 'user-1',
|
|
isOnline: true,
|
|
joinedAt: Date.now(),
|
|
oderId: 'user-1',
|
|
role: 'host',
|
|
status: 'online',
|
|
username: 'alice'
|
|
};
|
|
}
|
|
|
|
function createRoom(): Room {
|
|
return {
|
|
channels: [{ id: 'general', name: 'general', position: 0, type: 'text' }, { id: 'voice', name: 'voice', position: 1, type: 'voice' }],
|
|
description: 'Plugin API room',
|
|
hostId: 'user-1',
|
|
id: 'room-1',
|
|
isPrivate: false,
|
|
members: [],
|
|
name: 'Plugin API Room',
|
|
roles: []
|
|
};
|
|
}
|
|
|
|
async function invokeApiMethod(method: (...args: unknown[]) => unknown, path: string): Promise<unknown> {
|
|
switch (path) {
|
|
case 'attachments.import':
|
|
return method({ files: [], messageId: 'message-1' });
|
|
case 'channels.addAudioChannel':
|
|
return method({ name: 'Audio' });
|
|
case 'channels.addTextChannel':
|
|
return method({ name: 'Text' });
|
|
case 'channels.addVideoChannel':
|
|
return method({ name: 'Video' });
|
|
case 'channels.remove':
|
|
return method('general');
|
|
case 'channels.rename':
|
|
return method('general', 'renamed');
|
|
case 'channels.select':
|
|
return method('general');
|
|
case 'clientData.read':
|
|
case 'serverData.read':
|
|
return method('key');
|
|
case 'clientData.remove':
|
|
case 'serverData.remove':
|
|
return method('key');
|
|
case 'clientData.write':
|
|
case 'serverData.write':
|
|
return method('key', { ok: true });
|
|
case 'commands.register':
|
|
return method('echo', { name: 'echo', run: () => {} });
|
|
case 'events.publishP2p':
|
|
case 'events.publishServer':
|
|
return method('e2e:server', {});
|
|
case 'events.subscribeP2p':
|
|
case 'events.subscribeServer':
|
|
return method({ eventName: 'e2e:server', handler: () => {} });
|
|
case 'media.addCustomAudioStream':
|
|
case 'media.addCustomVideoStream':
|
|
return method({ stream: new MediaStream() });
|
|
case 'media.playAudioClip':
|
|
return method({ url: 'data:audio/wav;base64,' });
|
|
case 'media.setInputVolume':
|
|
case 'media.setOutputVolume':
|
|
return method(0.5);
|
|
case 'messageBus.publish':
|
|
return method({ topic: 'test' });
|
|
case 'messageBus.sendLatestMessages':
|
|
return method({});
|
|
case 'messageBus.subscribe':
|
|
return method({ handler: () => {} });
|
|
case 'messages.delete':
|
|
return method('message-1');
|
|
case 'messages.edit':
|
|
return method('message-1', 'updated');
|
|
case 'messages.moderateDelete':
|
|
return method('message-1');
|
|
case 'messages.import':
|
|
return method([]);
|
|
case 'messages.send':
|
|
return method('hello');
|
|
case 'messages.sendAsPluginUser':
|
|
return method({ content: 'hello', pluginUserId: 'bot' });
|
|
case 'messages.setTyping':
|
|
return method(true);
|
|
case 'messages.subscribeTyping':
|
|
return method(() => {});
|
|
case 'messages.sync':
|
|
return method([]);
|
|
case 'p2p.broadcastData':
|
|
return method('e2e:server', {});
|
|
case 'p2p.sendData':
|
|
return method('peer-1', 'e2e:server', {});
|
|
case 'profile.update':
|
|
return method({ displayName: 'Updated' });
|
|
case 'profile.updateAvatar':
|
|
return method({ avatarHash: 'hash', avatarMime: 'image/png', avatarUrl: '/avatar.png' });
|
|
case 'roles.setAssignments':
|
|
return method([]);
|
|
case 'server.registerPluginUser':
|
|
return method({ displayName: 'Bot' });
|
|
case 'server.updateIcon':
|
|
return method('icon-hash');
|
|
case 'server.updatePermissions':
|
|
return method({ allowVoice: true });
|
|
case 'server.updateSettings':
|
|
return method({ name: 'Room' });
|
|
case 'storage.get':
|
|
return method('key');
|
|
case 'storage.remove':
|
|
return method('key');
|
|
case 'storage.set':
|
|
return method('key', { ok: true });
|
|
case 'ui.mountElement':
|
|
return method('mount', { element: { tagName: 'DIV' }, target: 'body' });
|
|
case 'ui.registerAppPage':
|
|
case 'ui.registerChannelSection':
|
|
case 'ui.registerComposerAction':
|
|
case 'ui.registerEmbedRenderer':
|
|
case 'ui.registerProfileAction':
|
|
case 'ui.registerSettingsPage':
|
|
case 'ui.registerSidePanel':
|
|
case 'ui.registerToolbarAction':
|
|
return method('id', { label: 'Test', render: () => 'ok', run: () => {} });
|
|
case 'users.ban':
|
|
return method('user-2', 'reason');
|
|
case 'users.kick':
|
|
return method('user-2');
|
|
case 'users.setRole':
|
|
return method('user-2', 'member');
|
|
default:
|
|
return Promise.resolve(method());
|
|
}
|
|
}
|
|
|
|
function installBrowserMediaMocks(): void {
|
|
vi.stubGlobal('MediaStream', class MediaStream {});
|
|
vi.stubGlobal('Audio', class Audio {
|
|
volume = 1;
|
|
|
|
async play(): Promise<void> {}
|
|
});
|
|
}
|
|
|
|
function installLocalStorageMock(): void {
|
|
const storage = new Map<string, string>();
|
|
|
|
vi.stubGlobal('localStorage', {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
storage.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
storage.delete(key);
|
|
},
|
|
clear: () => {
|
|
storage.clear();
|
|
}
|
|
});
|
|
}
|