Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions core/server/live/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export type {
import { liveServer, pendingAuthProviders } from './websocket-plugin'
import type { LiveAuthProvider as _LiveAuthProvider } from '@fluxstack/live'

function requireLiveServer() {
if (!liveServer) {
throw new Error(
'LiveComponents plugin not initialized. ' +
'Ensure the live-components plugin is loaded before accessing Live singletons.'
)
}
return liveServer
}

/**
* Backward-compatible liveAuthManager.
* Buffers register() calls that happen before the plugin setup(),
Expand All @@ -51,49 +61,49 @@ export const liveAuthManager = {
pendingAuthProviders.push(provider)
}
},
get authenticate() { return liveServer!.authManager.authenticate.bind(liveServer!.authManager) },
get hasProviders() { return liveServer!.authManager.hasProviders.bind(liveServer!.authManager) },
get authorizeRoom() { return liveServer!.authManager.authorizeRoom.bind(liveServer!.authManager) },
get authorizeAction() { return liveServer!.authManager.authorizeAction.bind(liveServer!.authManager) },
get authorizeComponent() { return liveServer!.authManager.authorizeComponent.bind(liveServer!.authManager) },
get authenticate() { return requireLiveServer().authManager.authenticate.bind(requireLiveServer().authManager) },
get hasProviders() { return requireLiveServer().authManager.hasProviders.bind(requireLiveServer().authManager) },
get authorizeRoom() { return requireLiveServer().authManager.authorizeRoom.bind(requireLiveServer().authManager) },
get authorizeAction() { return requireLiveServer().authManager.authorizeAction.bind(requireLiveServer().authManager) },
get authorizeComponent() { return requireLiveServer().authManager.authorizeComponent.bind(requireLiveServer().authManager) },
} as any

/** @deprecated Access via liveServer.registry instead */
export const componentRegistry = new Proxy({} as any, {
get(_, prop) { return (liveServer!.registry as any)[prop] }
get(_, prop) { return (requireLiveServer().registry as any)[prop] }
})

/** @deprecated Access via liveServer.connectionManager instead */
export const connectionManager = new Proxy({} as any, {
get(_, prop) { return (liveServer!.connectionManager as any)[prop] }
get(_, prop) { return (requireLiveServer().connectionManager as any)[prop] }
})

/** @deprecated Access via liveServer.roomManager instead */
export const liveRoomManager = new Proxy({} as any, {
get(_, prop) { return (liveServer!.roomManager as any)[prop] }
get(_, prop) { return (requireLiveServer().roomManager as any)[prop] }
})

/** @deprecated Access via liveServer.roomEvents instead */
export const roomEvents = new Proxy({} as any, {
get(_, prop) { return (liveServer!.roomEvents as any)[prop] }
get(_, prop) { return (requireLiveServer().roomEvents as any)[prop] }
})

/** @deprecated Access via liveServer.fileUploadManager instead */
export const fileUploadManager = new Proxy({} as any, {
get(_, prop) { return (liveServer!.fileUploadManager as any)[prop] }
get(_, prop) { return (requireLiveServer().fileUploadManager as any)[prop] }
})

/** @deprecated Access via liveServer.performanceMonitor instead */
export const performanceMonitor = new Proxy({} as any, {
get(_, prop) { return (liveServer!.performanceMonitor as any)[prop] }
get(_, prop) { return (requireLiveServer().performanceMonitor as any)[prop] }
})

/** @deprecated Access via liveServer.stateSignature instead */
export const stateSignature = new Proxy({} as any, {
get(_, prop) { return (liveServer!.stateSignature as any)[prop] }
get(_, prop) { return (requireLiveServer().stateSignature as any)[prop] }
})

// Room state backward compat
export const roomState = new Proxy({} as any, {
get(_, prop) { return (liveServer!.roomManager as any)[prop] }
get(_, prop) { return (requireLiveServer().roomManager as any)[prop] }
})
171 changes: 171 additions & 0 deletions tests/unit/core/live-compat-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

/**
* Tests for the Live Components compat layer null guard (#85).
*
* When the LiveComponents plugin has not been initialized yet,
* accessing any deprecated singleton should throw a descriptive error
* instead of crashing with an opaque null dereference.
*/

// We need to control the value of liveServer exported from websocket-plugin.
// The compat layer in core/server/live/index.ts imports liveServer at module
// level, so we mock the websocket-plugin module to control its value.

const EXPECTED_ERROR = 'LiveComponents plugin not initialized'

describe('Live Components compat layer null guard (#85)', () => {
describe('when liveServer is null (plugin not initialized)', () => {
beforeEach(() => {
vi.resetModules()
})

afterEach(() => {
vi.restoreAllMocks()
})

it('componentRegistry proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { componentRegistry } = await import('../../../core/server/live/index')

expect(() => componentRegistry.someMethod).toThrowError(EXPECTED_ERROR)
})

it('connectionManager proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { connectionManager } = await import('../../../core/server/live/index')

expect(() => connectionManager.someMethod).toThrowError(EXPECTED_ERROR)
})

it('liveRoomManager proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { liveRoomManager } = await import('../../../core/server/live/index')

expect(() => liveRoomManager.someMethod).toThrowError(EXPECTED_ERROR)
})

it('roomEvents proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { roomEvents } = await import('../../../core/server/live/index')

expect(() => roomEvents.someMethod).toThrowError(EXPECTED_ERROR)
})

it('fileUploadManager proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { fileUploadManager } = await import('../../../core/server/live/index')

expect(() => fileUploadManager.someMethod).toThrowError(EXPECTED_ERROR)
})

