@open-mercato/events 0.8.1-develop.7296.1.2111d779db → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,60 @@
1
- jest.mock('@open-mercato/shared/lib/api/context', () => ({
2
- resolveRequestContext: jest.fn(async () => ({
1
+ type MockAuth = {
2
+ tenantId: string | null
3
+ sub: string
4
+ orgId: string | null
5
+ roles: string[]
6
+ isApiKey?: boolean
7
+ isSuperAdmin?: boolean
8
+ }
9
+
10
+ type MockOrganizationScope = {
11
+ selectedId: string | null
12
+ filterIds: string[] | null
13
+ allowedIds: string[] | null
14
+ tenantId: string | null
15
+ selectionRejected?: boolean
16
+ }
17
+
18
+ type MockOrganizationScopeInput = {
19
+ auth: MockAuth | null | undefined
20
+ request?: Request
21
+ }
22
+
23
+ const mockResolveOrganizationScopeForRequest = jest.fn(
24
+ async ({ auth }: MockOrganizationScopeInput): Promise<MockOrganizationScope> => {
25
+ const selectedId = auth?.orgId ?? null
26
+ return {
27
+ selectedId,
28
+ filterIds: selectedId ? [selectedId] : null,
29
+ allowedIds: selectedId ? [selectedId] : null,
30
+ tenantId: auth?.tenantId ?? null,
31
+ }
32
+ },
33
+ )
34
+
35
+ function buildResolvedContext(auth: MockAuth | null = {
36
+ tenantId: 't1',
37
+ sub: 'u1',
38
+ orgId: 'o1',
39
+ roles: ['admin'],
40
+ }) {
41
+ return {
3
42
  ctx: {
4
- auth: { tenantId: 't1', sub: 'u1', orgId: 'o1', roles: ['admin'] },
5
- selectedOrganizationId: 'o1',
43
+ auth,
44
+ container: {
45
+ resolve(name: string) {
46
+ if (name !== 'organizationScopeService') throw new Error(`Unexpected service: ${name}`)
47
+ return { resolveForRequest: mockResolveOrganizationScopeForRequest }
48
+ },
49
+ },
6
50
  },
7
- container: {},
8
- })),
51
+ }
52
+ }
53
+
54
+ const mockResolveRequestContext = jest.fn(async () => buildResolvedContext())
55
+
56
+ jest.mock('@open-mercato/shared/lib/api/context', () => ({
57
+ resolveRequestContext: (...args: unknown[]) => mockResolveRequestContext(...args),
9
58
  }))
10
59
 
11
60
  type EmitOptions = {
@@ -54,19 +103,160 @@ createModuleEvents({
54
103
 
55
104
  // req.signal is a linked/derived signal in Node, so we spy AFTER the
56
105
  // Request is constructed to intercept the handler's real calls.
57
- function makeTrackedRequest() {
106
+ function makeTrackedRequest(init: RequestInit = {}) {
58
107
  const controller = new AbortController()
59
- const req = new Request('http://localhost/api/events/stream', { signal: controller.signal })
108
+ const req = new Request('http://localhost/api/events/stream', { ...init, signal: controller.signal })
60
109
  const addSpy = jest.spyOn(req.signal, 'addEventListener')
61
110
  const removeSpy = jest.spyOn(req.signal, 'removeEventListener')
62
111
  return { req, controller, addSpy, removeSpy }
63
112
  }
64
113
 
114
+ function deferred<T>() {
115
+ let resolve!: (value: T) => void
116
+ let reject!: (reason?: unknown) => void
117
+ const promise = new Promise<T>((resolvePromise, rejectPromise) => {
118
+ resolve = resolvePromise
119
+ reject = rejectPromise
120
+ })
121
+ return { promise, resolve, reject }
122
+ }
123
+
124
+ async function flushPromises(): Promise<void> {
125
+ await Promise.resolve()
126
+ await Promise.resolve()
127
+ }
128
+
65
129
  describe('SSE event stream — abort listener hygiene', () => {
130
+ beforeEach(() => {
131
+ mockResolveRequestContext.mockReset()
132
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext())
133
+ mockResolveOrganizationScopeForRequest.mockReset()
134
+ mockResolveOrganizationScopeForRequest.mockImplementation(async ({ auth }: MockOrganizationScopeInput) => {
135
+ const selectedId = auth?.orgId ?? null
136
+ return {
137
+ selectedId,
138
+ filterIds: selectedId ? [selectedId] : null,
139
+ allowedIds: selectedId ? [selectedId] : null,
140
+ tenantId: auth?.tenantId ?? null,
141
+ }
142
+ })
143
+ delete process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS
144
+ delete process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS
145
+ jest.spyOn(Math, 'random').mockReturnValue(0.5)
146
+ })
147
+
66
148
  afterEach(() => {
149
+ jest.useRealTimers()
150
+ delete process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS
151
+ delete process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS
67
152
  jest.restoreAllMocks()
68
153
  })
69
154
 
155
+ it.each([
156
+ ['x-api-key', { 'x-api-key': 'key-secret', cookie: 'auth_token=staff-token' }],
157
+ ['Authorization ApiKey', { authorization: 'aPiKeY key-secret', cookie: 'auth_token=staff-token' }],
158
+ ])('rejects %s credentials before resolving request context', async (_credential, headers) => {
159
+ const { req, addSpy } = makeTrackedRequest({ headers })
160
+
161
+ const response = await GET(req)
162
+
163
+ expect(response.status).toBe(401)
164
+ expect(mockResolveRequestContext).not.toHaveBeenCalled()
165
+ expect(addSpy).not.toHaveBeenCalled()
166
+ })
167
+
168
+ it('rejects an API-key trusted context even when raw API-key headers are absent', async () => {
169
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext({
170
+ tenantId: 't1',
171
+ sub: 'api_key:key-1',
172
+ orgId: 'o1',
173
+ roles: ['admin'],
174
+ isApiKey: true,
175
+ }))
176
+ const { req, addSpy } = makeTrackedRequest()
177
+
178
+ const response = await GET(req)
179
+
180
+ expect(response.status).toBe(401)
181
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(1)
182
+ expect(addSpy).not.toHaveBeenCalled()
183
+ })
184
+
185
+ it('rejects a selected organization that the canonical DI scope does not allow', async () => {
186
+ mockResolveOrganizationScopeForRequest.mockResolvedValue({
187
+ selectedId: 'o2',
188
+ filterIds: ['o1'],
189
+ allowedIds: ['o1'],
190
+ tenantId: 't1',
191
+ })
192
+ const { req, addSpy } = makeTrackedRequest({
193
+ headers: { cookie: 'auth_token=staff-token; om_selected_org=o2' },
194
+ })
195
+
196
+ const response = await GET(req)
197
+
198
+ expect(response.status).toBe(401)
199
+ expect(mockResolveOrganizationScopeForRequest).toHaveBeenCalledWith({
200
+ auth: expect.objectContaining({ sub: 'u1' }),
201
+ request: req,
202
+ })
203
+ expect(addSpy).not.toHaveBeenCalled()
204
+ })
205
+
206
+ it('rejects an initial scope whose normalized tenant differs from canonical auth without registering it', async () => {
207
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext({
208
+ tenantId: ' t1 ',
209
+ sub: 'u1',
210
+ orgId: 'o1',
211
+ roles: ['admin'],
212
+ }))
213
+ mockResolveOrganizationScopeForRequest.mockResolvedValue({
214
+ selectedId: 'o1',
215
+ filterIds: ['o1'],
216
+ allowedIds: ['o1'],
217
+ tenantId: ' t2 ',
218
+ })
219
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
220
+ const { req, addSpy } = makeTrackedRequest()
221
+
222
+ const response = await GET(req)
223
+
224
+ expect(response.status).toBe(401)
225
+ expect(registerGlobalEventTapMock).not.toHaveBeenCalled()
226
+ expect(enqueueSpy).not.toHaveBeenCalled()
227
+ expect(addSpy).not.toHaveBeenCalled()
228
+ })
229
+
230
+ it.each([
231
+ ['staff cookie', { cookie: 'auth_token=cookie-token; om_selected_org=o1' }],
232
+ ['staff Bearer token', { authorization: 'Bearer bearer-token' }],
233
+ ])('copies %s credentials into a distinct canonical revalidation request', async (_credential, headers) => {
234
+ jest.useFakeTimers()
235
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
236
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
237
+ const { req } = makeTrackedRequest({ headers })
238
+ const trustedContextSymbol = Symbol.for('open-mercato.auth.trustedContext')
239
+ ;(req as unknown as Record<symbol, unknown>)[trustedContextSymbol] = { auth: { sub: 'stale' } }
240
+ const response = await GET(req)
241
+ const reader = response.body!.getReader()
242
+ await reader.read()
243
+
244
+ await jest.advanceTimersByTimeAsync(1000)
245
+
246
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(2)
247
+ const validationRequest = mockResolveRequestContext.mock.calls[1][0] as Request
248
+ expect(validationRequest).not.toBe(req)
249
+ expect(validationRequest.headers.get('cookie')).toBe(req.headers.get('cookie'))
250
+ expect(validationRequest.headers.get('authorization')).toBe(req.headers.get('authorization'))
251
+ expect((validationRequest as unknown as Record<symbol, unknown>)[trustedContextSymbol]).toBeUndefined()
252
+ expect(mockResolveOrganizationScopeForRequest).toHaveBeenCalledTimes(2)
253
+ expect(mockResolveOrganizationScopeForRequest.mock.calls[1][0]).toEqual({
254
+ auth: expect.objectContaining({ sub: 'u1' }),
255
+ request: validationRequest,
256
+ })
257
+ await reader.cancel()
258
+ })
259
+
70
260
  it('registers the abort listener with { once: true }', async () => {
71
261
  const { req, addSpy } = makeTrackedRequest()
72
262
  const res = await GET(req)
@@ -109,6 +299,31 @@ describe('SSE event stream — abort listener hygiene', () => {
109
299
  try { await (res.body as ReadableStream).cancel() } catch {}
110
300
  })
111
301
 
302
+ it('fully cleans up an already-aborted request without registering a live connection', async () => {
303
+ jest.useFakeTimers()
304
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
305
+ const closeSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'close')
306
+ const { req, controller, addSpy, removeSpy } = makeTrackedRequest()
307
+ controller.abort()
308
+
309
+ const response = await GET(req)
310
+ const reader = response.body!.getReader()
311
+ await reader.read()
312
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
313
+ const enqueueCountAfterClose = enqueueSpy.mock.calls.length
314
+
315
+ await mockGlobalEventTap?.(
316
+ 'stream_privacy_test.browser',
317
+ { tenantId: 't1', organizationId: 'o1', marker: 'must-not-arrive' },
318
+ )
319
+
320
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterClose)
321
+ expect(closeSpy).toHaveBeenCalledTimes(1)
322
+ expect(addSpy.mock.calls.filter((call) => call[0] === 'abort')).toHaveLength(1)
323
+ expect(removeSpy.mock.calls.filter((call) => call[0] === 'abort')).toHaveLength(1)
324
+ expect(jest.getTimerCount()).toBe(0)
325
+ })
326
+
112
327
  it('flushes an initial connected comment so EventSource opens immediately', async () => {
113
328
  const { req } = makeTrackedRequest()
114
329
  const res = await GET(req)
@@ -167,6 +382,103 @@ describe('SSE event stream — abort listener hygiene', () => {
167
382
  try { await reader.cancel() } catch {}
168
383
  })
169
384
 
385
+ it('still receives home-organization events when "All organizations" is selected', async () => {
386
+ // An unrestricted admin's organization switcher persists `om_selected_org=__all__`,
387
+ // so the resolved scope has NO concrete `selectedId`. Deriving the connection's
388
+ // organization from `selectedId` alone leaves it null, and `matchesAudience`
389
+ // then drops every organization-scoped event — the bridge goes silent on the
390
+ // default selection (TC-WF-059).
391
+ mockResolveOrganizationScopeForRequest.mockResolvedValue({
392
+ selectedId: null,
393
+ filterIds: null,
394
+ allowedIds: null,
395
+ tenantId: 't1',
396
+ })
397
+ const { req } = makeTrackedRequest({
398
+ headers: { cookie: 'auth_token=staff-token; om_selected_org=__all__' },
399
+ })
400
+ const res = await GET(req)
401
+ expect(res.status).toBe(200)
402
+
403
+ const reader = (res.body as ReadableStream<Uint8Array>).getReader()
404
+ await reader.read()
405
+
406
+ await mockGlobalEventTap?.(
407
+ 'stream_privacy_test.browser',
408
+ { tenantId: 't1', marker: 'all-orgs-expected' },
409
+ { tenantId: 't1', organizationId: 'o1' },
410
+ )
411
+
412
+ const { value, done } = await reader.read()
413
+ expect(done).toBe(false)
414
+ expect(new TextDecoder().decode(value)).toContain('"marker":"all-orgs-expected"')
415
+
416
+ try { await reader.cancel() } catch {}
417
+ })
418
+
419
+ it('resolves ordinary multi-org staff scope through DI when replacing an O1 stream with O2', async () => {
420
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
421
+ const ordinaryStaffAuth: MockAuth = {
422
+ tenantId: 't1',
423
+ sub: 'u1',
424
+ orgId: 'o1',
425
+ roles: ['staff'],
426
+ isSuperAdmin: false,
427
+ }
428
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext(ordinaryStaffAuth))
429
+ mockResolveOrganizationScopeForRequest.mockImplementation(async ({ auth, request }: MockOrganizationScopeInput) => {
430
+ const cookie = request?.headers.get('cookie') ?? ''
431
+ const requestedId = cookie.includes('om_selected_org=o2') ? 'o2' : 'o1'
432
+ const allowedIds = ['o1', 'o2']
433
+ return {
434
+ selectedId: allowedIds.includes(requestedId) ? requestedId : 'o1',
435
+ filterIds: [requestedId],
436
+ allowedIds,
437
+ tenantId: auth?.tenantId ?? null,
438
+ ...(allowedIds.includes(requestedId) ? {} : { selectionRejected: true }),
439
+ }
440
+ })
441
+ const { req: openingRequest } = makeTrackedRequest({
442
+ headers: { cookie: 'auth_token=staff-token; om_selected_org=o1' },
443
+ })
444
+ const openingResponse = await GET(openingRequest)
445
+ const openingReader = openingResponse.body!.getReader()
446
+ await openingReader.read()
447
+ await openingReader.cancel()
448
+ const enqueueCountAfterOpeningClose = enqueueSpy.mock.calls.length
449
+
450
+ const { req: replacementRequest } = makeTrackedRequest({
451
+ headers: { cookie: 'auth_token=staff-token; om_selected_org=o2' },
452
+ })
453
+ const replacementResponse = await GET(replacementRequest)
454
+ const replacementReader = replacementResponse.body!.getReader()
455
+ await replacementReader.read()
456
+
457
+ await mockGlobalEventTap?.(
458
+ 'stream_privacy_test.browser',
459
+ { tenantId: 't1', organizationId: 'o1', marker: 'closed-opening-scope' },
460
+ )
461
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterOpeningClose + 1)
462
+ await mockGlobalEventTap?.(
463
+ 'stream_privacy_test.browser',
464
+ { tenantId: 't1', organizationId: 'o2', marker: 'current-request-scope' },
465
+ )
466
+
467
+ const delivered = await replacementReader.read()
468
+ const decoded = new TextDecoder().decode(delivered.value)
469
+ expect(decoded).toContain('current-request-scope')
470
+ expect(decoded).not.toContain('closed-opening-scope')
471
+ expect(mockResolveOrganizationScopeForRequest).toHaveBeenNthCalledWith(1, {
472
+ auth: ordinaryStaffAuth,
473
+ request: openingRequest,
474
+ })
475
+ expect(mockResolveOrganizationScopeForRequest).toHaveBeenNthCalledWith(2, {
476
+ auth: ordinaryStaffAuth,
477
+ request: replacementRequest,
478
+ })
479
+ await replacementReader.cancel()
480
+ })
481
+
170
482
  it('honors a trusted multi-organization audience for a clientBroadcast event', async () => {
171
483
  const { req } = makeTrackedRequest()
172
484
  const res = await GET(req)
@@ -486,4 +798,334 @@ describe('SSE event stream — abort listener hygiene', () => {
486
798
  jest.restoreAllMocks()
487
799
  }
488
800
  })
801
+
802
+ it('closes and fully cleans up when canonical recipient roles change', async () => {
803
+ jest.useFakeTimers()
804
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
805
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
806
+ const { req, addSpy, removeSpy } = makeTrackedRequest()
807
+ const response = await GET(req)
808
+ const reader = response.body!.getReader()
809
+ await reader.read()
810
+ const attachedListener = addSpy.mock.calls.find((call) => call[0] === 'abort')![1]
811
+ expect(jest.getTimerCount()).toBe(3)
812
+
813
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext({
814
+ tenantId: 't1',
815
+ sub: 'u1',
816
+ orgId: 'o1',
817
+ roles: ['viewer'],
818
+ }))
819
+ await jest.advanceTimersByTimeAsync(1000)
820
+
821
+ await mockGlobalEventTap?.(
822
+ 'stream_privacy_test.browser',
823
+ { tenantId: 't1', organizationId: 'o1', recipientRoleId: 'admin', marker: 'revoked-role' },
824
+ )
825
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
826
+ expect(removeSpy).toHaveBeenCalledWith('abort', attachedListener)
827
+ expect(jest.getTimerCount()).toBe(0)
828
+ })
829
+
830
+ it('closes on periodic canonical auth/scope tenant mismatch without delivery or resurrection', async () => {
831
+ jest.useFakeTimers()
832
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
833
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
834
+ mockResolveOrganizationScopeForRequest
835
+ .mockResolvedValueOnce({
836
+ selectedId: 'o1',
837
+ filterIds: ['o1'],
838
+ allowedIds: ['o1'],
839
+ tenantId: 't1',
840
+ })
841
+ .mockResolvedValue({
842
+ selectedId: 'o1',
843
+ filterIds: ['o1'],
844
+ allowedIds: ['o1'],
845
+ tenantId: 't2',
846
+ })
847
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
848
+ const { req } = makeTrackedRequest()
849
+ const response = await GET(req)
850
+ const reader = response.body!.getReader()
851
+ await reader.read()
852
+
853
+ await jest.advanceTimersByTimeAsync(1000)
854
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
855
+ const enqueueCountAfterClose = enqueueSpy.mock.calls.length
856
+
857
+ await mockGlobalEventTap?.(
858
+ 'stream_privacy_test.browser',
859
+ { tenantId: 't1', organizationId: 'o1', marker: 'must-not-arrive' },
860
+ )
861
+ await jest.advanceTimersByTimeAsync(5000)
862
+
863
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterClose)
864
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(2)
865
+ expect(mockResolveOrganizationScopeForRequest).toHaveBeenCalledTimes(2)
866
+ expect(jest.getTimerCount()).toBe(0)
867
+ })
868
+
869
+ it.each([
870
+ {
871
+ change: 'tenant move',
872
+ nextContext: buildResolvedContext({ tenantId: 't2', sub: 'u1', orgId: 'o1', roles: ['admin'] }),
873
+ },
874
+ {
875
+ change: 'organization move',
876
+ nextContext: buildResolvedContext({ tenantId: 't1', sub: 'u1', orgId: 'o2', roles: ['admin'] }),
877
+ },
878
+ ])('closes after a canonical $change', async ({ nextContext }) => {
879
+ jest.useFakeTimers()
880
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
881
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
882
+ const { req } = makeTrackedRequest()
883
+ const response = await GET(req)
884
+ const reader = response.body!.getReader()
885
+ await reader.read()
886
+
887
+ mockResolveRequestContext.mockResolvedValue(nextContext)
888
+ await jest.advanceTimersByTimeAsync(1000)
889
+
890
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
891
+ expect(jest.getTimerCount()).toBe(0)
892
+ })
893
+
894
+ it('closes when canonical identity is missing', async () => {
895
+ jest.useFakeTimers()
896
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
897
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
898
+ const { req } = makeTrackedRequest()
899
+ const response = await GET(req)
900
+ const reader = response.body!.getReader()
901
+ await reader.read()
902
+
903
+ mockResolveRequestContext.mockResolvedValue(buildResolvedContext(null))
904
+ await jest.advanceTimersByTimeAsync(1000)
905
+
906
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
907
+ expect(jest.getTimerCount()).toBe(0)
908
+ })
909
+
910
+ it('fails closed when canonical auth validation throws', async () => {
911
+ jest.useFakeTimers()
912
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
913
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
914
+ const { req } = makeTrackedRequest()
915
+ const response = await GET(req)
916
+ const reader = response.body!.getReader()
917
+ await reader.read()
918
+
919
+ mockResolveRequestContext.mockRejectedValue(new Error('validation unavailable'))
920
+ await jest.advanceTimersByTimeAsync(1000)
921
+
922
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
923
+ expect(jest.getTimerCount()).toBe(0)
924
+ })
925
+
926
+ it('caps connection lifetime and leaves EventSource free to reconnect', async () => {
927
+ jest.useFakeTimers()
928
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '10000'
929
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '1000'
930
+ const { req, addSpy, removeSpy } = makeTrackedRequest()
931
+ const response = await GET(req)
932
+ const reader = response.body!.getReader()
933
+ await reader.read()
934
+ const attachedListener = addSpy.mock.calls.find((call) => call[0] === 'abort')![1]
935
+
936
+ await jest.advanceTimersByTimeAsync(1000)
937
+
938
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
939
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(1)
940
+ expect(removeSpy).toHaveBeenCalledWith('abort', attachedListener)
941
+ expect(jest.getTimerCount()).toBe(0)
942
+ })
943
+
944
+ it.each([
945
+ ['the low end', 0, 8_500],
946
+ ['the high end', 1, 11_500],
947
+ ])('jitters the configured maximum age by 15%% at %s of the random range', async (_case, randomValue, expectedDelayMs) => {
948
+ jest.useFakeTimers()
949
+ jest.spyOn(Math, 'random').mockReturnValue(randomValue)
950
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '999999'
951
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
952
+ const closeSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'close')
953
+ const { req } = makeTrackedRequest()
954
+ const response = await GET(req)
955
+ const reader = response.body!.getReader()
956
+ await reader.read()
957
+
958
+ jest.advanceTimersByTime(expectedDelayMs - 1)
959
+ expect(closeSpy).not.toHaveBeenCalled()
960
+ jest.advanceTimersByTime(1)
961
+
962
+ expect(closeSpy).toHaveBeenCalledTimes(1)
963
+ expect(jest.getTimerCount()).toBe(0)
964
+ await reader.cancel()
965
+ })
966
+
967
+ it.each([
968
+ ['the minimum age', '1000', 0, 1_000],
969
+ ['the Node timer maximum', '2147483647', 1, 2_147_483_647],
970
+ ])('keeps the jittered maximum age within %s', async (_case, configuredValue, randomValue, expectedDelayMs) => {
971
+ jest.useFakeTimers()
972
+ jest.spyOn(Math, 'random').mockReturnValue(randomValue)
973
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '999999'
974
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = configuredValue
975
+ const timeoutSpy = jest.spyOn(globalThis, 'setTimeout')
976
+ const { req } = makeTrackedRequest()
977
+
978
+ const response = await GET(req)
979
+ const reader = response.body!.getReader()
980
+ await reader.read()
981
+
982
+ expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedDelayMs)
983
+ await reader.cancel()
984
+ })
985
+
986
+ it.each(['0', 'not-a-number', 'Infinity'])('falls back to a finite maximum age for invalid value %s', async (invalidValue) => {
987
+ jest.useFakeTimers()
988
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '999999'
989
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = invalidValue
990
+ const closeSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'close')
991
+ const { req } = makeTrackedRequest()
992
+ const response = await GET(req)
993
+ const reader = response.body!.getReader()
994
+ await reader.read()
995
+
996
+ jest.advanceTimersByTime(299_999)
997
+ expect(closeSpy).not.toHaveBeenCalled()
998
+ jest.advanceTimersByTime(1)
999
+
1000
+ expect(closeSpy).toHaveBeenCalledTimes(1)
1001
+ expect(jest.getTimerCount()).toBe(0)
1002
+ await reader.cancel()
1003
+ })
1004
+
1005
+ it.each([
1006
+ ['the exact Node timer maximum', '2147483647'],
1007
+ ['one millisecond above the Node timer maximum', '2147483648'],
1008
+ ['an oversized finite integer', '9007199254740991'],
1009
+ ])('bounds SSE timers for %s', async (_case, configuredValue) => {
1010
+ jest.useFakeTimers()
1011
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = configuredValue
1012
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = configuredValue
1013
+ const intervalSpy = jest.spyOn(globalThis, 'setInterval')
1014
+ const timeoutSpy = jest.spyOn(globalThis, 'setTimeout')
1015
+ const { req } = makeTrackedRequest()
1016
+
1017
+ const response = await GET(req)
1018
+ const reader = response.body!.getReader()
1019
+ await reader.read()
1020
+
1021
+ expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 2_147_483_647)
1022
+ expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2_147_483_647)
1023
+ await reader.cancel()
1024
+ expect(jest.getTimerCount()).toBe(0)
1025
+ })
1026
+
1027
+ it('cannot resurrect or deliver after cancellation during deferred validation', async () => {
1028
+ jest.useFakeTimers()
1029
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
1030
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
1031
+ const validation = deferred<ReturnType<typeof buildResolvedContext>>()
1032
+ mockResolveRequestContext
1033
+ .mockResolvedValueOnce(buildResolvedContext())
1034
+ .mockImplementationOnce(() => validation.promise)
1035
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
1036
+ const { req, addSpy, removeSpy } = makeTrackedRequest()
1037
+ const response = await GET(req)
1038
+ const reader = response.body!.getReader()
1039
+ await reader.read()
1040
+ const attachedListener = addSpy.mock.calls.find((call) => call[0] === 'abort')![1]
1041
+
1042
+ jest.advanceTimersByTime(1000)
1043
+ await flushPromises()
1044
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(2)
1045
+ await reader.cancel()
1046
+ const enqueueCountAfterCancel = enqueueSpy.mock.calls.length
1047
+
1048
+ validation.resolve(buildResolvedContext())
1049
+ await flushPromises()
1050
+ await mockGlobalEventTap?.(
1051
+ 'stream_privacy_test.browser',
1052
+ { tenantId: 't1', organizationId: 'o1', marker: 'must-not-arrive' },
1053
+ )
1054
+
1055
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterCancel)
1056
+ expect(removeSpy.mock.calls.filter((call) => call[0] === 'abort' && call[1] === attachedListener)).toHaveLength(1)
1057
+ expect(jest.getTimerCount()).toBe(0)
1058
+ })
1059
+
1060
+ it('cannot resurrect after maximum age closes during deferred validation', async () => {
1061
+ jest.useFakeTimers()
1062
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
1063
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '1500'
1064
+ const validation = deferred<ReturnType<typeof buildResolvedContext>>()
1065
+ mockResolveRequestContext
1066
+ .mockResolvedValueOnce(buildResolvedContext())
1067
+ .mockImplementationOnce(() => validation.promise)
1068
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
1069
+ const closeSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'close')
1070
+ const { req, addSpy, removeSpy } = makeTrackedRequest()
1071
+ const response = await GET(req)
1072
+ const reader = response.body!.getReader()
1073
+ await reader.read()
1074
+ const attachedListener = addSpy.mock.calls.find((call) => call[0] === 'abort')![1]
1075
+
1076
+ jest.advanceTimersByTime(1000)
1077
+ await flushPromises()
1078
+ expect(mockResolveRequestContext).toHaveBeenCalledTimes(2)
1079
+ jest.advanceTimersByTime(500)
1080
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
1081
+ const enqueueCountAfterClose = enqueueSpy.mock.calls.length
1082
+
1083
+ validation.resolve(buildResolvedContext())
1084
+ await flushPromises()
1085
+ await mockGlobalEventTap?.(
1086
+ 'stream_privacy_test.browser',
1087
+ { tenantId: 't1', organizationId: 'o1', marker: 'must-not-arrive' },
1088
+ )
1089
+
1090
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterClose)
1091
+ expect(closeSpy).toHaveBeenCalledTimes(1)
1092
+ expect(removeSpy.mock.calls.filter((call) => call[0] === 'abort' && call[1] === attachedListener)).toHaveLength(1)
1093
+ expect(jest.getTimerCount()).toBe(0)
1094
+ })
1095
+
1096
+ it('ignores a late validation failure after abort cleanup', async () => {
1097
+ jest.useFakeTimers()
1098
+ process.env.OM_EVENTS_SSE_AUTH_REVALIDATION_INTERVAL_MS = '1000'
1099
+ process.env.OM_EVENTS_SSE_CONNECTION_MAX_AGE_MS = '10000'
1100
+ const validation = deferred<ReturnType<typeof buildResolvedContext>>()
1101
+ mockResolveRequestContext
1102
+ .mockResolvedValueOnce(buildResolvedContext())
1103
+ .mockImplementationOnce(() => validation.promise)
1104
+ const enqueueSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'enqueue')
1105
+ const closeSpy = jest.spyOn(ReadableStreamDefaultController.prototype, 'close')
1106
+ const { req, controller, addSpy, removeSpy } = makeTrackedRequest()
1107
+ const response = await GET(req)
1108
+ const reader = response.body!.getReader()
1109
+ await reader.read()
1110
+ const attachedListener = addSpy.mock.calls.find((call) => call[0] === 'abort')![1]
1111
+
1112
+ jest.advanceTimersByTime(1000)
1113
+ await flushPromises()
1114
+ controller.abort()
1115
+ await flushPromises()
1116
+ await expect(reader.read()).resolves.toEqual({ value: undefined, done: true })
1117
+ const enqueueCountAfterClose = enqueueSpy.mock.calls.length
1118
+
1119
+ validation.reject(new Error('late validation failure'))
1120
+ await flushPromises()
1121
+ await mockGlobalEventTap?.(
1122
+ 'stream_privacy_test.browser',
1123
+ { tenantId: 't1', organizationId: 'o1', marker: 'must-not-arrive' },
1124
+ )
1125
+
1126
+ expect(enqueueSpy).toHaveBeenCalledTimes(enqueueCountAfterClose)
1127
+ expect(closeSpy).toHaveBeenCalledTimes(1)
1128
+ expect(removeSpy.mock.calls.filter((call) => call[0] === 'abort' && call[1] === attachedListener)).toHaveLength(1)
1129
+ expect(jest.getTimerCount()).toBe(0)
1130
+ })
489
1131
  })