/** * Unit tests for EufySecurityProvider */ import { EufySecurityProvider } from "../../src/eufy-provider"; import { DeviceUtils } from "../../src/utils/device-utils"; import sdk from "@scrypted/sdk"; import { StartListeningResponse } from "@caplaz/eufy-security-client"; import { configureSharedCompatibilityStreaming } from "../../src/services/device/stream-service"; jest.mock("../../src/services/device/stream-service", () => ({ configureSharedCompatibilityStreaming: jest.fn(), DEFAULT_COMPATIBILITY_STREAMING_CONFIG: { encoderCapacity: 1, criticalTemperatureC: 85, recoveryTemperatureC: 75, }, })); jest.mock("@scrypted/sdk", () => ({ ScryptedDeviceBase: class { storage = { getItem: jest.fn().mockReturnValue(null), setItem: jest.fn() }; console = { log: jest.fn(), warn: jest.fn(), error: jest.fn() }; nativeId = undefined; onDeviceEvent = jest.fn(); }, ScryptedInterface: { Camera: "Camera", VideoCamera: "VideoCamera", MotionSensor: "MotionSensor", Settings: "Settings", Refresh: "Refresh", Battery: "Battery", Charger: "Charger", Sensors: "Sensors", OnOff: "OnOff", Brightness: "Brightness", PanTiltZoom: "PanTiltZoom", BinarySensor: "BinarySensor", SecuritySystem: "SecuritySystem", }, SecuritySystemMode: { AwayArmed: "AwayArmed", HomeArmed: "HomeArmed", Disarmed: "Disarmed", NightArmed: "NightArmed", }, ChargeState: { Charging: "Charging", NotCharging: "NotCharging", }, default: { deviceManager: { onDevicesChanged: jest.fn(), }, mediaManager: { createFFmpegMediaObject: jest.fn(), }, }, deviceManager: { onDevicesChanged: jest.fn(), }, })); jest.mock("../../src/utils/device-utils", () => ({ DeviceUtils: { createDeviceManifest: jest.fn(), createStationManifest: jest.fn(), genericDeviceInformation: jest.fn().mockReturnValue([]), allWriteableDeviceProperties: jest.fn().mockReturnValue([]), }, })); jest.mock("@caplaz/eufy-security-client", () => { const actual = jest.requireActual("@caplaz/eufy-security-client"); return { ...actual, EufyWebSocketClient: jest.fn().mockImplementation(() => ({ isConnected: jest.fn().mockReturnValue(false), getState: jest.fn().mockReturnValue({}), on: jest.fn(), off: jest.fn(), })), AuthenticationManager: jest.fn().mockImplementation(() => ({ on: jest.fn(), off: jest.fn(), getAuthStatusMessage: jest.fn().mockReturnValue("Ready"), getAuthState: jest.fn().mockReturnValue("none"), })), }; }); jest.mock("../../src/utils/memory-manager", () => ({ MemoryManager: { setMemoryThreshold: jest.fn(), }, getCurrentMemoryUsageMB: jest.fn().mockReturnValue({ rssMB: 100 }), })); describe("EufySecurityProvider.registerDevicesFromServerState", () => { let provider: EufySecurityProvider; let mockOnDevicesChanged: jest.Mock; let mockCreateDeviceManifest: jest.Mock; beforeEach(() => { (configureSharedCompatibilityStreaming as jest.Mock).mockClear(); mockOnDevicesChanged = sdk.deviceManager.onDevicesChanged as jest.Mock; mockOnDevicesChanged.mockClear(); mockCreateDeviceManifest = DeviceUtils.createDeviceManifest as jest.Mock; mockCreateDeviceManifest.mockClear(); provider = new EufySecurityProvider("test-provider"); }); it("applies conservative shared compatibility encoder defaults", () => { expect(configureSharedCompatibilityStreaming).toHaveBeenCalledWith({ encoderCapacity: 1, criticalTemperatureC: 85, recoveryTemperatureC: 75, }); }); it("shows advanced encoder and thermal settings with conservative defaults", async () => { const settings = await provider.getSettings(); expect(settings).toEqual( expect.arrayContaining([ expect.objectContaining({ key: "compatibilityEncoderCapacity", value: 1, }), expect.objectContaining({ key: "compatibilityEncoderCriticalTemperatureC", value: 85, }), expect.objectContaining({ key: "compatibilityEncoderRecoveryTemperatureC", value: 75, }), ]), ); }); it("applies a validated capacity update to the shared compatibility pool", async () => { await provider.putSetting("compatibilityEncoderCapacity", 2); expect(configureSharedCompatibilityStreaming).toHaveBeenLastCalledWith({ encoderCapacity: 2, criticalTemperatureC: 85, recoveryTemperatureC: 75, }); expect((provider as any).storage.setItem).toHaveBeenCalledWith( "compatibilityEncoderCapacity", "2", ); await expect( provider.putSetting("compatibilityEncoderRecoveryTemperatureC", 90), ).rejects.toThrow("recovery temperature must be lower"); }); it("rejects invalid encoder inputs without persisting their raw values", async () => { const setItem = (provider as any).storage.setItem as jest.Mock; const configuration = configureSharedCompatibilityStreaming as jest.Mock; const persistedBefore = setItem.mock.calls.length; const configuredBefore = configuration.mock.calls.length; await expect( provider.putSetting("compatibilityEncoderCapacity", "not-a-number"), ).rejects.toThrow("finite number"); await expect( provider.putSetting("compatibilityEncoderCriticalTemperatureC", 200), ).rejects.toThrow("between 0 and 125"); expect(setItem).toHaveBeenCalledTimes(persistedBefore); expect(configuration).toHaveBeenCalledTimes(configuredBefore); }); it("rejects nullish encoder updates instead of falling back to stored values", async () => { const storage = (provider as any).storage; storage.getItem.mockImplementation((key: string) => key === "compatibilityEncoderCapacity" ? "2" : null, ); const persistedBefore = storage.setItem.mock.calls.length; await expect( provider.putSetting("compatibilityEncoderCapacity", null), ).rejects.toThrow("finite number"); await expect( provider.putSetting("compatibilityEncoderCapacity", undefined as any), ).rejects.toThrow("finite number"); expect(storage.setItem).toHaveBeenCalledTimes(persistedBefore); }); it("groups 3 devices across 2 stations into exactly 2 onDevicesChanged calls", async () => { // Two devices on station A, one device on station B const manifests = [ { nativeId: "device_CAM001", providerNativeId: "station_STA001", name: "Cam 1", }, { nativeId: "device_CAM002", providerNativeId: "station_STA001", name: "Cam 2", }, { nativeId: "device_CAM003", providerNativeId: "station_STA002", name: "Cam 3", }, ]; mockCreateDeviceManifest .mockResolvedValueOnce(manifests[0]) .mockResolvedValueOnce(manifests[1]) .mockResolvedValueOnce(manifests[2]); const serverState = { state: { devices: ["CAM001", "CAM002", "CAM003"], stations: [], }, } as unknown as StartListeningResponse; await (provider as any).registerDevicesFromServerState(serverState); expect(mockOnDevicesChanged).toHaveBeenCalledTimes(2); const calls = mockOnDevicesChanged.mock.calls; const callByStation = new Map( calls.map((c: any[]) => [c[0].providerNativeId, c[0].devices]), ); expect(callByStation.get("station_STA001")).toHaveLength(2); expect(callByStation.get("station_STA001")).toEqual( expect.arrayContaining([manifests[0], manifests[1]]), ); expect(callByStation.get("station_STA002")).toHaveLength(1); expect(callByStation.get("station_STA002")).toEqual([manifests[2]]); }); it("makes a single onDevicesChanged call when all devices belong to one station", async () => { const manifests = [ { nativeId: "device_CAM001", providerNativeId: "station_STA001", name: "Cam 1", }, { nativeId: "device_CAM002", providerNativeId: "station_STA001", name: "Cam 2", }, ]; mockCreateDeviceManifest .mockResolvedValueOnce(manifests[0]) .mockResolvedValueOnce(manifests[1]); const serverState = { state: { devices: ["CAM001", "CAM002"], stations: [] }, } as unknown as StartListeningResponse; await (provider as any).registerDevicesFromServerState(serverState); expect(mockOnDevicesChanged).toHaveBeenCalledTimes(1); expect(mockOnDevicesChanged).toHaveBeenCalledWith({ providerNativeId: "station_STA001", devices: manifests, }); }); it("does nothing when there are no devices", async () => { const serverState = { state: { devices: [], stations: [] }, } as unknown as StartListeningResponse; await (provider as any).registerDevicesFromServerState(serverState); expect(mockOnDevicesChanged).not.toHaveBeenCalled(); expect(mockCreateDeviceManifest).not.toHaveBeenCalled(); }); it("groups devices with undefined providerNativeId into their own onDevicesChanged call", async () => { const manifests = [ { nativeId: "device_CAM001", providerNativeId: "station_STA001", name: "Cam 1", }, { nativeId: "device_CAM002", providerNativeId: undefined, name: "Orphan Cam", }, ]; mockCreateDeviceManifest .mockResolvedValueOnce(manifests[0]) .mockResolvedValueOnce(manifests[1]); const serverState = { state: { devices: ["CAM001", "CAM002"], stations: [] }, } as unknown as StartListeningResponse; await (provider as any).registerDevicesFromServerState(serverState); expect(mockOnDevicesChanged).toHaveBeenCalledTimes(2); const calls = mockOnDevicesChanged.mock.calls; const stationCall = calls.find( (c: any[]) => c[0].providerNativeId === "station_STA001", ); const undefinedCall = calls.find( (c: any[]) => c[0].providerNativeId === undefined, ); expect(stationCall).toBeDefined(); expect(stationCall![0].devices).toEqual([manifests[0]]); expect(undefinedCall).toBeDefined(); expect(undefinedCall![0].devices).toEqual([manifests[1]]); }); });