it('performanceMonitor proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { performanceMonitor } = await import('../../../core/server/live/index')

expect(() => performanceMonitor.someMethod).toThrowError(EXPECTED_ERROR)
})

it('stateSignature proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { stateSignature } = await import('../../../core/server/live/index')

expect(() => stateSignature.someMethod).toThrowError(EXPECTED_ERROR)
})

it('roomState proxy throws descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { roomState } = await import('../../../core/server/live/index')

expect(() => roomState.someMethod).toThrowError(EXPECTED_ERROR)
})

it('liveAuthManager getters throw descriptive error', async () => {
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: [],
}))
const { liveAuthManager } = await import('../../../core/server/live/index')

expect(() => liveAuthManager.authenticate).toThrowError(EXPECTED_ERROR)
expect(() => liveAuthManager.hasProviders).toThrowError(EXPECTED_ERROR)
expect(() => liveAuthManager.authorizeRoom).toThrowError(EXPECTED_ERROR)
expect(() => liveAuthManager.authorizeAction).toThrowError(EXPECTED_ERROR)
expect(() => liveAuthManager.authorizeComponent).toThrowError(EXPECTED_ERROR)
})

it('liveAuthManager.register() buffers provider when liveServer is null', async () => {
const pending: any[] = []
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: null,
pendingAuthProviders: pending,
}))
const { liveAuthManager } = await import('../../../core/server/live/index')

const fakeProvider = { authenticate: vi.fn() }
liveAuthManager.register(fakeProvider)

expect(pending).toHaveLength(1)
expect(pending[0]).toBe(fakeProvider)
})
})

describe('when liveServer is initialized', () => {
beforeEach(() => {
vi.resetModules()
})

afterEach(() => {
vi.restoreAllMocks()
})

it('componentRegistry proxy delegates to liveServer.registry', async () => {
const mockRegistry = { register: vi.fn(), get: vi.fn(() => 'found') }
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: { registry: mockRegistry },
pendingAuthProviders: [],
}))
const { componentRegistry } = await import('../../../core/server/live/index')

expect(componentRegistry.get()).toBe('found')
})

it('liveAuthManager.register() delegates to liveServer.useAuth', async () => {
const useAuth = vi.fn()
vi.doMock('../../../core/server/live/websocket-plugin', () => ({
liveServer: { useAuth },
pendingAuthProviders: [],
}))
const { liveAuthManager } = await import('../../../core/server/live/index')

const fakeProvider = { authenticate: vi.fn() }
liveAuthManager.register(fakeProvider)

expect(useAuth).toHaveBeenCalledWith(fakeProvider)
})
})
})
Loading