UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

256 lines 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ArcSSEClient_1 = require("../ArcSSEClient"); /** Minimal fake EventSource that records listener registrations and lets tests fire them */ class FakeEventSource { constructor(url, opts) { this.listeners = {}; this.closed = false; this.url = url; this.opts = opts; FakeEventSource.instances.push(this); } addEventListener(type, fn) { if (!this.listeners[type]) this.listeners[type] = []; this.listeners[type].push(fn); } /** Helper used by tests to simulate an incoming server event */ emit(type, event = {}) { var _a; for (const fn of (_a = this.listeners[type]) !== null && _a !== void 0 ? _a : []) { fn(event); } } close() { this.closed = true; } } FakeEventSource.instances = []; function makeClient(overrides = {}) { FakeEventSource.instances = []; const events = []; const errors = []; const lastEventIds = []; const client = new ArcSSEClient_1.ArcSSEClient({ baseUrl: 'https://arcade.example.com', callbackToken: 'tok-abc123', onEvent: e => events.push(e), onError: e => errors.push(e), onLastEventIdChanged: id => lastEventIds.push(id), EventSourceClass: FakeEventSource, ...overrides }); return { client, events, errors, lastEventIds }; } describe('ArcSSEClient', () => { beforeEach(() => { FakeEventSource.instances = []; jest.spyOn(console, 'log').mockImplementation(() => { }); }); afterEach(() => { jest.restoreAllMocks(); }); // ── URL construction ────────────────────────────────────────────────────── describe('URL construction', () => { test('builds correct URL with no trailing slash', () => { const { client } = makeClient({ baseUrl: 'https://arcade.example.com' }); client.connect(); const es = FakeEventSource.instances[0]; expect(es.url).toBe('https://arcade.example.com/events?callbackToken=tok-abc123'); }); test('strips single trailing slash from baseUrl', () => { const { client } = makeClient({ baseUrl: 'https://arcade.example.com/' }); client.connect(); expect(FakeEventSource.instances[0].url).toBe('https://arcade.example.com/events?callbackToken=tok-abc123'); }); test('strips multiple trailing slashes from baseUrl', () => { const { client } = makeClient({ baseUrl: 'https://arcade.example.com///' }); client.connect(); expect(FakeEventSource.instances[0].url).toBe('https://arcade.example.com/events?callbackToken=tok-abc123'); }); test('percent-encodes callbackToken in URL', () => { const { client } = makeClient({ callbackToken: 'tok with spaces & chars' }); client.connect(); expect(FakeEventSource.instances[0].url).toContain('callbackToken=tok%20with%20spaces%20%26%20chars'); }); }); // ── connect ─────────────────────────────────────────────────────────────── describe('connect()', () => { test('creates an EventSource with correct headers', () => { const { client } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; expect(es.opts.headers['Last-Event-ID']).toBe('0'); }); test('uses lastEventId from options as Last-Event-ID header', () => { const { client } = makeClient({ lastEventId: '42' }); client.connect(); expect(FakeEventSource.instances[0].opts.headers['Last-Event-ID']).toBe('42'); }); test('does not create a second EventSource when already connected', () => { const { client } = makeClient(); client.connect(); client.connect(); expect(FakeEventSource.instances.length).toBe(1); }); test('open event sets connected state (no crash)', () => { const { client } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; expect(() => es.emit('open')).not.toThrow(); }); }); // ── status event handling ───────────────────────────────────────────────── describe('status events', () => { test('dispatches parsed event to onEvent callback', () => { const { client, events } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; const payload = { txid: 'aaaa', txStatus: 'MINED', timestamp: '2025-01-01T00:00:00Z' }; es.emit('status', { data: JSON.stringify(payload) }); expect(events).toHaveLength(1); expect(events[0]).toEqual(payload); }); test('updates lastEventId and calls onLastEventIdChanged', () => { const { client, lastEventIds } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; es.emit('status', { data: JSON.stringify({ txid: 'bbbb', txStatus: 'SEEN_ON_NETWORK', timestamp: '' }), lastEventId: '99' }); expect(client.lastEventId).toBe('99'); expect(lastEventIds).toEqual(['99']); }); test('does not update lastEventId when event has no lastEventId', () => { const { client } = makeClient({ lastEventId: 'initial' }); client.connect(); const es = FakeEventSource.instances[0]; es.emit('status', { data: JSON.stringify({ txid: 'cccc', txStatus: 'MINED', timestamp: '' }) // no lastEventId field }); expect(client.lastEventId).toBe('initial'); }); test('ignores malformed JSON without throwing', () => { const { client, events } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; expect(() => es.emit('status', { data: 'not-json' })).not.toThrow(); expect(events).toHaveLength(0); }); }); // ── error event handling ────────────────────────────────────────────────── describe('error events', () => { test('calls onError with message from event', () => { const { client, errors } = makeClient(); client.connect(); FakeEventSource.instances[0].emit('error', { message: 'connection refused' }); expect(errors).toHaveLength(1); expect(errors[0].message).toBe('connection refused'); }); test('calls onError with generic message when event has no message', () => { const { client, errors } = makeClient(); client.connect(); FakeEventSource.instances[0].emit('error', {}); expect(errors[0].message).toBe('SSE error'); }); test('does not throw when onError is not provided', () => { const client = new ArcSSEClient_1.ArcSSEClient({ baseUrl: 'https://arcade.example.com', callbackToken: 'tok', onEvent: () => { }, EventSourceClass: FakeEventSource }); client.connect(); expect(() => FakeEventSource.instances[0].emit('error', {})).not.toThrow(); }); }); // ── close ───────────────────────────────────────────────────────────────── describe('close()', () => { test('calls close on the underlying EventSource', () => { const { client } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; client.close(); expect(es.closed).toBe(true); }); test('is a no-op when not connected', () => { const { client } = makeClient(); expect(() => client.close()).not.toThrow(); }); test('allows reconnect after close', () => { const { client } = makeClient(); client.connect(); client.close(); client.connect(); expect(FakeEventSource.instances.length).toBe(2); }); }); // ── fetchEvents ─────────────────────────────────────────────────────────── describe('fetchEvents()', () => { test('returns 0', async () => { const { client } = makeClient(); const result = await client.fetchEvents(); expect(result).toBe(0); }); test('opens connection if not already connected', async () => { const { client } = makeClient(); await client.fetchEvents(); expect(FakeEventSource.instances.length).toBe(1); }); test('does not open a second connection when already connected', async () => { const { client } = makeClient(); client.connect(); FakeEventSource.instances[0].emit('open'); // mark as connected await client.fetchEvents(); expect(FakeEventSource.instances.length).toBe(1); }); test('does not reconnect while connecting (open not yet fired)', async () => { const { client } = makeClient(); client.connect(); // es exists, connected=false but connecting=true (open never fired yet) await client.fetchEvents(); // should NOT tear down — still in connecting state expect(FakeEventSource.instances[0].closed).toBeFalsy(); expect(FakeEventSource.instances.length).toBe(1); }); test('reconnects with stale (errored) EventSource by closing first', async () => { const { client } = makeClient(); client.connect(); // Simulate error which clears connecting flag FakeEventSource.instances[0].emit('error', { message: 'fail' }); // Now es exists, connected=false, connecting=false — should reconnect await client.fetchEvents(); expect(FakeEventSource.instances[0].closed).toBe(true); expect(FakeEventSource.instances.length).toBe(2); }); }); // ── lastEventId getter ──────────────────────────────────────────────────── describe('lastEventId', () => { test('returns undefined when not set', () => { const { client } = makeClient(); expect(client.lastEventId).toBeUndefined(); }); test('returns initial value from options', () => { const { client } = makeClient({ lastEventId: 'start' }); expect(client.lastEventId).toBe('start'); }); test('advances as events are received', () => { const { client } = makeClient(); client.connect(); const es = FakeEventSource.instances[0]; es.emit('status', { data: JSON.stringify({ txid: 'x', txStatus: 'MINED', timestamp: '' }), lastEventId: '1' }); es.emit('status', { data: JSON.stringify({ txid: 'y', txStatus: 'MINED', timestamp: '' }), lastEventId: '2' }); expect(client.lastEventId).toBe('2'); }); }); }); //# sourceMappingURL=ArcSSEClient.test.js.map