Skip to content
Closed
4 changes: 2 additions & 2 deletions packages/federation-sdk/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ export async function createFederationContainer(
container.registerSingleton(SignatureVerificationService);
container.registerSingleton(FederationService);
container.registerSingleton(StateService);
container.registerSingleton(EventAuthorizationService);
container.registerSingleton('EventFetcherService', EventFetcherService);
container.registerSingleton(EventFetcherService);
container.registerSingleton(EventStateService);
container.registerSingleton(EventService);
container.registerSingleton(EventAuthorizationService);
container.registerSingleton(EventEmitterService);
container.registerSingleton(InviteService);
container.registerSingleton(MediaService);
Expand Down
248 changes: 248 additions & 0 deletions packages/federation-sdk/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
Note on testing framework:
- These tests are authored to run under the repository's existing test runner (Jest or Vitest).
- If running under Vitest, vi is available; under Jest, jest is available.
- We create a small shim so the same test file works in both without adding dependencies.
*/

const testApi = (() => {
const g: any = globalThis as any;
if (g.vi) return { spy: g.vi.spyOn.bind(g.vi), mock: g.vi.fn.bind(g.vi), reset: g.vi.resetAllMocks?.bind(g.vi) ?? (() => {}), clear: g.vi.clearAllMocks?.bind(g.vi) ?? (() => {}) };
if (g.jest) return { spy: g.jest.spyOn.bind(g.jest), mock: g.jest.fn.bind(g.jest), reset: g.jest.resetAllMocks?.bind(g.jest) ?? (() => {}), clear: g.jest.clearAllMocks?.bind(g.jest) ?? (() => {}) };
throw new Error("No supported test framework detected: expected vi or jest on globalThis.");
})();

import * as Tsyringe from 'tsyringe';

// Import subject under test
import {
getAllServices,
// Services we'll assert are resolved
ConfigService,
EduService,
EventAuthorizationService,
EventService,
FederationRequestService,
InviteService,
MediaService,
MessageService,
ProfilesService,
RoomService,
SendJoinService,
ServerService,
StateService,
WellKnownService,
// Selected runtime exports to sanity check re-exports
FederationModule,
FederationRequestService as FederationRequestServiceExport,
FederationService,
SignatureVerificationService,
WellKnownService as WellKnownServiceExport,
DatabaseConnectionService,
EduService as EduServiceExport,
ServerService as ServerServiceExport,
EventAuthorizationService as EventAuthorizationServiceExport,
EventStateService,
MissingEventService,
ProfilesService as ProfilesServiceExport,
EventFetcherService,
InviteService as InviteServiceExport,
MediaService as MediaServiceExport,
MessageService as MessageServiceExport,
EventService as EventServiceExport,
RoomService as RoomServiceExport,
StateService as StateServiceExport,
StagingAreaService,
SendJoinService as SendJoinServiceExport,
EventEmitterService,
MissingEventListener,
// Queues and utils
BaseQueue,
getErrorMessage,
USERNAME_REGEX,
ROOM_ID_REGEX,
LockManagerService,
EventRepository,
RoomRepository,
ServerRepository,
KeyRepository,
StateRepository,
StagingAreaListener,
createFederationContainer,
DependencyContainer
} from './index';

describe('packages/federation-sdk/src/index.ts public API', () => {
beforeEach(() => {
testApi.reset?.();
testApi.clear?.();
});

it('should expose key runtime exports', () => {
// Modules / classes (existence checks)
expect(FederationModule).toBeDefined();
expect(FederationService).toBeDefined();
expect(SignatureVerificationService).toBeDefined();
expect(DatabaseConnectionService).toBeDefined();
expect(EventStateService).toBeDefined();
expect(MissingEventService).toBeDefined();
expect(EventFetcherService).toBeDefined();
expect(StagingAreaService).toBeDefined();
expect(EventEmitterService).toBeDefined();
expect(MissingEventListener).toBeDefined();
expect(StagingAreaListener).toBeDefined();
// Re-export sanity (aliases point to same runtime)
expect(WellKnownServiceExport).toBe(WellKnownService);
expect(EduServiceExport).toBe(EduService);
expect(ServerServiceExport).toBe(ServerService);
expect(EventAuthorizationServiceExport).toBe(EventAuthorizationService);
expect(ProfilesServiceExport).toBe(ProfilesService);
expect(InviteServiceExport).toBe(InviteService);
expect(MediaServiceExport).toBe(MediaService);
expect(MessageServiceExport).toBe(MessageService);
expect(EventServiceExport).toBe(EventService);
expect(RoomServiceExport).toBe(RoomService);
expect(StateServiceExport).toBe(StateService);
expect(SendJoinServiceExport).toBe(SendJoinService);
// Utils and constants
expect(typeof getErrorMessage).toBe('function');
expect(USERNAME_REGEX).toBeInstanceOf(RegExp);
expect(ROOM_ID_REGEX).toBeInstanceOf(RegExp);
// Queues / Base types
expect(BaseQueue).toBeDefined();
// Container helpers
expect(createFederationContainer).toBeDefined();
expect(DependencyContainer).toBeDefined();
// Repositories
expect(EventRepository).toBeDefined();
expect(RoomRepository).toBeDefined();
expect(ServerRepository).toBeDefined();
expect(KeyRepository).toBeDefined();
expect(StateRepository).toBeDefined();
// Additional runtime export check
expect(FederationRequestServiceExport).toBe(FederationRequestService);
});

describe('getAllServices()', () => {
function makeMockInstances() {
// Unique objects to ensure identity mapping
return {
room: { name: 'room' },
message: { name: 'message' },
media: { name: 'media' },
event: { name: 'event' },
invite: { name: 'invite' },
wellKnown: { name: 'wellKnown' },
profile: { name: 'profile' },
state: { name: 'state' },
sendJoin: { name: 'sendJoin' },
server: { name: 'server' },
config: { name: 'config' },
edu: { name: 'edu' },
request: { name: 'request' },
federationAuth: { name: 'federationAuth' },
} as const;
}

function arrangeContainerResolveMock(instances: ReturnType<typeof makeMockInstances>) {
// Spy on container.resolve and route by token
const spy = testApi.spy(Tsyringe, 'container', 'get'); // Not available; fallback approach below
// In environments where spying on "container.resolve" directly is easier:
const resolveSpy = testApi.spy(Tsyringe.container as any, 'resolve');
(Tsyringe.container.resolve as unknown as jest.Mock | ((...args:any[])=>any)).mockImplementation((cls: any) => {
switch (cls) {
case RoomService: return instances.room;
case MessageService: return instances.message;
case MediaService: return instances.media;
case EventService: return instances.event;
case InviteService: return instances.invite;
case WellKnownService: return instances.wellKnown;
case ProfilesService: return instances.profile;
case StateService: return instances.state;
case SendJoinService: return instances.sendJoin;
case ServerService: return instances.server;
case ConfigService: return instances.config;
case EduService: return instances.edu;
case FederationRequestService: return instances.request;
case EventAuthorizationService: return instances.federationAuth;
default:
throw new Error('Unexpected token passed to container.resolve');
}
});
return resolveSpy;
}

it('returns a mapping of all services resolved from tsyringe container', () => {
const instances = makeMockInstances();
const resolveSpy = arrangeContainerResolveMock(instances);

const result = getAllServices();

// ensure resolve called for each service token exactly once
const expectedTokens = [
RoomService,
MessageService,
MediaService,
EventService,
InviteService,
WellKnownService,
ProfilesService,
StateService,
SendJoinService,
ServerService,
ConfigService,
EduService,
FederationRequestService,
EventAuthorizationService,
];
for (const token of expectedTokens) {
expect(resolveSpy).toHaveBeenCalledWith(token);
}
expect(resolveSpy).toHaveBeenCalledTimes(expectedTokens.length);

// result shape and identity
expect(result).toEqual(instances);
// identity checks
expect(result.room).toBe(instances.room);
expect(result.federationAuth).toBe(instances.federationAuth);
});

it('propagates errors thrown by container.resolve', () => {
const error = new Error('boom');
const resolveSpy = testApi.spy(Tsyringe.container as any, 'resolve');
(Tsyringe.container.resolve as unknown as jest.Mock | ((...args:any[])=>any)).mockImplementation((cls: any) => {
if (cls === RoomService) throw error;
return {};
});

expect(() => getAllServices()).toThrow(error);
expect(resolveSpy).toHaveBeenCalledWith(RoomService);
});

it('resolves fresh instances on each call (no shared object reuse by function wrapper)', () => {
const first = { name: 'first' };
const second = { name: 'second' };
const resolveSpy = testApi.spy(Tsyringe.container as any, 'resolve');
let call = 0;
(Tsyringe.container.resolve as unknown as jest.Mock | ((...args:any[])=>any)).mockImplementation((cls: any) => {
// Return different objects for room per call to ensure we call container each time
if (cls === RoomService) {
call += 1;
return call === 1 ? first : second;
}
return {};
});

const a = getAllServices();
const b = getAllServices();
expect(a.room).toBe(first);
expect(b.room).toBe(second);
expect(resolveSpy).toHaveBeenCalledWith(RoomService);
expect(resolveSpy).toHaveBeenCalledTimes(2 + 2 * 13); // 14 tokens per call; soft check below to be resilient

// Soft assertion: exactly 14 calls per invocation
const callsPerInvocation = 14;
expect((resolveSpy as any).mock.calls.length % callsPerInvocation).toBe(0);
});
});
});
9 changes: 6 additions & 3 deletions packages/federation-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Membership } from '@hs/core';
import { container } from 'tsyringe';
import { ConfigService } from './services/config.service';
import { EduService } from './services/edu.service';
import { EventAuthorizationService } from './services/event-authorization.service';
import { EventService } from './services/event.service';
import { FederationRequestService } from './services/federation-request.service';
import { InviteService } from './services/invite.service';
Expand Down Expand Up @@ -45,6 +46,7 @@ export { EventFetcherService } from './services/event-fetcher.service';
export type { FetchedEvents } from './services/event-fetcher.service';
export { InviteService } from './services/invite.service';
export type { ProcessInviteEvent } from './services/invite.service';
export { MediaService } from './services/media.service';
export { MessageService } from './services/message.service';
export { EventService } from './services/event.service';
export { RoomService } from './services/room.service';
Expand All @@ -53,7 +55,6 @@ export { StagingAreaService } from './services/staging-area.service';
export { SendJoinService } from './services/send-join.service';
export { EventEmitterService } from './services/event-emitter.service';
export { MissingEventListener } from './listeners/missing-event.listener';
export { MediaService } from './services/media.service';

