@durable-streams/server-conformance-tests 0.1.4 → 0.1.6

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.
@@ -61,7 +61,7 @@ async function fetchSSE(url, opts = {}) {
61
61
  */
62
62
  function parseSSEEvents(sseText) {
63
63
  const events = [];
64
- const normalized = sseText.replace(/\r\n/g, `\n`);
64
+ const normalized = sseText.replace(/\r\n/g, `\n`).replace(/\r/g, `\n`);
65
65
  const eventBlocks = normalized.split(`\n\n`).filter((block) => block.trim());
66
66
  for (const block of eventBlocks) {
67
67
  const lines = block.split(`\n`);
@@ -84,6 +84,7 @@ function parseSSEEvents(sseText) {
84
84
  */
85
85
  function runConformanceTests(options) {
86
86
  const getBaseUrl = () => options.baseUrl;
87
+ const getLongPollTestTimeoutMs = () => (options.longPollTimeoutMs ?? 2e4) + 1e3;
87
88
  (0, vitest.describe)(`Basic Stream Operations`, () => {
88
89
  (0, vitest.test)(`should create a stream`, async () => {
89
90
  const streamPath = `/v1/stream/create-test-${Date.now()}`;
@@ -252,13 +253,14 @@ function runConformanceTests(options) {
252
253
  const readPromise = (async () => {
253
254
  const res = await stream.stream({ live: `long-poll` });
254
255
  await new Promise((resolve) => {
255
- const unsubscribe = res.subscribeBytes(async (chunk) => {
256
+ const unsubscribe = res.subscribeBytes((chunk) => {
256
257
  if (chunk.data.length > 0) receivedData.push(new TextDecoder().decode(chunk.data));
257
258
  if (receivedData.length >= 1) {
258
259
  unsubscribe();
259
260
  res.cancel();
260
261
  resolve();
261
262
  }
263
+ return Promise.resolve();
262
264
  });
263
265
  });
264
266
  })();
@@ -266,7 +268,7 @@ function runConformanceTests(options) {
266
268
  await stream.append(`new data`);
267
269
  await readPromise;
268
270
  (0, vitest.expect)(receivedData).toContain(`new data`);
269
- }, 1e4);
271
+ }, getLongPollTestTimeoutMs());
270
272
  (0, vitest.test)(`should return immediately if data already exists`, async () => {
271
273
  const streamPath = `/v1/stream/longpoll-immediate-test-${Date.now()}`;
272
274
  const stream = await __durable_streams_client.DurableStream.create({
@@ -301,7 +303,7 @@ function runConformanceTests(options) {
301
303
  method: `PUT`,
302
304
  headers: { "Content-Type": `text/plain` }
303
305
  });
304
- (0, vitest.expect)([200, 204]).toContain(secondResponse.status);
306
+ (0, vitest.expect)(secondResponse.status).toBe(200);
305
307
  });
306
308
  (0, vitest.test)(`should return 409 on PUT with different config`, async () => {
307
309
  const streamPath = `/v1/stream/config-conflict-test-${Date.now()}`;
@@ -326,7 +328,7 @@ function runConformanceTests(options) {
326
328
  headers: { "Content-Type": `text/plain` },
327
329
  body: `hello world`
328
330
  });
329
- (0, vitest.expect)([200, 204]).toContain(response.status);
331
+ (0, vitest.expect)(response.status).toBe(204);
330
332
  (0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
331
333
  });
332
334
  (0, vitest.test)(`should return 404 on POST to non-existent stream`, async () => {
@@ -509,7 +511,7 @@ function runConformanceTests(options) {
509
511
  },
510
512
  body: `second`
511
513
  });
512
- (0, vitest.expect)([200, 204]).toContain(response.status);
514
+ (0, vitest.expect)(response.status).toBe(204);
513
515
  });
514
516
  (0, vitest.test)(`should reject duplicate seq values`, async () => {
515
517
  const streamPath = `/v1/stream/seq-duplicate-test-${Date.now()}`;
@@ -536,6 +538,140 @@ function runConformanceTests(options) {
536
538
  (0, vitest.expect)(response.status).toBe(409);
537
539
  });
538
540
  });
541
+ (0, vitest.describe)(`Browser Security Headers`, () => {
542
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on GET responses`, async () => {
543
+ const streamPath = `/v1/stream/security-get-nosniff-${Date.now()}`;
544
+ await fetch(`${getBaseUrl()}${streamPath}`, {
545
+ method: `PUT`,
546
+ headers: { "Content-Type": `text/plain` },
547
+ body: `test data`
548
+ });
549
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
550
+ (0, vitest.expect)(response.status).toBe(200);
551
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
552
+ });
553
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on PUT responses`, async () => {
554
+ const streamPath = `/v1/stream/security-put-nosniff-${Date.now()}`;
555
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, {
556
+ method: `PUT`,
557
+ headers: { "Content-Type": `text/plain` }
558
+ });
559
+ (0, vitest.expect)(response.status).toBe(201);
560
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
561
+ });
562
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on POST responses`, async () => {
563
+ const streamPath = `/v1/stream/security-post-nosniff-${Date.now()}`;
564
+ await fetch(`${getBaseUrl()}${streamPath}`, {
565
+ method: `PUT`,
566
+ headers: { "Content-Type": `text/plain` }
567
+ });
568
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, {
569
+ method: `POST`,
570
+ headers: { "Content-Type": `text/plain` },
571
+ body: `data`
572
+ });
573
+ (0, vitest.expect)([200, 204]).toContain(response.status);
574
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
575
+ });
576
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on HEAD responses`, async () => {
577
+ const streamPath = `/v1/stream/security-head-nosniff-${Date.now()}`;
578
+ await fetch(`${getBaseUrl()}${streamPath}`, {
579
+ method: `PUT`,
580
+ headers: { "Content-Type": `text/plain` }
581
+ });
582
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
583
+ (0, vitest.expect)(response.status).toBe(200);
584
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
585
+ });
586
+ (0, vitest.test)(`should include Cross-Origin-Resource-Policy header on GET responses`, async () => {
587
+ const streamPath = `/v1/stream/security-corp-get-${Date.now()}`;
588
+ await fetch(`${getBaseUrl()}${streamPath}`, {
589
+ method: `PUT`,
590
+ headers: { "Content-Type": `application/octet-stream` },
591
+ body: new Uint8Array([
592
+ 1,
593
+ 2,
594
+ 3,
595
+ 4
596
+ ])
597
+ });
598
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
599
+ (0, vitest.expect)(response.status).toBe(200);
600
+ const corp = response.headers.get(`cross-origin-resource-policy`);
601
+ (0, vitest.expect)(corp).toBeDefined();
602
+ (0, vitest.expect)([
603
+ `cross-origin`,
604
+ `same-origin`,
605
+ `same-site`
606
+ ]).toContain(corp);
607
+ });
608
+ (0, vitest.test)(`should include Cache-Control: no-store on HEAD responses`, async () => {
609
+ const streamPath = `/v1/stream/security-head-cache-${Date.now()}`;
610
+ await fetch(`${getBaseUrl()}${streamPath}`, {
611
+ method: `PUT`,
612
+ headers: { "Content-Type": `text/plain` }
613
+ });
614
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
615
+ (0, vitest.expect)(response.status).toBe(200);
616
+ const cacheControl = response.headers.get(`cache-control`);
617
+ (0, vitest.expect)(cacheControl).toBeDefined();
618
+ (0, vitest.expect)(cacheControl).toContain(`no-store`);
619
+ });
620
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on SSE responses`, async () => {
621
+ const streamPath = `/v1/stream/security-sse-nosniff-${Date.now()}`;
622
+ await fetch(`${getBaseUrl()}${streamPath}`, {
623
+ method: `PUT`,
624
+ headers: { "Content-Type": `application/json` },
625
+ body: JSON.stringify({ test: `data` })
626
+ });
627
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
628
+ const offset = headResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER) ?? `-1`;
629
+ const controller = new AbortController();
630
+ const timeoutId = setTimeout(() => controller.abort(), 500);
631
+ try {
632
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}&live=sse`, {
633
+ method: `GET`,
634
+ signal: controller.signal
635
+ });
636
+ (0, vitest.expect)(response.status).toBe(200);
637
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
638
+ } catch (e) {
639
+ if (!(e instanceof Error && e.name === `AbortError`)) throw e;
640
+ } finally {
641
+ clearTimeout(timeoutId);
642
+ }
643
+ });
644
+ (0, vitest.test)(`should include X-Content-Type-Options: nosniff on long-poll responses`, async () => {
645
+ const streamPath = `/v1/stream/security-longpoll-nosniff-${Date.now()}`;
646
+ await fetch(`${getBaseUrl()}${streamPath}`, {
647
+ method: `PUT`,
648
+ headers: { "Content-Type": `text/plain` },
649
+ body: `initial data`
650
+ });
651
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
652
+ const offset = headResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER) ?? `-1`;
653
+ const controller = new AbortController();
654
+ const timeoutId = setTimeout(() => controller.abort(), 500);
655
+ try {
656
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}&live=long-poll`, {
657
+ method: `GET`,
658
+ signal: controller.signal
659
+ });
660
+ (0, vitest.expect)([200, 204]).toContain(response.status);
661
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
662
+ } catch (e) {
663
+ if (!(e instanceof Error && e.name === `AbortError`)) throw e;
664
+ } finally {
665
+ clearTimeout(timeoutId);
666
+ }
667
+ });
668
+ (0, vitest.test)(`should include security headers on error responses`, async () => {
669
+ const streamPath = `/v1/stream/security-error-headers-${Date.now()}`;
670
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
671
+ (0, vitest.expect)(response.status).toBe(404);
672
+ (0, vitest.expect)(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
673
+ });
674
+ });
539
675
  (0, vitest.describe)(`TTL and Expiry Validation`, () => {
540
676
  (0, vitest.test)(`should reject both TTL and Expires-At (400)`, async () => {
541
677
  const streamPath = `/v1/stream/ttl-expires-conflict-test-${Date.now()}`;
@@ -606,7 +742,7 @@ function runConformanceTests(options) {
606
742
  headers: { "Content-Type": `TEXT/PLAIN` },
607
743
  body: `test`
608
744
  });
609
- (0, vitest.expect)([200, 204]).toContain(response.status);
745
+ (0, vitest.expect)(response.status).toBe(204);
610
746
  });
611
747
  (0, vitest.test)(`should allow idempotent create with different case content-type`, async () => {
612
748
  const streamPath = `/v1/stream/case-idempotent-test-${Date.now()}`;
@@ -619,7 +755,7 @@ function runConformanceTests(options) {
619
755
  method: `PUT`,
620
756
  headers: { "Content-Type": `APPLICATION/JSON` }
621
757
  });
622
- (0, vitest.expect)([200, 204]).toContain(response2.status);
758
+ (0, vitest.expect)(response2.status).toBe(200);
623
759
  });
624
760
  (0, vitest.test)(`should accept headers with different casing`, async () => {
625
761
  const streamPath = `/v1/stream/case-header-test-${Date.now()}`;
@@ -635,7 +771,7 @@ function runConformanceTests(options) {
635
771
  },
636
772
  body: `test`
637
773
  });
638
- (0, vitest.expect)([200, 204]).toContain(response.status);
774
+ (0, vitest.expect)(response.status).toBe(204);
639
775
  });
640
776
  });
641
777
  (0, vitest.describe)(`Content-Type Validation`, () => {
@@ -663,7 +799,7 @@ function runConformanceTests(options) {
663
799
  headers: { "Content-Type": `application/json` },
664
800
  body: `{"test": true}`
665
801
  });
666
- (0, vitest.expect)([200, 204]).toContain(response.status);
802
+ (0, vitest.expect)(response.status).toBe(204);
667
803
  });
668
804
  (0, vitest.test)(`should return stream content-type on GET`, async () => {
669
805
  const streamPath = `/v1/stream/content-type-get-test-${Date.now()}`;
@@ -955,7 +1091,8 @@ function runConformanceTests(options) {
955
1091
  (0, vitest.expect)(response.status).toBe(201);
956
1092
  const location = response.headers.get(`location`);
957
1093
  (0, vitest.expect)(location).toBeDefined();
958
- (0, vitest.expect)(location).toBe(`${getBaseUrl()}${streamPath}`);
1094
+ (0, vitest.expect)(location.endsWith(streamPath)).toBe(true);
1095
+ (0, vitest.expect)(() => new URL(location)).not.toThrow();
959
1096
  });
960
1097
  (0, vitest.test)(`should reject missing Content-Type on POST`, async () => {
961
1098
  const streamPath = `/v1/stream/missing-ct-post-test-${Date.now()}`;
@@ -1059,7 +1196,7 @@ function runConformanceTests(options) {
1059
1196
  clearTimeout(timeoutId);
1060
1197
  if (e instanceof Error && e.name !== `AbortError`) throw e;
1061
1198
  }
1062
- }, 1e4);
1199
+ }, getLongPollTestTimeoutMs());
1063
1200
  });
1064
1201
  (0, vitest.describe)(`TTL and Expiry Edge Cases`, () => {
1065
1202
  (0, vitest.test)(`should reject TTL with leading zeros`, async () => {
@@ -1159,7 +1296,7 @@ function runConformanceTests(options) {
1159
1296
  "Stream-TTL": `3600`
1160
1297
  }
1161
1298
  });
1162
- (0, vitest.expect)([200, 204]).toContain(response2.status);
1299
+ (0, vitest.expect)(response2.status).toBe(200);
1163
1300
  });
1164
1301
  (0, vitest.test)(`should reject idempotent PUT with different TTL`, async () => {
1165
1302
  const streamPath = `/v1/stream/ttl-conflict-test-${Date.now()}`;
@@ -1263,7 +1400,7 @@ function runConformanceTests(options) {
1263
1400
  headers: { "Content-Type": `text/plain` },
1264
1401
  body: `appended data`
1265
1402
  });
1266
- (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1403
+ (0, vitest.expect)(postBefore.status).toBe(204);
1267
1404
  await sleep(1500);
1268
1405
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1269
1406
  method: `POST`,
@@ -1323,7 +1460,7 @@ function runConformanceTests(options) {
1323
1460
  headers: { "Content-Type": `text/plain` },
1324
1461
  body: `appended data`
1325
1462
  });
1326
- (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1463
+ (0, vitest.expect)(postBefore.status).toBe(204);
1327
1464
  await sleep(1500);
1328
1465
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1329
1466
  method: `POST`,
@@ -1746,6 +1883,88 @@ function runConformanceTests(options) {
1746
1883
  (0, vitest.expect)(received).toContain(`data: line2`);
1747
1884
  (0, vitest.expect)(received).toContain(`data: line3`);
1748
1885
  });
1886
+ (0, vitest.test)(`should prevent CRLF injection in payloads - embedded event boundaries become literal data`, async () => {
1887
+ const streamPath = `/v1/stream/sse-crlf-injection-test-${Date.now()}`;
1888
+ const maliciousPayload = `safe content\r\n\r\nevent: control\r\ndata: {"injected":true}\r\n\r\nmore safe content`;
1889
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1890
+ method: `PUT`,
1891
+ headers: { "Content-Type": `text/plain` },
1892
+ body: maliciousPayload
1893
+ });
1894
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1895
+ (0, vitest.expect)(response.status).toBe(200);
1896
+ const events = parseSSEEvents(received);
1897
+ const dataEvents = events.filter((e) => e.type === `data`);
1898
+ const controlEvents = events.filter((e) => e.type === `control`);
1899
+ (0, vitest.expect)(dataEvents.length).toBe(1);
1900
+ (0, vitest.expect)(controlEvents.length).toBe(1);
1901
+ const dataContent = dataEvents[0].data;
1902
+ (0, vitest.expect)(dataContent).toContain(`event: control`);
1903
+ (0, vitest.expect)(dataContent).toContain(`data: {"injected":true}`);
1904
+ const controlContent = JSON.parse(controlEvents[0].data);
1905
+ (0, vitest.expect)(controlContent.injected).toBeUndefined();
1906
+ (0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
1907
+ });
1908
+ (0, vitest.test)(`should prevent CRLF injection - LF-only attack vectors`, async () => {
1909
+ const streamPath = `/v1/stream/sse-lf-injection-test-${Date.now()}`;
1910
+ const maliciousPayload = `start\n\nevent: data\ndata: fake-event\n\nend`;
1911
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1912
+ method: `PUT`,
1913
+ headers: { "Content-Type": `text/plain` },
1914
+ body: maliciousPayload
1915
+ });
1916
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1917
+ (0, vitest.expect)(response.status).toBe(200);
1918
+ const events = parseSSEEvents(received);
1919
+ const dataEvents = events.filter((e) => e.type === `data`);
1920
+ (0, vitest.expect)(dataEvents.length).toBe(1);
1921
+ const dataContent = dataEvents[0].data;
1922
+ (0, vitest.expect)(dataContent).toContain(`event: data`);
1923
+ (0, vitest.expect)(dataContent).toContain(`data: fake-event`);
1924
+ });
1925
+ (0, vitest.test)(`should prevent CRLF injection - carriage return only attack vectors`, async () => {
1926
+ const streamPath = `/v1/stream/sse-cr-injection-test-${Date.now()}`;
1927
+ const maliciousPayload = `start\r\revent: control\rdata: {"cr_injected":true}\r\rend`;
1928
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1929
+ method: `PUT`,
1930
+ headers: { "Content-Type": `text/plain` },
1931
+ body: maliciousPayload
1932
+ });
1933
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1934
+ (0, vitest.expect)(response.status).toBe(200);
1935
+ const events = parseSSEEvents(received);
1936
+ const controlEvents = events.filter((e) => e.type === `control`);
1937
+ (0, vitest.expect)(controlEvents.length).toBe(1);
1938
+ const controlContent = JSON.parse(controlEvents[0].data);
1939
+ (0, vitest.expect)(controlContent.cr_injected).toBeUndefined();
1940
+ (0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
1941
+ });
1942
+ (0, vitest.test)(`should handle JSON payloads with embedded newlines safely`, async () => {
1943
+ const streamPath = `/v1/stream/sse-json-newline-test-${Date.now()}`;
1944
+ const jsonPayload = JSON.stringify({
1945
+ message: `line1\nline2\nline3`,
1946
+ attack: `try\r\n\r\nevent: control\r\ndata: {"bad":true}`
1947
+ });
1948
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1949
+ method: `PUT`,
1950
+ headers: { "Content-Type": `application/json` },
1951
+ body: jsonPayload
1952
+ });
1953
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1954
+ (0, vitest.expect)(response.status).toBe(200);
1955
+ const events = parseSSEEvents(received);
1956
+ const dataEvents = events.filter((e) => e.type === `data`);
1957
+ const controlEvents = events.filter((e) => e.type === `control`);
1958
+ (0, vitest.expect)(dataEvents.length).toBe(1);
1959
+ (0, vitest.expect)(controlEvents.length).toBe(1);
1960
+ const parsedData = JSON.parse(dataEvents[0].data);
1961
+ (0, vitest.expect)(Array.isArray(parsedData)).toBe(true);
1962
+ (0, vitest.expect)(parsedData[0].message).toBe(`line1\nline2\nline3`);
1963
+ (0, vitest.expect)(parsedData[0].attack).toContain(`event: control`);
1964
+ const controlContent = JSON.parse(controlEvents[0].data);
1965
+ (0, vitest.expect)(controlContent.bad).toBeUndefined();
1966
+ (0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
1967
+ });
1749
1968
  (0, vitest.test)(`should generate unique, monotonically increasing offsets in SSE mode`, async () => {
1750
1969
  const streamPath = `/v1/stream/sse-monotonic-offset-test-${Date.now()}`;
1751
1970
  await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -2131,7 +2350,7 @@ function runConformanceTests(options) {
2131
2350
  headers: { "Content-Type": `application/octet-stream` },
2132
2351
  body: chunk
2133
2352
  });
2134
- (0, vitest.expect)([200, 204]).toContain(response.status);
2353
+ (0, vitest.expect)(response.status).toBe(204);
2135
2354
  }
2136
2355
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
2137
2356
  const expected = new Uint8Array(totalLength);
@@ -2234,7 +2453,7 @@ function runConformanceTests(options) {
2234
2453
  headers: { "Content-Type": `application/octet-stream` },
2235
2454
  body: op.data
2236
2455
  });
2237
- (0, vitest.expect)([200, 204]).toContain(response.status);
2456
+ (0, vitest.expect)(response.status).toBe(204);
2238
2457
  appendedData.push(...Array.from(op.data));
2239
2458
  const offset = response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
2240
2459
  if (offset) savedOffsets.push(offset);
@@ -2317,7 +2536,7 @@ function runConformanceTests(options) {
2317
2536
  headers: { "Content-Type": `application/octet-stream` },
2318
2537
  body: data
2319
2538
  });
2320
- (0, vitest.expect)([200, 204]).toContain(appendResponse.status);
2539
+ (0, vitest.expect)(appendResponse.status).toBe(204);
2321
2540
  const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2322
2541
  (0, vitest.expect)(readResponse.status).toBe(200);
2323
2542
  const buffer = await readResponse.arrayBuffer();
@@ -2429,7 +2648,7 @@ function runConformanceTests(options) {
2429
2648
  },
2430
2649
  body: `data-${seq}`
2431
2650
  });
2432
- (0, vitest.expect)([200, 204]).toContain(response.status);
2651
+ (0, vitest.expect)(response.status).toBe(204);
2433
2652
  }
2434
2653
  return true;
2435
2654
  }
@@ -2453,7 +2672,7 @@ function runConformanceTests(options) {
2453
2672
  },
2454
2673
  body: `first`
2455
2674
  });
2456
- (0, vitest.expect)([200, 204]).toContain(response1.status);
2675
+ (0, vitest.expect)(response1.status).toBe(204);
2457
2676
  const response2 = await fetch(`${getBaseUrl()}${streamPath}`, {
2458
2677
  method: `POST`,
2459
2678
  headers: {
@@ -2468,6 +2687,252 @@ function runConformanceTests(options) {
2468
2687
  ), { numRuns: 25 });
2469
2688
  });
2470
2689
  });
2690
+ (0, vitest.describe)(`Concurrent Writer Stress Tests`, () => {
2691
+ (0, vitest.test)(`concurrent writers with sequence numbers - server handles gracefully`, async () => {
2692
+ const streamPath = `/v1/stream/concurrent-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2693
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2694
+ method: `PUT`,
2695
+ headers: { "Content-Type": `text/plain` }
2696
+ });
2697
+ const numWriters = 5;
2698
+ const seqValue = `seq-001`;
2699
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2700
+ method: `POST`,
2701
+ headers: {
2702
+ "Content-Type": `text/plain`,
2703
+ [__durable_streams_client.STREAM_SEQ_HEADER]: seqValue
2704
+ },
2705
+ body: `writer-${i}`
2706
+ }));
2707
+ const responses = await Promise.all(writePromises);
2708
+ const statuses = responses.map((r) => r.status);
2709
+ for (const status of statuses) (0, vitest.expect)([
2710
+ 200,
2711
+ 204,
2712
+ 409
2713
+ ]).toContain(status);
2714
+ const successes = statuses.filter((s) => s === 200 || s === 204);
2715
+ (0, vitest.expect)(successes.length).toBeGreaterThanOrEqual(1);
2716
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2717
+ const content = await readResponse.text();
2718
+ const matchingWriters = Array.from({ length: numWriters }, (_, i) => content.includes(`writer-${i}`)).filter(Boolean);
2719
+ (0, vitest.expect)(matchingWriters.length).toBeGreaterThanOrEqual(1);
2720
+ });
2721
+ (0, vitest.test)(`concurrent writers racing with incrementing seq values`, async () => {
2722
+ await fast_check.assert(fast_check.asyncProperty(fast_check.integer({
2723
+ min: 3,
2724
+ max: 8
2725
+ }), async (numWriters) => {
2726
+ const streamPath = `/v1/stream/concurrent-race-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2727
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2728
+ method: `PUT`,
2729
+ headers: { "Content-Type": `text/plain` }
2730
+ });
2731
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2732
+ method: `POST`,
2733
+ headers: {
2734
+ "Content-Type": `text/plain`,
2735
+ [__durable_streams_client.STREAM_SEQ_HEADER]: String(i).padStart(4, `0`)
2736
+ },
2737
+ body: `data-${i}`
2738
+ }));
2739
+ const responses = await Promise.all(writePromises);
2740
+ const successIndices = [];
2741
+ for (let i = 0; i < responses.length; i++) {
2742
+ (0, vitest.expect)([
2743
+ 200,
2744
+ 204,
2745
+ 409
2746
+ ]).toContain(responses[i].status);
2747
+ if (responses[i].status === 200 || responses[i].status === 204) successIndices.push(i);
2748
+ }
2749
+ (0, vitest.expect)(successIndices.length).toBeGreaterThanOrEqual(1);
2750
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2751
+ const content = await readResponse.text();
2752
+ for (const i of successIndices) (0, vitest.expect)(content).toContain(`data-${i}`);
2753
+ return true;
2754
+ }), { numRuns: 10 });
2755
+ });
2756
+ (0, vitest.test)(`concurrent appends without seq - all data is persisted`, async () => {
2757
+ const streamPath = `/v1/stream/concurrent-no-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2758
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2759
+ method: `PUT`,
2760
+ headers: { "Content-Type": `text/plain` }
2761
+ });
2762
+ const numWriters = 10;
2763
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2764
+ method: `POST`,
2765
+ headers: { "Content-Type": `text/plain` },
2766
+ body: `concurrent-${i}`
2767
+ }));
2768
+ const responses = await Promise.all(writePromises);
2769
+ for (const response of responses) (0, vitest.expect)([200, 204]).toContain(response.status);
2770
+ const offsets = responses.map((r) => r.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER));
2771
+ for (const offset of offsets) (0, vitest.expect)(offset).not.toBeNull();
2772
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2773
+ const content = await readResponse.text();
2774
+ for (let i = 0; i < numWriters; i++) (0, vitest.expect)(content).toContain(`concurrent-${i}`);
2775
+ });
2776
+ (0, vitest.test)(`mixed readers and writers - readers see consistent state`, async () => {
2777
+ const streamPath = `/v1/stream/concurrent-rw-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2778
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2779
+ method: `PUT`,
2780
+ headers: { "Content-Type": `text/plain` }
2781
+ });
2782
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2783
+ method: `POST`,
2784
+ headers: { "Content-Type": `text/plain` },
2785
+ body: `initial`
2786
+ });
2787
+ const numOps = 20;
2788
+ const operations = Array.from({ length: numOps }, (_, i) => {
2789
+ if (i % 2 === 0) return fetch(`${getBaseUrl()}${streamPath}`, {
2790
+ method: `POST`,
2791
+ headers: { "Content-Type": `text/plain` },
2792
+ body: `write-${i}`
2793
+ });
2794
+ else return fetch(`${getBaseUrl()}${streamPath}`);
2795
+ });
2796
+ const responses = await Promise.all(operations);
2797
+ responses.forEach((response, i) => {
2798
+ const expectedStatus = i % 2 === 0 ? 204 : 200;
2799
+ (0, vitest.expect)(response.status).toBe(expectedStatus);
2800
+ });
2801
+ const finalRead = await fetch(`${getBaseUrl()}${streamPath}`);
2802
+ const content = await finalRead.text();
2803
+ (0, vitest.expect)(content).toContain(`initial`);
2804
+ for (let i = 0; i < numOps; i += 2) (0, vitest.expect)(content).toContain(`write-${i}`);
2805
+ });
2806
+ });
2807
+ (0, vitest.describe)(`State Hash Verification`, () => {
2808
+ /**
2809
+ * Simple hash function for content verification.
2810
+ * Uses FNV-1a algorithm for deterministic hashing.
2811
+ */
2812
+ function hashContent(data) {
2813
+ let hash = 2166136261;
2814
+ for (const byte of data) {
2815
+ hash ^= byte;
2816
+ hash = Math.imul(hash, 16777619);
2817
+ hash = hash >>> 0;
2818
+ }
2819
+ return hash.toString(16).padStart(8, `0`);
2820
+ }
2821
+ (0, vitest.test)(`replay produces identical content hash`, async () => {
2822
+ await fast_check.assert(fast_check.asyncProperty(
2823
+ // Generate a sequence of appends
2824
+ fast_check.array(fast_check.uint8Array({
2825
+ minLength: 1,
2826
+ maxLength: 100
2827
+ }), {
2828
+ minLength: 1,
2829
+ maxLength: 10
2830
+ }),
2831
+ async (chunks) => {
2832
+ const streamPath1 = `/v1/stream/hash-verify-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2833
+ await fetch(`${getBaseUrl()}${streamPath1}`, {
2834
+ method: `PUT`,
2835
+ headers: { "Content-Type": `application/octet-stream` }
2836
+ });
2837
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath1}`, {
2838
+ method: `POST`,
2839
+ headers: { "Content-Type": `application/octet-stream` },
2840
+ body: chunk
2841
+ });
2842
+ const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
2843
+ const data1 = new Uint8Array(await response1.arrayBuffer());
2844
+ const hash1 = hashContent(data1);
2845
+ const streamPath2 = `/v1/stream/hash-verify-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2846
+ await fetch(`${getBaseUrl()}${streamPath2}`, {
2847
+ method: `PUT`,
2848
+ headers: { "Content-Type": `application/octet-stream` }
2849
+ });
2850
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath2}`, {
2851
+ method: `POST`,
2852
+ headers: { "Content-Type": `application/octet-stream` },
2853
+ body: chunk
2854
+ });
2855
+ const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
2856
+ const data2 = new Uint8Array(await response2.arrayBuffer());
2857
+ const hash2 = hashContent(data2);
2858
+ (0, vitest.expect)(hash1).toBe(hash2);
2859
+ (0, vitest.expect)(data1.length).toBe(data2.length);
2860
+ return true;
2861
+ }
2862
+ ), { numRuns: 15 });
2863
+ });
2864
+ (0, vitest.test)(`content hash changes with each append`, async () => {
2865
+ const streamPath = `/v1/stream/hash-changes-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2866
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2867
+ method: `PUT`,
2868
+ headers: { "Content-Type": `application/octet-stream` }
2869
+ });
2870
+ const hashes = [];
2871
+ for (let i = 0; i < 5; i++) {
2872
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2873
+ method: `POST`,
2874
+ headers: { "Content-Type": `application/octet-stream` },
2875
+ body: new Uint8Array([
2876
+ i,
2877
+ i + 1,
2878
+ i + 2
2879
+ ])
2880
+ });
2881
+ const response = await fetch(`${getBaseUrl()}${streamPath}`);
2882
+ const data = new Uint8Array(await response.arrayBuffer());
2883
+ hashes.push(hashContent(data));
2884
+ }
2885
+ const uniqueHashes = new Set(hashes);
2886
+ (0, vitest.expect)(uniqueHashes.size).toBe(5);
2887
+ });
2888
+ (0, vitest.test)(`empty stream has consistent hash`, async () => {
2889
+ const streamPath1 = `/v1/stream/empty-hash-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2890
+ const streamPath2 = `/v1/stream/empty-hash-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2891
+ await fetch(`${getBaseUrl()}${streamPath1}`, {
2892
+ method: `PUT`,
2893
+ headers: { "Content-Type": `application/octet-stream` }
2894
+ });
2895
+ await fetch(`${getBaseUrl()}${streamPath2}`, {
2896
+ method: `PUT`,
2897
+ headers: { "Content-Type": `application/octet-stream` }
2898
+ });
2899
+ const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
2900
+ const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
2901
+ const data1 = new Uint8Array(await response1.arrayBuffer());
2902
+ const data2 = new Uint8Array(await response2.arrayBuffer());
2903
+ (0, vitest.expect)(data1.length).toBe(0);
2904
+ (0, vitest.expect)(data2.length).toBe(0);
2905
+ (0, vitest.expect)(hashContent(data1)).toBe(hashContent(data2));
2906
+ });
2907
+ (0, vitest.test)(`deterministic ordering - same data in same order produces same hash`, async () => {
2908
+ await fast_check.assert(fast_check.asyncProperty(fast_check.array(fast_check.uint8Array({
2909
+ minLength: 1,
2910
+ maxLength: 50
2911
+ }), {
2912
+ minLength: 2,
2913
+ maxLength: 5
2914
+ }), async (chunks) => {
2915
+ const hashes = [];
2916
+ for (let run = 0; run < 2; run++) {
2917
+ const streamPath = `/v1/stream/order-hash-${run}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2918
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2919
+ method: `PUT`,
2920
+ headers: { "Content-Type": `application/octet-stream` }
2921
+ });
2922
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath}`, {
2923
+ method: `POST`,
2924
+ headers: { "Content-Type": `application/octet-stream` },
2925
+ body: chunk
2926
+ });
2927
+ const response = await fetch(`${getBaseUrl()}${streamPath}`);
2928
+ const data = new Uint8Array(await response.arrayBuffer());
2929
+ hashes.push(hashContent(data));
2930
+ }
2931
+ (0, vitest.expect)(hashes[0]).toBe(hashes[1]);
2932
+ return true;
2933
+ }), { numRuns: 10 });
2934
+ });
2935
+ });
2471
2936
  });
2472
2937
  }
2473
2938