From bc087c7dbdd6b4bee29bf2b63b4fade8e6375835 Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:14:20 +0200 Subject: [PATCH] test(frontend): type sse.test.ts against the real fetch-event-source types Uses the library's own FetchEventSourceInit/EventSourceMessage types instead of any for the mocked options object, so the test would catch a real signature drift in connectSSE's usage of the library. --- flowsint-app/src/api/sse.test.ts | 58 +++++++++++++++++++------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/flowsint-app/src/api/sse.test.ts b/flowsint-app/src/api/sse.test.ts index 43e98098..60bc8f9b 100644 --- a/flowsint-app/src/api/sse.test.ts +++ b/flowsint-app/src/api/sse.test.ts @@ -1,14 +1,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { FetchEventSourceInit, EventSourceMessage } from '@microsoft/fetch-event-source' +import type { SSEEnvelope } from './sse' // Capture the options passed to fetchEventSource so we can drive its callbacks. -const calls: any[] = [] -const fetchEventSourceMock = vi.fn((url: string, opts: any) => { +const calls: { url: string; opts: FetchEventSourceInit }[] = [] +const fetchEventSourceMock = vi.fn((url: string, opts: FetchEventSourceInit) => { calls.push({ url, opts }) return new Promise(() => {}) // never resolves; we drive callbacks manually }) vi.mock('@microsoft/fetch-event-source', () => ({ - fetchEventSource: (url: string, opts: any) => fetchEventSourceMock(url, opts) + fetchEventSource: (url: string, opts: FetchEventSourceInit) => fetchEventSourceMock(url, opts) })) vi.mock('@/stores/auth-store', () => ({ @@ -20,7 +22,15 @@ import { connectSSE } from './sse' const lastOpts = () => calls[calls.length - 1].opts const fakeResponse = (status: number, contentType: string) => - ({ ok: status >= 200 && status < 300, status, headers: { get: () => contentType } }) as any + ({ + ok: status >= 200 && status < 300, + status, + headers: { get: () => contentType } + }) as unknown as Response + +// The tests only ever care about `data` — id/event are required by the +// library's type but irrelevant to connectSSE's own parsing. +const message = (data: string): EventSourceMessage => ({ id: '', event: '', data }) beforeEach(() => { calls.length = 0 @@ -35,45 +45,47 @@ describe('connectSSE', () => { }) it('forwards parsed envelopes and skips "connected"', () => { - const received: any[] = [] + const received: SSEEnvelope[] = [] connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) }) const o = lastOpts() - o.onmessage({ data: JSON.stringify({ event: 'connected', data: 'hi' }) }) - o.onmessage({ data: JSON.stringify({ event: 'log', data: '{"msg":"a"}' }) }) + o.onmessage?.(message(JSON.stringify({ event: 'connected', data: 'hi' }))) + o.onmessage?.(message(JSON.stringify({ event: 'log', data: '{"msg":"a"}' }))) expect(received).toEqual([{ event: 'log', data: '{"msg":"a"}' }]) }) it('ignores empty and malformed messages without throwing', () => { - const received: any[] = [] + const received: SSEEnvelope[] = [] connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) }) const o = lastOpts() - o.onmessage({ data: '' }) - o.onmessage({ data: 'not-json' }) + o.onmessage?.(message('')) + o.onmessage?.(message('not-json')) expect(received).toEqual([]) }) it('aborts the request when the disposer is called', () => { const dispose = connectSSE({ url: 'http://x', onMessage: () => {} }) - expect(lastOpts().signal.aborted).toBe(false) + expect(lastOpts().signal?.aborted).toBe(false) dispose() - expect(lastOpts().signal.aborted).toBe(true) + expect(lastOpts().signal?.aborted).toBe(true) }) it('throws (no retry) on a non-event-stream open', async () => { connectSSE({ url: 'http://x', onMessage: () => {} }) - await expect(lastOpts().onopen(fakeResponse(401, 'application/json'))).rejects.toThrow() + await expect(lastOpts().onopen?.(fakeResponse(401, 'application/json'))).rejects.toThrow() }) it('accepts a valid event-stream open', async () => { connectSSE({ url: 'http://x', onMessage: () => {} }) - await expect(lastOpts().onopen(fakeResponse(200, 'text/event-stream'))).resolves.toBeUndefined() + await expect( + lastOpts().onopen?.(fakeResponse(200, 'text/event-stream')) + ).resolves.toBeUndefined() }) it('returns increasing backoff for transient errors', () => { connectSSE({ url: 'http://x', onMessage: () => {} }) const o = lastOpts() - const first = o.onerror(new Error('network')) - const second = o.onerror(new Error('network')) + const first = o.onerror?.(new Error('network')) + const second = o.onerror?.(new Error('network')) expect(first).toBe(2000) expect(second).toBe(4000) }) @@ -83,26 +95,26 @@ describe('connectSSE', () => { const o = lastOpts() let fatal: unknown try { - await o.onopen(fakeResponse(403, 'application/json')) + await o.onopen?.(fakeResponse(403, 'application/json')) } catch (e) { fatal = e } - expect(() => o.onerror(fatal)).toThrow() + expect(() => o.onerror?.(fatal)).toThrow() }) it('ignores valid JSON that is not an envelope', () => { - const received: any[] = [] + const received: SSEEnvelope[] = [] connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) }) const o = lastOpts() - o.onmessage({ data: '42' }) - o.onmessage({ data: 'null' }) - o.onmessage({ data: '{"data":"no-event"}' }) + o.onmessage?.(message('42')) + o.onmessage?.(message('null')) + o.onmessage?.(message('{"data":"no-event"}')) expect(received).toEqual([]) }) it('calls onError once when the connection promise rejects (non-fatal)', async () => { const errors: unknown[] = [] - fetchEventSourceMock.mockImplementationOnce((url: string, opts: any) => { + fetchEventSourceMock.mockImplementationOnce((url: string, opts: FetchEventSourceInit) => { calls.push({ url, opts }) return Promise.reject(new Error('boom')) })