Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { DEFAULT_SYNC_SERVER_URL, SYNC_SERVER_URL_KEY, getSyncServerUrl, getSyncTransportPolicy, persistInsecureSyncOverride, persistSyncServerUrl, subscribeSyncServerUrl,} from '../sync-server-config'
// Local alias to keep the existing test names readable. The// underlying helper was renamed from setSyncServerUrl to// persistSyncServerUrl (R8 mechanical guard) to make it impossible// to call the persist-only path by mistake; only repo.setSyncServerUrl// should be invoked from app/UI code.const setSyncServerUrl = persistSyncServerUrl
describe('sync-server-config', () => { beforeEach(() => { localStorage.clear() }) afterEach(() => { vi.unstubAllEnvs() })
it('falls back to default when no localStorage and no env var', () => { expect(getSyncServerUrl()).toBe(DEFAULT_SYNC_SERVER_URL) expect(DEFAULT_SYNC_SERVER_URL).toBe('ws://192.168.1.240:7030') })
it('prefers VITE_SYNC_SERVER_URL over the default', () => { vi.stubEnv('VITE_SYNC_SERVER_URL', 'ws://envserver:9999') expect(getSyncServerUrl()).toBe('ws://envserver:9999') })
it('prefers localStorage over env and default', () => { vi.stubEnv('VITE_SYNC_SERVER_URL', 'ws://envserver:9999') localStorage.setItem(SYNC_SERVER_URL_KEY, 'ws://userserver:1234') expect(getSyncServerUrl()).toBe('ws://userserver:1234') })
it('setSyncServerUrl persists to localStorage and notifies subscribers', () => { const listener = vi.fn() const unsubscribe = subscribeSyncServerUrl(listener) setSyncServerUrl('ws://new:1111') expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBe('ws://new:1111') expect(listener).toHaveBeenCalledWith('ws://new:1111') unsubscribe() setSyncServerUrl('ws://newer:2222') expect(listener).toHaveBeenCalledTimes(1) expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBe('ws://newer:2222') })
it('ignores empty / whitespace-only inputs (would brick the app)', () => { expect(() => setSyncServerUrl('')).toThrow(/empty/) expect(() => setSyncServerUrl(' ')).toThrow(/empty/) expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBeNull() })
it('rejects non-ws(s) schemes', () => { expect(() => setSyncServerUrl('http://example.com')).toThrow(/ws:|wss:/) expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBeNull() })
it('does not fire listeners for rejected inputs', () => { const listener = vi.fn() const unsubscribe = subscribeSyncServerUrl(listener) expect(() => setSyncServerUrl('')).toThrow() expect(() => setSyncServerUrl('http://nope')).toThrow() expect(listener).not.toHaveBeenCalled() unsubscribe() })
it('accepts wss:// URLs as a valid scheme', () => { setSyncServerUrl('wss://secure.example.com') expect(getSyncServerUrl()).toBe('wss://secure.example.com') })
it('rejects host-less ws: URLs (ws://, ws:///path)', () => { // The regex-only validation accepted these silently; the WebSocket // adapter would later throw asynchronously inside connect(). Catch // them at the call site so the Settings UI can render a clear // inline error instead. expect(() => setSyncServerUrl('ws://')).toThrow(/host|url/i) expect(() => setSyncServerUrl('ws:///path')).toThrow(/host|url/i) expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBeNull() })
it('rejects URLs with whitespace in the host', () => { expect(() => setSyncServerUrl('ws://x y z:1234')).toThrow() expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBeNull() })
it('accepts ws+host+port URLs (positive control for the new validator)', () => { setSyncServerUrl('ws://example.com:1234') expect(getSyncServerUrl()).toBe('ws://example.com:1234') setSyncServerUrl('wss://example.com:8443/path') expect(getSyncServerUrl()).toBe('wss://example.com:8443/path') })
it('blocks remote cleartext only in production, even for stored/build defaults', () => { vi.stubEnv('PROD', true) for (const url of ['ws://192.168.1.240:7030', 'ws://localhost.evil.test', 'ws://127.0.0.2:1234']) { expect(getSyncTransportPolicy(url)).toBe('blocked') } expect(getSyncTransportPolicy('ws://localhost:3030')).toBe('allowed') expect(getSyncTransportPolicy('ws://127.0.0.1:3030')).toBe('allowed') expect(getSyncTransportPolicy('ws://[::1]:3030')).toBe('allowed') expect(getSyncTransportPolicy('wss://example.com')).toBe('allowed') persistInsecureSyncOverride(true) expect(getSyncTransportPolicy('ws://192.168.1.240:7030')).toBe('insecure-override') persistInsecureSyncOverride(false) expect(getSyncTransportPolicy('ws://192.168.1.240:7030')).toBe('blocked') })
it('isolates a throwing listener from other listeners', () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const bad = vi.fn(() => { throw new Error('listener boom') }) const good = vi.fn() const unsubBad = subscribeSyncServerUrl(bad) const unsubGood = subscribeSyncServerUrl(good)
setSyncServerUrl('ws://throw-test:9999')
// Both listeners were called despite the first one throwing. expect(bad).toHaveBeenCalledTimes(1) expect(bad).toHaveBeenCalledWith('ws://throw-test:9999') expect(good).toHaveBeenCalledTimes(1) expect(good).toHaveBeenCalledWith('ws://throw-test:9999') // The error was logged, not re-thrown. expect(errorSpy).toHaveBeenCalledTimes(1) expect(errorSpy.mock.calls[0][0]).toBe('sync-server-url listener threw:') expect(errorSpy.mock.calls[0][1]).toBeInstanceOf(Error) expect((errorSpy.mock.calls[0][1] as Error).message).toBe('listener boom') // The URL was still persisted despite the listener error. expect(localStorage.getItem(SYNC_SERVER_URL_KEY)).toBe('ws://throw-test:9999')
unsubBad() unsubGood() errorSpy.mockRestore() })})