// Repository interfaces and implementations

Expand Down Expand Up @@ -96,6 +97,7 @@ export { StateRepository } from './repositories/state.repository';
export interface HomeserverServices {
room: RoomService;
message: MessageService;
media: MediaService;
event: EventService;
invite: InviteService;
wellKnown: WellKnownService;
Expand All @@ -105,8 +107,8 @@ export interface HomeserverServices {
server: ServerService;
config: ConfigService;
edu: EduService;
media: MediaService;
request: FederationRequestService;
federationAuth: EventAuthorizationService;
}

export type HomeserverEventSignatures = {
Expand Down Expand Up @@ -220,6 +222,7 @@ export function getAllServices(): HomeserverServices {
return {
room: container.resolve(RoomService),
message: container.resolve(MessageService),
media: container.resolve(MediaService),
event: container.resolve(EventService),
invite: container.resolve(InviteService),
wellKnown: container.resolve(WellKnownService),
Expand All @@ -229,8 +232,8 @@ export function getAllServices(): HomeserverServices {
server: container.resolve(ServerService),
config: container.resolve(ConfigService),
edu: container.resolve(EduService),
media: container.resolve(MediaService),
request: container.resolve(FederationRequestService),
federationAuth: container.resolve(EventAuthorizationService),
};
}

Expand Down
Loading
Loading