@durable-streams/server-conformance-tests 0.1.5 → 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.
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
- const require_src = require('./src-BJQjRfnf.cjs');
1
+ const require_src = require('./src-ChUwq33M.cjs');
2
2
 
3
3
  exports.runConformanceTests = require_src.runConformanceTests
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { runConformanceTests } from "./src-BtF2jQ-Q.js";
1
+ import { runConformanceTests } from "./src-DWkKYD4d.js";
2
2
 
3
3
  export { runConformanceTests };
@@ -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`);
@@ -303,7 +303,7 @@ function runConformanceTests(options) {
303
303
  method: `PUT`,
304
304
  headers: { "Content-Type": `text/plain` }
305
305
  });
306
- (0, vitest.expect)([200, 204]).toContain(secondResponse.status);
306
+ (0, vitest.expect)(secondResponse.status).toBe(200);
307
307
  });
308
308
  (0, vitest.test)(`should return 409 on PUT with different config`, async () => {
309
309
  const streamPath = `/v1/stream/config-conflict-test-${Date.now()}`;
@@ -328,7 +328,7 @@ function runConformanceTests(options) {
328
328
  headers: { "Content-Type": `text/plain` },
329
329
  body: `hello world`
330
330
  });
331
- (0, vitest.expect)([200, 204]).toContain(response.status);
331
+ (0, vitest.expect)(response.status).toBe(204);
332
332
  (0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
333
333
  });
334
334
  (0, vitest.test)(`should return 404 on POST to non-existent stream`, async () => {
@@ -511,7 +511,7 @@ function runConformanceTests(options) {
511
511
  },
512
512
  body: `second`
513
513
  });
514
- (0, vitest.expect)([200, 204]).toContain(response.status);
514
+ (0, vitest.expect)(response.status).toBe(204);
515
515
  });
516
516
  (0, vitest.test)(`should reject duplicate seq values`, async () => {
517
517
  const streamPath = `/v1/stream/seq-duplicate-test-${Date.now()}`;
@@ -538,6 +538,140 @@ function runConformanceTests(options) {
538
538
  (0, vitest.expect)(response.status).toBe(409);
539
539
  });
540
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
+ });
541
675
  (0, vitest.describe)(`TTL and Expiry Validation`, () => {
542
676
  (0, vitest.test)(`should reject both TTL and Expires-At (400)`, async () => {
543
677
  const streamPath = `/v1/stream/ttl-expires-conflict-test-${Date.now()}`;
@@ -608,7 +742,7 @@ function runConformanceTests(options) {
608
742
  headers: { "Content-Type": `TEXT/PLAIN` },
609
743
  body: `test`
610
744
  });
611
- (0, vitest.expect)([200, 204]).toContain(response.status);
745
+ (0, vitest.expect)(response.status).toBe(204);
612
746
  });
613
747
  (0, vitest.test)(`should allow idempotent create with different case content-type`, async () => {
614
748
  const streamPath = `/v1/stream/case-idempotent-test-${Date.now()}`;
@@ -621,7 +755,7 @@ function runConformanceTests(options) {
621
755
  method: `PUT`,
622
756
  headers: { "Content-Type": `APPLICATION/JSON` }
623
757
  });
624
- (0, vitest.expect)([200, 204]).toContain(response2.status);
758
+ (0, vitest.expect)(response2.status).toBe(200);
625
759
  });
626
760
  (0, vitest.test)(`should accept headers with different casing`, async () => {
627
761
  const streamPath = `/v1/stream/case-header-test-${Date.now()}`;
@@ -637,7 +771,7 @@ function runConformanceTests(options) {
637
771
  },
638
772
  body: `test`
639
773
  });
640
- (0, vitest.expect)([200, 204]).toContain(response.status);
774
+ (0, vitest.expect)(response.status).toBe(204);
641
775
  });
642
776
  });
643
777
  (0, vitest.describe)(`Content-Type Validation`, () => {
@@ -665,7 +799,7 @@ function runConformanceTests(options) {
665
799
  headers: { "Content-Type": `application/json` },
666
800
  body: `{"test": true}`
667
801
  });
668
- (0, vitest.expect)([200, 204]).toContain(response.status);
802
+ (0, vitest.expect)(response.status).toBe(204);
669
803
  });
670
804
  (0, vitest.test)(`should return stream content-type on GET`, async () => {
671
805
  const streamPath = `/v1/stream/content-type-get-test-${Date.now()}`;
@@ -957,7 +1091,8 @@ function runConformanceTests(options) {
957
1091
  (0, vitest.expect)(response.status).toBe(201);
958
1092
  const location = response.headers.get(`location`);
959
1093
  (0, vitest.expect)(location).toBeDefined();
960
- (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();
961
1096
  });
962
1097
  (0, vitest.test)(`should reject missing Content-Type on POST`, async () => {
963
1098
  const streamPath = `/v1/stream/missing-ct-post-test-${Date.now()}`;
@@ -1161,7 +1296,7 @@ function runConformanceTests(options) {
1161
1296
  "Stream-TTL": `3600`
1162
1297
  }
1163
1298
  });
1164
- (0, vitest.expect)([200, 204]).toContain(response2.status);
1299
+ (0, vitest.expect)(response2.status).toBe(200);
1165
1300
  });
1166
1301
  (0, vitest.test)(`should reject idempotent PUT with different TTL`, async () => {
1167
1302
  const streamPath = `/v1/stream/ttl-conflict-test-${Date.now()}`;
@@ -1265,7 +1400,7 @@ function runConformanceTests(options) {
1265
1400
  headers: { "Content-Type": `text/plain` },
1266
1401
  body: `appended data`
1267
1402
  });
1268
- (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1403
+ (0, vitest.expect)(postBefore.status).toBe(204);
1269
1404
  await sleep(1500);
1270
1405
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1271
1406
  method: `POST`,
@@ -1325,7 +1460,7 @@ function runConformanceTests(options) {
1325
1460
  headers: { "Content-Type": `text/plain` },
1326
1461
  body: `appended data`
1327
1462
  });
1328
- (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1463
+ (0, vitest.expect)(postBefore.status).toBe(204);
1329
1464
  await sleep(1500);
1330
1465
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1331
1466
  method: `POST`,
@@ -1748,6 +1883,88 @@ function runConformanceTests(options) {
1748
1883
  (0, vitest.expect)(received).toContain(`data: line2`);
1749
1884
  (0, vitest.expect)(received).toContain(`data: line3`);
1750
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
+ });
1751
1968
  (0, vitest.test)(`should generate unique, monotonically increasing offsets in SSE mode`, async () => {
1752
1969
  const streamPath = `/v1/stream/sse-monotonic-offset-test-${Date.now()}`;
1753
1970
  await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -2133,7 +2350,7 @@ function runConformanceTests(options) {
2133
2350
  headers: { "Content-Type": `application/octet-stream` },
2134
2351
  body: chunk
2135
2352
  });
2136
- (0, vitest.expect)([200, 204]).toContain(response.status);
2353
+ (0, vitest.expect)(response.status).toBe(204);
2137
2354
  }
2138
2355
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
2139
2356
  const expected = new Uint8Array(totalLength);
@@ -2236,7 +2453,7 @@ function runConformanceTests(options) {
2236
2453
  headers: { "Content-Type": `application/octet-stream` },
2237
2454
  body: op.data
2238
2455
  });
2239
- (0, vitest.expect)([200, 204]).toContain(response.status);
2456
+ (0, vitest.expect)(response.status).toBe(204);
2240
2457
  appendedData.push(...Array.from(op.data));
2241
2458
  const offset = response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
2242
2459
  if (offset) savedOffsets.push(offset);
@@ -2319,7 +2536,7 @@ function runConformanceTests(options) {
2319
2536
  headers: { "Content-Type": `application/octet-stream` },
2320
2537
  body: data
2321
2538
  });
2322
- (0, vitest.expect)([200, 204]).toContain(appendResponse.status);
2539
+ (0, vitest.expect)(appendResponse.status).toBe(204);
2323
2540
  const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2324
2541
  (0, vitest.expect)(readResponse.status).toBe(200);
2325
2542
  const buffer = await readResponse.arrayBuffer();
@@ -2431,7 +2648,7 @@ function runConformanceTests(options) {
2431
2648
  },
2432
2649
  body: `data-${seq}`
2433
2650
  });
2434
- (0, vitest.expect)([200, 204]).toContain(response.status);
2651
+ (0, vitest.expect)(response.status).toBe(204);
2435
2652
  }
2436
2653
  return true;
2437
2654
  }
@@ -2455,7 +2672,7 @@ function runConformanceTests(options) {
2455
2672
  },
2456
2673
  body: `first`
2457
2674
  });
2458
- (0, vitest.expect)([200, 204]).toContain(response1.status);
2675
+ (0, vitest.expect)(response1.status).toBe(204);
2459
2676
  const response2 = await fetch(`${getBaseUrl()}${streamPath}`, {
2460
2677
  method: `POST`,
2461
2678
  headers: {
@@ -2470,6 +2687,252 @@ function runConformanceTests(options) {
2470
2687
  ), { numRuns: 25 });
2471
2688
  });
2472
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
+ });
2473
2936
  });
2474
2937
  }
2475
2938