@durable-streams/server-conformance-tests 0.1.5 → 0.1.7
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-BJQjRfnf.cjs → src-CfXXlBaO.cjs} +1593 -18
- package/dist/{src-BtF2jQ-Q.js → src-GWuAOela.js} +1593 -18
- package/dist/test-runner.cjs +1 -1
- package/dist/test-runner.js +1 -1
- package/package.json +2 -2
- package/src/index.ts +3103 -866
|
@@ -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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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)(
|
|
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()}`;
|
|
@@ -747,6 +881,199 @@ function runConformanceTests(options) {
|
|
|
747
881
|
(0, vitest.expect)(text1).toBe(text2);
|
|
748
882
|
(0, vitest.expect)(text1).toBe(`hello world`);
|
|
749
883
|
});
|
|
884
|
+
(0, vitest.test)(`should accept offset=now as sentinel for current tail position`, async () => {
|
|
885
|
+
const streamPath = `/v1/stream/offset-now-sentinel-test-${Date.now()}`;
|
|
886
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
887
|
+
method: `PUT`,
|
|
888
|
+
headers: { "Content-Type": `text/plain` },
|
|
889
|
+
body: `historical data`
|
|
890
|
+
});
|
|
891
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
892
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
893
|
+
const text = await response.text();
|
|
894
|
+
(0, vitest.expect)(text).toBe(``);
|
|
895
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
896
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
|
|
897
|
+
});
|
|
898
|
+
(0, vitest.test)(`should return correct tail offset for offset=now`, async () => {
|
|
899
|
+
const streamPath = `/v1/stream/offset-now-tail-test-${Date.now()}`;
|
|
900
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
901
|
+
method: `PUT`,
|
|
902
|
+
headers: { "Content-Type": `text/plain` },
|
|
903
|
+
body: `initial data`
|
|
904
|
+
});
|
|
905
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
906
|
+
const tailOffset = readResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
907
|
+
(0, vitest.expect)(tailOffset).toBeDefined();
|
|
908
|
+
const nowResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
909
|
+
(0, vitest.expect)(nowResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBe(tailOffset);
|
|
910
|
+
});
|
|
911
|
+
(0, vitest.test)(`should be able to resume from offset=now result`, async () => {
|
|
912
|
+
const streamPath = `/v1/stream/offset-now-resume-test-${Date.now()}`;
|
|
913
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
914
|
+
method: `PUT`,
|
|
915
|
+
headers: { "Content-Type": `text/plain` },
|
|
916
|
+
body: `old data`
|
|
917
|
+
});
|
|
918
|
+
const nowResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
919
|
+
const nowOffset = nowResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
920
|
+
(0, vitest.expect)(nowOffset).toBeDefined();
|
|
921
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
922
|
+
method: `POST`,
|
|
923
|
+
headers: { "Content-Type": `text/plain` },
|
|
924
|
+
body: `new data`
|
|
925
|
+
});
|
|
926
|
+
const resumeResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=${nowOffset}`, { method: `GET` });
|
|
927
|
+
const resumeText = await resumeResponse.text();
|
|
928
|
+
(0, vitest.expect)(resumeText).toBe(`new data`);
|
|
929
|
+
});
|
|
930
|
+
(0, vitest.test)(`should work with offset=now on empty stream`, async () => {
|
|
931
|
+
const streamPath = `/v1/stream/offset-now-empty-test-${Date.now()}`;
|
|
932
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
933
|
+
method: `PUT`,
|
|
934
|
+
headers: { "Content-Type": `text/plain` }
|
|
935
|
+
});
|
|
936
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
937
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
938
|
+
const text = await response.text();
|
|
939
|
+
(0, vitest.expect)(text).toBe(``);
|
|
940
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
941
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
|
|
942
|
+
});
|
|
943
|
+
(0, vitest.test)(`should return empty JSON array for offset=now on JSON streams`, async () => {
|
|
944
|
+
const streamPath = `/v1/stream/offset-now-json-body-test-${Date.now()}`;
|
|
945
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
946
|
+
method: `PUT`,
|
|
947
|
+
headers: { "Content-Type": `application/json` },
|
|
948
|
+
body: `[{"event": "historical"}]`
|
|
949
|
+
});
|
|
950
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
951
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
952
|
+
(0, vitest.expect)(response.headers.get(`content-type`)).toBe(`application/json`);
|
|
953
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
954
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
|
|
955
|
+
const body = await response.text();
|
|
956
|
+
(0, vitest.expect)(body).toBe(`[]`);
|
|
957
|
+
});
|
|
958
|
+
(0, vitest.test)(`should return empty body for offset=now on non-JSON streams`, async () => {
|
|
959
|
+
const streamPath = `/v1/stream/offset-now-text-body-test-${Date.now()}`;
|
|
960
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
961
|
+
method: `PUT`,
|
|
962
|
+
headers: { "Content-Type": `text/plain` },
|
|
963
|
+
body: `historical data`
|
|
964
|
+
});
|
|
965
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
966
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
967
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
968
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
|
|
969
|
+
const body = await response.text();
|
|
970
|
+
(0, vitest.expect)(body).toBe(``);
|
|
971
|
+
});
|
|
972
|
+
(0, vitest.test)(`should support offset=now with long-poll mode (waits for data)`, async () => {
|
|
973
|
+
const streamPath = `/v1/stream/offset-now-longpoll-test-${Date.now()}`;
|
|
974
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
975
|
+
method: `PUT`,
|
|
976
|
+
headers: { "Content-Type": `text/plain` },
|
|
977
|
+
body: `existing data`
|
|
978
|
+
});
|
|
979
|
+
const readRes = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
980
|
+
const tailOffset = readRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
981
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
|
|
982
|
+
(0, vitest.expect)(response.status).toBe(204);
|
|
983
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
984
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBe(tailOffset);
|
|
985
|
+
});
|
|
986
|
+
(0, vitest.test)(`should receive data with offset=now long-poll when appended`, async () => {
|
|
987
|
+
const streamPath = `/v1/stream/offset-now-longpoll-data-test-${Date.now()}`;
|
|
988
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
989
|
+
method: `PUT`,
|
|
990
|
+
headers: { "Content-Type": `text/plain` },
|
|
991
|
+
body: `historical`
|
|
992
|
+
});
|
|
993
|
+
const longPollPromise = fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
|
|
994
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
995
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
996
|
+
method: `POST`,
|
|
997
|
+
headers: { "Content-Type": `text/plain` },
|
|
998
|
+
body: `new data`
|
|
999
|
+
});
|
|
1000
|
+
const response = await longPollPromise;
|
|
1001
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
1002
|
+
const text = await response.text();
|
|
1003
|
+
(0, vitest.expect)(text).toBe(`new data`);
|
|
1004
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
1005
|
+
});
|
|
1006
|
+
(0, vitest.test)(`should support offset=now with SSE mode`, async () => {
|
|
1007
|
+
const streamPath = `/v1/stream/offset-now-sse-test-${Date.now()}`;
|
|
1008
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1009
|
+
method: `PUT`,
|
|
1010
|
+
headers: { "Content-Type": `text/plain` },
|
|
1011
|
+
body: `existing data`
|
|
1012
|
+
});
|
|
1013
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1014
|
+
const tailOffset = readResponse.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
1015
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { untilContent: `"upToDate"` });
|
|
1016
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
1017
|
+
const controlMatch = received.match(/event: control\s*\n\s*data: ({[^}]+})/);
|
|
1018
|
+
(0, vitest.expect)(controlMatch).toBeDefined();
|
|
1019
|
+
if (controlMatch && controlMatch[1]) {
|
|
1020
|
+
const controlData = JSON.parse(controlMatch[1]);
|
|
1021
|
+
(0, vitest.expect)(controlData[`upToDate`]).toBe(true);
|
|
1022
|
+
(0, vitest.expect)(controlData[`streamNextOffset`]).toBe(tailOffset);
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
(0, vitest.test)(`should return 404 for offset=now on non-existent stream`, async () => {
|
|
1026
|
+
const streamPath = `/v1/stream/offset-now-404-test-${Date.now()}`;
|
|
1027
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
|
|
1028
|
+
(0, vitest.expect)(response.status).toBe(404);
|
|
1029
|
+
});
|
|
1030
|
+
(0, vitest.test)(`should return 404 for offset=now with long-poll on non-existent stream`, async () => {
|
|
1031
|
+
const streamPath = `/v1/stream/offset-now-longpoll-404-test-${Date.now()}`;
|
|
1032
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
|
|
1033
|
+
(0, vitest.expect)(response.status).toBe(404);
|
|
1034
|
+
});
|
|
1035
|
+
(0, vitest.test)(`should return 404 for offset=now with SSE on non-existent stream`, async () => {
|
|
1036
|
+
const streamPath = `/v1/stream/offset-now-sse-404-test-${Date.now()}`;
|
|
1037
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { method: `GET` });
|
|
1038
|
+
(0, vitest.expect)(response.status).toBe(404);
|
|
1039
|
+
});
|
|
1040
|
+
(0, vitest.test)(`should support offset=now with long-poll on empty stream`, async () => {
|
|
1041
|
+
const streamPath = `/v1/stream/offset-now-empty-longpoll-test-${Date.now()}`;
|
|
1042
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1043
|
+
method: `PUT`,
|
|
1044
|
+
headers: { "Content-Type": `text/plain` }
|
|
1045
|
+
});
|
|
1046
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
|
|
1047
|
+
(0, vitest.expect)(response.status).toBe(204);
|
|
1048
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
|
|
1049
|
+
const offset = response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
1050
|
+
(0, vitest.expect)(offset).toBeDefined();
|
|
1051
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1052
|
+
method: `POST`,
|
|
1053
|
+
headers: { "Content-Type": `text/plain` },
|
|
1054
|
+
body: `first data`
|
|
1055
|
+
});
|
|
1056
|
+
const resumeResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}`, { method: `GET` });
|
|
1057
|
+
(0, vitest.expect)(resumeResponse.status).toBe(200);
|
|
1058
|
+
const resumeText = await resumeResponse.text();
|
|
1059
|
+
(0, vitest.expect)(resumeText).toBe(`first data`);
|
|
1060
|
+
});
|
|
1061
|
+
(0, vitest.test)(`should support offset=now with SSE on empty stream`, async () => {
|
|
1062
|
+
const streamPath = `/v1/stream/offset-now-empty-sse-test-${Date.now()}`;
|
|
1063
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1064
|
+
method: `PUT`,
|
|
1065
|
+
headers: { "Content-Type": `text/plain` }
|
|
1066
|
+
});
|
|
1067
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { untilContent: `"upToDate"` });
|
|
1068
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
1069
|
+
const controlMatch = received.match(/event: control\s*\n\s*data: ({[^}]+})/);
|
|
1070
|
+
(0, vitest.expect)(controlMatch).toBeDefined();
|
|
1071
|
+
if (controlMatch && controlMatch[1]) {
|
|
1072
|
+
const controlData = JSON.parse(controlMatch[1]);
|
|
1073
|
+
(0, vitest.expect)(controlData[`upToDate`]).toBe(true);
|
|
1074
|
+
(0, vitest.expect)(controlData[`streamNextOffset`]).toBeDefined();
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
750
1077
|
(0, vitest.test)(`should reject malformed offset (contains comma)`, async () => {
|
|
751
1078
|
const streamPath = `/v1/stream/offset-comma-test-${Date.now()}`;
|
|
752
1079
|
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
@@ -957,7 +1284,8 @@ function runConformanceTests(options) {
|
|
|
957
1284
|
(0, vitest.expect)(response.status).toBe(201);
|
|
958
1285
|
const location = response.headers.get(`location`);
|
|
959
1286
|
(0, vitest.expect)(location).toBeDefined();
|
|
960
|
-
(0, vitest.expect)(location).toBe(
|
|
1287
|
+
(0, vitest.expect)(location.endsWith(streamPath)).toBe(true);
|
|
1288
|
+
(0, vitest.expect)(() => new URL(location)).not.toThrow();
|
|
961
1289
|
});
|
|
962
1290
|
(0, vitest.test)(`should reject missing Content-Type on POST`, async () => {
|
|
963
1291
|
const streamPath = `/v1/stream/missing-ct-post-test-${Date.now()}`;
|
|
@@ -1161,7 +1489,7 @@ function runConformanceTests(options) {
|
|
|
1161
1489
|
"Stream-TTL": `3600`
|
|
1162
1490
|
}
|
|
1163
1491
|
});
|
|
1164
|
-
(0, vitest.expect)(
|
|
1492
|
+
(0, vitest.expect)(response2.status).toBe(200);
|
|
1165
1493
|
});
|
|
1166
1494
|
(0, vitest.test)(`should reject idempotent PUT with different TTL`, async () => {
|
|
1167
1495
|
const streamPath = `/v1/stream/ttl-conflict-test-${Date.now()}`;
|
|
@@ -1265,7 +1593,7 @@ function runConformanceTests(options) {
|
|
|
1265
1593
|
headers: { "Content-Type": `text/plain` },
|
|
1266
1594
|
body: `appended data`
|
|
1267
1595
|
});
|
|
1268
|
-
(0, vitest.expect)(
|
|
1596
|
+
(0, vitest.expect)(postBefore.status).toBe(204);
|
|
1269
1597
|
await sleep(1500);
|
|
1270
1598
|
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1271
1599
|
method: `POST`,
|
|
@@ -1325,7 +1653,7 @@ function runConformanceTests(options) {
|
|
|
1325
1653
|
headers: { "Content-Type": `text/plain` },
|
|
1326
1654
|
body: `appended data`
|
|
1327
1655
|
});
|
|
1328
|
-
(0, vitest.expect)(
|
|
1656
|
+
(0, vitest.expect)(postBefore.status).toBe(204);
|
|
1329
1657
|
await sleep(1500);
|
|
1330
1658
|
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1331
1659
|
method: `POST`,
|
|
@@ -1748,6 +2076,88 @@ function runConformanceTests(options) {
|
|
|
1748
2076
|
(0, vitest.expect)(received).toContain(`data: line2`);
|
|
1749
2077
|
(0, vitest.expect)(received).toContain(`data: line3`);
|
|
1750
2078
|
});
|
|
2079
|
+
(0, vitest.test)(`should prevent CRLF injection in payloads - embedded event boundaries become literal data`, async () => {
|
|
2080
|
+
const streamPath = `/v1/stream/sse-crlf-injection-test-${Date.now()}`;
|
|
2081
|
+
const maliciousPayload = `safe content\r\n\r\nevent: control\r\ndata: {"injected":true}\r\n\r\nmore safe content`;
|
|
2082
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2083
|
+
method: `PUT`,
|
|
2084
|
+
headers: { "Content-Type": `text/plain` },
|
|
2085
|
+
body: maliciousPayload
|
|
2086
|
+
});
|
|
2087
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
|
|
2088
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
2089
|
+
const events = parseSSEEvents(received);
|
|
2090
|
+
const dataEvents = events.filter((e) => e.type === `data`);
|
|
2091
|
+
const controlEvents = events.filter((e) => e.type === `control`);
|
|
2092
|
+
(0, vitest.expect)(dataEvents.length).toBe(1);
|
|
2093
|
+
(0, vitest.expect)(controlEvents.length).toBe(1);
|
|
2094
|
+
const dataContent = dataEvents[0].data;
|
|
2095
|
+
(0, vitest.expect)(dataContent).toContain(`event: control`);
|
|
2096
|
+
(0, vitest.expect)(dataContent).toContain(`data: {"injected":true}`);
|
|
2097
|
+
const controlContent = JSON.parse(controlEvents[0].data);
|
|
2098
|
+
(0, vitest.expect)(controlContent.injected).toBeUndefined();
|
|
2099
|
+
(0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
|
|
2100
|
+
});
|
|
2101
|
+
(0, vitest.test)(`should prevent CRLF injection - LF-only attack vectors`, async () => {
|
|
2102
|
+
const streamPath = `/v1/stream/sse-lf-injection-test-${Date.now()}`;
|
|
2103
|
+
const maliciousPayload = `start\n\nevent: data\ndata: fake-event\n\nend`;
|
|
2104
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2105
|
+
method: `PUT`,
|
|
2106
|
+
headers: { "Content-Type": `text/plain` },
|
|
2107
|
+
body: maliciousPayload
|
|
2108
|
+
});
|
|
2109
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
|
|
2110
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
2111
|
+
const events = parseSSEEvents(received);
|
|
2112
|
+
const dataEvents = events.filter((e) => e.type === `data`);
|
|
2113
|
+
(0, vitest.expect)(dataEvents.length).toBe(1);
|
|
2114
|
+
const dataContent = dataEvents[0].data;
|
|
2115
|
+
(0, vitest.expect)(dataContent).toContain(`event: data`);
|
|
2116
|
+
(0, vitest.expect)(dataContent).toContain(`data: fake-event`);
|
|
2117
|
+
});
|
|
2118
|
+
(0, vitest.test)(`should prevent CRLF injection - carriage return only attack vectors`, async () => {
|
|
2119
|
+
const streamPath = `/v1/stream/sse-cr-injection-test-${Date.now()}`;
|
|
2120
|
+
const maliciousPayload = `start\r\revent: control\rdata: {"cr_injected":true}\r\rend`;
|
|
2121
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2122
|
+
method: `PUT`,
|
|
2123
|
+
headers: { "Content-Type": `text/plain` },
|
|
2124
|
+
body: maliciousPayload
|
|
2125
|
+
});
|
|
2126
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
|
|
2127
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
2128
|
+
const events = parseSSEEvents(received);
|
|
2129
|
+
const controlEvents = events.filter((e) => e.type === `control`);
|
|
2130
|
+
(0, vitest.expect)(controlEvents.length).toBe(1);
|
|
2131
|
+
const controlContent = JSON.parse(controlEvents[0].data);
|
|
2132
|
+
(0, vitest.expect)(controlContent.cr_injected).toBeUndefined();
|
|
2133
|
+
(0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
|
|
2134
|
+
});
|
|
2135
|
+
(0, vitest.test)(`should handle JSON payloads with embedded newlines safely`, async () => {
|
|
2136
|
+
const streamPath = `/v1/stream/sse-json-newline-test-${Date.now()}`;
|
|
2137
|
+
const jsonPayload = JSON.stringify({
|
|
2138
|
+
message: `line1\nline2\nline3`,
|
|
2139
|
+
attack: `try\r\n\r\nevent: control\r\ndata: {"bad":true}`
|
|
2140
|
+
});
|
|
2141
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2142
|
+
method: `PUT`,
|
|
2143
|
+
headers: { "Content-Type": `application/json` },
|
|
2144
|
+
body: jsonPayload
|
|
2145
|
+
});
|
|
2146
|
+
const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
|
|
2147
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
2148
|
+
const events = parseSSEEvents(received);
|
|
2149
|
+
const dataEvents = events.filter((e) => e.type === `data`);
|
|
2150
|
+
const controlEvents = events.filter((e) => e.type === `control`);
|
|
2151
|
+
(0, vitest.expect)(dataEvents.length).toBe(1);
|
|
2152
|
+
(0, vitest.expect)(controlEvents.length).toBe(1);
|
|
2153
|
+
const parsedData = JSON.parse(dataEvents[0].data);
|
|
2154
|
+
(0, vitest.expect)(Array.isArray(parsedData)).toBe(true);
|
|
2155
|
+
(0, vitest.expect)(parsedData[0].message).toBe(`line1\nline2\nline3`);
|
|
2156
|
+
(0, vitest.expect)(parsedData[0].attack).toContain(`event: control`);
|
|
2157
|
+
const controlContent = JSON.parse(controlEvents[0].data);
|
|
2158
|
+
(0, vitest.expect)(controlContent.bad).toBeUndefined();
|
|
2159
|
+
(0, vitest.expect)(controlContent.streamNextOffset).toBeDefined();
|
|
2160
|
+
});
|
|
1751
2161
|
(0, vitest.test)(`should generate unique, monotonically increasing offsets in SSE mode`, async () => {
|
|
1752
2162
|
const streamPath = `/v1/stream/sse-monotonic-offset-test-${Date.now()}`;
|
|
1753
2163
|
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
@@ -2133,7 +2543,7 @@ function runConformanceTests(options) {
|
|
|
2133
2543
|
headers: { "Content-Type": `application/octet-stream` },
|
|
2134
2544
|
body: chunk
|
|
2135
2545
|
});
|
|
2136
|
-
(0, vitest.expect)(
|
|
2546
|
+
(0, vitest.expect)(response.status).toBe(204);
|
|
2137
2547
|
}
|
|
2138
2548
|
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2139
2549
|
const expected = new Uint8Array(totalLength);
|
|
@@ -2236,7 +2646,7 @@ function runConformanceTests(options) {
|
|
|
2236
2646
|
headers: { "Content-Type": `application/octet-stream` },
|
|
2237
2647
|
body: op.data
|
|
2238
2648
|
});
|
|
2239
|
-
(0, vitest.expect)(
|
|
2649
|
+
(0, vitest.expect)(response.status).toBe(204);
|
|
2240
2650
|
appendedData.push(...Array.from(op.data));
|
|
2241
2651
|
const offset = response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
|
|
2242
2652
|
if (offset) savedOffsets.push(offset);
|
|
@@ -2303,7 +2713,7 @@ function runConformanceTests(options) {
|
|
|
2303
2713
|
return true;
|
|
2304
2714
|
}
|
|
2305
2715
|
), { numRuns: 25 });
|
|
2306
|
-
});
|
|
2716
|
+
}, 15e3);
|
|
2307
2717
|
(0, vitest.test)(`read-your-writes: data is immediately visible after append`, async () => {
|
|
2308
2718
|
await fast_check.assert(fast_check.asyncProperty(fast_check.uint8Array({
|
|
2309
2719
|
minLength: 1,
|
|
@@ -2319,7 +2729,7 @@ function runConformanceTests(options) {
|
|
|
2319
2729
|
headers: { "Content-Type": `application/octet-stream` },
|
|
2320
2730
|
body: data
|
|
2321
2731
|
});
|
|
2322
|
-
(0, vitest.expect)(
|
|
2732
|
+
(0, vitest.expect)(appendResponse.status).toBe(204);
|
|
2323
2733
|
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
2324
2734
|
(0, vitest.expect)(readResponse.status).toBe(200);
|
|
2325
2735
|
const buffer = await readResponse.arrayBuffer();
|
|
@@ -2431,7 +2841,7 @@ function runConformanceTests(options) {
|
|
|
2431
2841
|
},
|
|
2432
2842
|
body: `data-${seq}`
|
|
2433
2843
|
});
|
|
2434
|
-
(0, vitest.expect)(
|
|
2844
|
+
(0, vitest.expect)(response.status).toBe(204);
|
|
2435
2845
|
}
|
|
2436
2846
|
return true;
|
|
2437
2847
|
}
|
|
@@ -2455,7 +2865,7 @@ function runConformanceTests(options) {
|
|
|
2455
2865
|
},
|
|
2456
2866
|
body: `first`
|
|
2457
2867
|
});
|
|
2458
|
-
(0, vitest.expect)(
|
|
2868
|
+
(0, vitest.expect)(response1.status).toBe(204);
|
|
2459
2869
|
const response2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2460
2870
|
method: `POST`,
|
|
2461
2871
|
headers: {
|
|
@@ -2470,6 +2880,1171 @@ function runConformanceTests(options) {
|
|
|
2470
2880
|
), { numRuns: 25 });
|
|
2471
2881
|
});
|
|
2472
2882
|
});
|
|
2883
|
+
(0, vitest.describe)(`Concurrent Writer Stress Tests`, () => {
|
|
2884
|
+
(0, vitest.test)(`concurrent writers with sequence numbers - server handles gracefully`, async () => {
|
|
2885
|
+
const streamPath = `/v1/stream/concurrent-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2886
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2887
|
+
method: `PUT`,
|
|
2888
|
+
headers: { "Content-Type": `text/plain` }
|
|
2889
|
+
});
|
|
2890
|
+
const numWriters = 5;
|
|
2891
|
+
const seqValue = `seq-001`;
|
|
2892
|
+
const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2893
|
+
method: `POST`,
|
|
2894
|
+
headers: {
|
|
2895
|
+
"Content-Type": `text/plain`,
|
|
2896
|
+
[__durable_streams_client.STREAM_SEQ_HEADER]: seqValue
|
|
2897
|
+
},
|
|
2898
|
+
body: `writer-${i}`
|
|
2899
|
+
}));
|
|
2900
|
+
const responses = await Promise.all(writePromises);
|
|
2901
|
+
const statuses = responses.map((r) => r.status);
|
|
2902
|
+
for (const status of statuses) (0, vitest.expect)([
|
|
2903
|
+
200,
|
|
2904
|
+
204,
|
|
2905
|
+
409
|
|
2906
|
+
]).toContain(status);
|
|
2907
|
+
const successes = statuses.filter((s) => s === 200 || s === 204);
|
|
2908
|
+
(0, vitest.expect)(successes.length).toBeGreaterThanOrEqual(1);
|
|
2909
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
2910
|
+
const content = await readResponse.text();
|
|
2911
|
+
const matchingWriters = Array.from({ length: numWriters }, (_, i) => content.includes(`writer-${i}`)).filter(Boolean);
|
|
2912
|
+
(0, vitest.expect)(matchingWriters.length).toBeGreaterThanOrEqual(1);
|
|
2913
|
+
});
|
|
2914
|
+
(0, vitest.test)(`concurrent writers racing with incrementing seq values`, async () => {
|
|
2915
|
+
await fast_check.assert(fast_check.asyncProperty(fast_check.integer({
|
|
2916
|
+
min: 3,
|
|
2917
|
+
max: 8
|
|
2918
|
+
}), async (numWriters) => {
|
|
2919
|
+
const streamPath = `/v1/stream/concurrent-race-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2920
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2921
|
+
method: `PUT`,
|
|
2922
|
+
headers: { "Content-Type": `text/plain` }
|
|
2923
|
+
});
|
|
2924
|
+
const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2925
|
+
method: `POST`,
|
|
2926
|
+
headers: {
|
|
2927
|
+
"Content-Type": `text/plain`,
|
|
2928
|
+
[__durable_streams_client.STREAM_SEQ_HEADER]: String(i).padStart(4, `0`)
|
|
2929
|
+
},
|
|
2930
|
+
body: `data-${i}`
|
|
2931
|
+
}));
|
|
2932
|
+
const responses = await Promise.all(writePromises);
|
|
2933
|
+
const successIndices = [];
|
|
2934
|
+
for (let i = 0; i < responses.length; i++) {
|
|
2935
|
+
(0, vitest.expect)([
|
|
2936
|
+
200,
|
|
2937
|
+
204,
|
|
2938
|
+
409
|
|
2939
|
+
]).toContain(responses[i].status);
|
|
2940
|
+
if (responses[i].status === 200 || responses[i].status === 204) successIndices.push(i);
|
|
2941
|
+
}
|
|
2942
|
+
(0, vitest.expect)(successIndices.length).toBeGreaterThanOrEqual(1);
|
|
2943
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
2944
|
+
const content = await readResponse.text();
|
|
2945
|
+
for (const i of successIndices) (0, vitest.expect)(content).toContain(`data-${i}`);
|
|
2946
|
+
return true;
|
|
2947
|
+
}), { numRuns: 10 });
|
|
2948
|
+
});
|
|
2949
|
+
(0, vitest.test)(`concurrent appends without seq - all data is persisted`, async () => {
|
|
2950
|
+
const streamPath = `/v1/stream/concurrent-no-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2951
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2952
|
+
method: `PUT`,
|
|
2953
|
+
headers: { "Content-Type": `text/plain` }
|
|
2954
|
+
});
|
|
2955
|
+
const numWriters = 10;
|
|
2956
|
+
const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2957
|
+
method: `POST`,
|
|
2958
|
+
headers: { "Content-Type": `text/plain` },
|
|
2959
|
+
body: `concurrent-${i}`
|
|
2960
|
+
}));
|
|
2961
|
+
const responses = await Promise.all(writePromises);
|
|
2962
|
+
for (const response of responses) (0, vitest.expect)([200, 204]).toContain(response.status);
|
|
2963
|
+
const offsets = responses.map((r) => r.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER));
|
|
2964
|
+
for (const offset of offsets) (0, vitest.expect)(offset).not.toBeNull();
|
|
2965
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
2966
|
+
const content = await readResponse.text();
|
|
2967
|
+
for (let i = 0; i < numWriters; i++) (0, vitest.expect)(content).toContain(`concurrent-${i}`);
|
|
2968
|
+
});
|
|
2969
|
+
(0, vitest.test)(`mixed readers and writers - readers see consistent state`, async () => {
|
|
2970
|
+
const streamPath = `/v1/stream/concurrent-rw-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2971
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2972
|
+
method: `PUT`,
|
|
2973
|
+
headers: { "Content-Type": `text/plain` }
|
|
2974
|
+
});
|
|
2975
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2976
|
+
method: `POST`,
|
|
2977
|
+
headers: { "Content-Type": `text/plain` },
|
|
2978
|
+
body: `initial`
|
|
2979
|
+
});
|
|
2980
|
+
const numOps = 20;
|
|
2981
|
+
const operations = Array.from({ length: numOps }, (_, i) => {
|
|
2982
|
+
if (i % 2 === 0) return fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2983
|
+
method: `POST`,
|
|
2984
|
+
headers: { "Content-Type": `text/plain` },
|
|
2985
|
+
body: `write-${i}`
|
|
2986
|
+
});
|
|
2987
|
+
else return fetch(`${getBaseUrl()}${streamPath}`);
|
|
2988
|
+
});
|
|
2989
|
+
const responses = await Promise.all(operations);
|
|
2990
|
+
responses.forEach((response, i) => {
|
|
2991
|
+
if (i % 2 === 0) (0, vitest.expect)([200, 204]).toContain(response.status);
|
|
2992
|
+
else (0, vitest.expect)(response.status).toBe(200);
|
|
2993
|
+
});
|
|
2994
|
+
const finalRead = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
2995
|
+
const content = await finalRead.text();
|
|
2996
|
+
(0, vitest.expect)(content).toContain(`initial`);
|
|
2997
|
+
for (let i = 0; i < numOps; i += 2) (0, vitest.expect)(content).toContain(`write-${i}`);
|
|
2998
|
+
});
|
|
2999
|
+
});
|
|
3000
|
+
(0, vitest.describe)(`State Hash Verification`, () => {
|
|
3001
|
+
/**
|
|
3002
|
+
* Simple hash function for content verification.
|
|
3003
|
+
* Uses FNV-1a algorithm for deterministic hashing.
|
|
3004
|
+
*/
|
|
3005
|
+
function hashContent(data) {
|
|
3006
|
+
let hash = 2166136261;
|
|
3007
|
+
for (const byte of data) {
|
|
3008
|
+
hash ^= byte;
|
|
3009
|
+
hash = Math.imul(hash, 16777619);
|
|
3010
|
+
hash = hash >>> 0;
|
|
3011
|
+
}
|
|
3012
|
+
return hash.toString(16).padStart(8, `0`);
|
|
3013
|
+
}
|
|
3014
|
+
(0, vitest.test)(`replay produces identical content hash`, async () => {
|
|
3015
|
+
await fast_check.assert(fast_check.asyncProperty(
|
|
3016
|
+
// Generate a sequence of appends
|
|
3017
|
+
fast_check.array(fast_check.uint8Array({
|
|
3018
|
+
minLength: 1,
|
|
3019
|
+
maxLength: 100
|
|
3020
|
+
}), {
|
|
3021
|
+
minLength: 1,
|
|
3022
|
+
maxLength: 10
|
|
3023
|
+
}),
|
|
3024
|
+
async (chunks) => {
|
|
3025
|
+
const streamPath1 = `/v1/stream/hash-verify-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3026
|
+
await fetch(`${getBaseUrl()}${streamPath1}`, {
|
|
3027
|
+
method: `PUT`,
|
|
3028
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3029
|
+
});
|
|
3030
|
+
for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath1}`, {
|
|
3031
|
+
method: `POST`,
|
|
3032
|
+
headers: { "Content-Type": `application/octet-stream` },
|
|
3033
|
+
body: chunk
|
|
3034
|
+
});
|
|
3035
|
+
const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
|
|
3036
|
+
const data1 = new Uint8Array(await response1.arrayBuffer());
|
|
3037
|
+
const hash1 = hashContent(data1);
|
|
3038
|
+
const streamPath2 = `/v1/stream/hash-verify-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3039
|
+
await fetch(`${getBaseUrl()}${streamPath2}`, {
|
|
3040
|
+
method: `PUT`,
|
|
3041
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3042
|
+
});
|
|
3043
|
+
for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath2}`, {
|
|
3044
|
+
method: `POST`,
|
|
3045
|
+
headers: { "Content-Type": `application/octet-stream` },
|
|
3046
|
+
body: chunk
|
|
3047
|
+
});
|
|
3048
|
+
const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
|
|
3049
|
+
const data2 = new Uint8Array(await response2.arrayBuffer());
|
|
3050
|
+
const hash2 = hashContent(data2);
|
|
3051
|
+
(0, vitest.expect)(hash1).toBe(hash2);
|
|
3052
|
+
(0, vitest.expect)(data1.length).toBe(data2.length);
|
|
3053
|
+
return true;
|
|
3054
|
+
}
|
|
3055
|
+
), { numRuns: 15 });
|
|
3056
|
+
}, 15e3);
|
|
3057
|
+
(0, vitest.test)(`content hash changes with each append`, async () => {
|
|
3058
|
+
const streamPath = `/v1/stream/hash-changes-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3059
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3060
|
+
method: `PUT`,
|
|
3061
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3062
|
+
});
|
|
3063
|
+
const hashes = [];
|
|
3064
|
+
for (let i = 0; i < 5; i++) {
|
|
3065
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3066
|
+
method: `POST`,
|
|
3067
|
+
headers: { "Content-Type": `application/octet-stream` },
|
|
3068
|
+
body: new Uint8Array([
|
|
3069
|
+
i,
|
|
3070
|
+
i + 1,
|
|
3071
|
+
i + 2
|
|
3072
|
+
])
|
|
3073
|
+
});
|
|
3074
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3075
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
3076
|
+
hashes.push(hashContent(data));
|
|
3077
|
+
}
|
|
3078
|
+
const uniqueHashes = new Set(hashes);
|
|
3079
|
+
(0, vitest.expect)(uniqueHashes.size).toBe(5);
|
|
3080
|
+
});
|
|
3081
|
+
(0, vitest.test)(`empty stream has consistent hash`, async () => {
|
|
3082
|
+
const streamPath1 = `/v1/stream/empty-hash-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3083
|
+
const streamPath2 = `/v1/stream/empty-hash-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3084
|
+
await fetch(`${getBaseUrl()}${streamPath1}`, {
|
|
3085
|
+
method: `PUT`,
|
|
3086
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3087
|
+
});
|
|
3088
|
+
await fetch(`${getBaseUrl()}${streamPath2}`, {
|
|
3089
|
+
method: `PUT`,
|
|
3090
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3091
|
+
});
|
|
3092
|
+
const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
|
|
3093
|
+
const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
|
|
3094
|
+
const data1 = new Uint8Array(await response1.arrayBuffer());
|
|
3095
|
+
const data2 = new Uint8Array(await response2.arrayBuffer());
|
|
3096
|
+
(0, vitest.expect)(data1.length).toBe(0);
|
|
3097
|
+
(0, vitest.expect)(data2.length).toBe(0);
|
|
3098
|
+
(0, vitest.expect)(hashContent(data1)).toBe(hashContent(data2));
|
|
3099
|
+
});
|
|
3100
|
+
(0, vitest.test)(`deterministic ordering - same data in same order produces same hash`, async () => {
|
|
3101
|
+
await fast_check.assert(fast_check.asyncProperty(fast_check.array(fast_check.uint8Array({
|
|
3102
|
+
minLength: 1,
|
|
3103
|
+
maxLength: 50
|
|
3104
|
+
}), {
|
|
3105
|
+
minLength: 2,
|
|
3106
|
+
maxLength: 5
|
|
3107
|
+
}), async (chunks) => {
|
|
3108
|
+
const hashes = [];
|
|
3109
|
+
for (let run = 0; run < 2; run++) {
|
|
3110
|
+
const streamPath = `/v1/stream/order-hash-${run}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3111
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3112
|
+
method: `PUT`,
|
|
3113
|
+
headers: { "Content-Type": `application/octet-stream` }
|
|
3114
|
+
});
|
|
3115
|
+
for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3116
|
+
method: `POST`,
|
|
3117
|
+
headers: { "Content-Type": `application/octet-stream` },
|
|
3118
|
+
body: chunk
|
|
3119
|
+
});
|
|
3120
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3121
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
3122
|
+
hashes.push(hashContent(data));
|
|
3123
|
+
}
|
|
3124
|
+
(0, vitest.expect)(hashes[0]).toBe(hashes[1]);
|
|
3125
|
+
return true;
|
|
3126
|
+
}), { numRuns: 10 });
|
|
3127
|
+
});
|
|
3128
|
+
});
|
|
3129
|
+
});
|
|
3130
|
+
(0, vitest.describe)(`Idempotent Producer Operations`, () => {
|
|
3131
|
+
const PRODUCER_ID_HEADER = `Producer-Id`;
|
|
3132
|
+
const PRODUCER_EPOCH_HEADER = `Producer-Epoch`;
|
|
3133
|
+
const PRODUCER_SEQ_HEADER = `Producer-Seq`;
|
|
3134
|
+
const PRODUCER_EXPECTED_SEQ_HEADER = `Producer-Expected-Seq`;
|
|
3135
|
+
const PRODUCER_RECEIVED_SEQ_HEADER = `Producer-Received-Seq`;
|
|
3136
|
+
(0, vitest.test)(`should accept first append with producer headers (epoch=0, seq=0)`, async () => {
|
|
3137
|
+
const streamPath = `/v1/stream/producer-basic-${Date.now()}`;
|
|
3138
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3139
|
+
method: `PUT`,
|
|
3140
|
+
headers: { "Content-Type": `text/plain` }
|
|
3141
|
+
});
|
|
3142
|
+
const response = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3143
|
+
method: `POST`,
|
|
3144
|
+
headers: {
|
|
3145
|
+
"Content-Type": `text/plain`,
|
|
3146
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3147
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3148
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3149
|
+
},
|
|
3150
|
+
body: `hello`
|
|
3151
|
+
});
|
|
3152
|
+
(0, vitest.expect)(response.status).toBe(200);
|
|
3153
|
+
(0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeTruthy();
|
|
3154
|
+
(0, vitest.expect)(response.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`0`);
|
|
3155
|
+
});
|
|
3156
|
+
(0, vitest.test)(`should accept sequential producer sequences`, async () => {
|
|
3157
|
+
const streamPath = `/v1/stream/producer-seq-${Date.now()}`;
|
|
3158
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3159
|
+
method: `PUT`,
|
|
3160
|
+
headers: { "Content-Type": `text/plain` }
|
|
3161
|
+
});
|
|
3162
|
+
const r0 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3163
|
+
method: `POST`,
|
|
3164
|
+
headers: {
|
|
3165
|
+
"Content-Type": `text/plain`,
|
|
3166
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3167
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3168
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3169
|
+
},
|
|
3170
|
+
body: `msg0`
|
|
3171
|
+
});
|
|
3172
|
+
(0, vitest.expect)(r0.status).toBe(200);
|
|
3173
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3174
|
+
method: `POST`,
|
|
3175
|
+
headers: {
|
|
3176
|
+
"Content-Type": `text/plain`,
|
|
3177
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3178
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3179
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3180
|
+
},
|
|
3181
|
+
body: `msg1`
|
|
3182
|
+
});
|
|
3183
|
+
(0, vitest.expect)(r1.status).toBe(200);
|
|
3184
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3185
|
+
method: `POST`,
|
|
3186
|
+
headers: {
|
|
3187
|
+
"Content-Type": `text/plain`,
|
|
3188
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3189
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3190
|
+
[PRODUCER_SEQ_HEADER]: `2`
|
|
3191
|
+
},
|
|
3192
|
+
body: `msg2`
|
|
3193
|
+
});
|
|
3194
|
+
(0, vitest.expect)(r2.status).toBe(200);
|
|
3195
|
+
});
|
|
3196
|
+
(0, vitest.test)(`should return 204 for duplicate sequence (idempotent success)`, async () => {
|
|
3197
|
+
const streamPath = `/v1/stream/producer-dup-${Date.now()}`;
|
|
3198
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3199
|
+
method: `PUT`,
|
|
3200
|
+
headers: { "Content-Type": `text/plain` }
|
|
3201
|
+
});
|
|
3202
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3203
|
+
method: `POST`,
|
|
3204
|
+
headers: {
|
|
3205
|
+
"Content-Type": `text/plain`,
|
|
3206
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3207
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3208
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3209
|
+
},
|
|
3210
|
+
body: `hello`
|
|
3211
|
+
});
|
|
3212
|
+
(0, vitest.expect)(r1.status).toBe(200);
|
|
3213
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3214
|
+
method: `POST`,
|
|
3215
|
+
headers: {
|
|
3216
|
+
"Content-Type": `text/plain`,
|
|
3217
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3218
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3219
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3220
|
+
},
|
|
3221
|
+
body: `hello`
|
|
3222
|
+
});
|
|
3223
|
+
(0, vitest.expect)(r2.status).toBe(204);
|
|
3224
|
+
});
|
|
3225
|
+
(0, vitest.test)(`should accept epoch upgrade (new epoch starts at seq=0)`, async () => {
|
|
3226
|
+
const streamPath = `/v1/stream/producer-epoch-upgrade-${Date.now()}`;
|
|
3227
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3228
|
+
method: `PUT`,
|
|
3229
|
+
headers: { "Content-Type": `text/plain` }
|
|
3230
|
+
});
|
|
3231
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3232
|
+
method: `POST`,
|
|
3233
|
+
headers: {
|
|
3234
|
+
"Content-Type": `text/plain`,
|
|
3235
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3236
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3237
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3238
|
+
},
|
|
3239
|
+
body: `epoch0-msg0`
|
|
3240
|
+
});
|
|
3241
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3242
|
+
method: `POST`,
|
|
3243
|
+
headers: {
|
|
3244
|
+
"Content-Type": `text/plain`,
|
|
3245
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3246
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3247
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3248
|
+
},
|
|
3249
|
+
body: `epoch0-msg1`
|
|
3250
|
+
});
|
|
3251
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3252
|
+
method: `POST`,
|
|
3253
|
+
headers: {
|
|
3254
|
+
"Content-Type": `text/plain`,
|
|
3255
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3256
|
+
[PRODUCER_EPOCH_HEADER]: `1`,
|
|
3257
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3258
|
+
},
|
|
3259
|
+
body: `epoch1-msg0`
|
|
3260
|
+
});
|
|
3261
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3262
|
+
(0, vitest.expect)(r.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
|
|
3263
|
+
});
|
|
3264
|
+
(0, vitest.test)(`should reject stale epoch with 403 (zombie fencing)`, async () => {
|
|
3265
|
+
const streamPath = `/v1/stream/producer-stale-epoch-${Date.now()}`;
|
|
3266
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3267
|
+
method: `PUT`,
|
|
3268
|
+
headers: { "Content-Type": `text/plain` }
|
|
3269
|
+
});
|
|
3270
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3271
|
+
method: `POST`,
|
|
3272
|
+
headers: {
|
|
3273
|
+
"Content-Type": `text/plain`,
|
|
3274
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3275
|
+
[PRODUCER_EPOCH_HEADER]: `1`,
|
|
3276
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3277
|
+
},
|
|
3278
|
+
body: `msg`
|
|
3279
|
+
});
|
|
3280
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3281
|
+
method: `POST`,
|
|
3282
|
+
headers: {
|
|
3283
|
+
"Content-Type": `text/plain`,
|
|
3284
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3285
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3286
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3287
|
+
},
|
|
3288
|
+
body: `zombie`
|
|
3289
|
+
});
|
|
3290
|
+
(0, vitest.expect)(r.status).toBe(403);
|
|
3291
|
+
(0, vitest.expect)(r.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
|
|
3292
|
+
});
|
|
3293
|
+
(0, vitest.test)(`should reject sequence gap with 409`, async () => {
|
|
3294
|
+
const streamPath = `/v1/stream/producer-seq-gap-${Date.now()}`;
|
|
3295
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3296
|
+
method: `PUT`,
|
|
3297
|
+
headers: { "Content-Type": `text/plain` }
|
|
3298
|
+
});
|
|
3299
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3300
|
+
method: `POST`,
|
|
3301
|
+
headers: {
|
|
3302
|
+
"Content-Type": `text/plain`,
|
|
3303
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3304
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3305
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3306
|
+
},
|
|
3307
|
+
body: `msg0`
|
|
3308
|
+
});
|
|
3309
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3310
|
+
method: `POST`,
|
|
3311
|
+
headers: {
|
|
3312
|
+
"Content-Type": `text/plain`,
|
|
3313
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3314
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3315
|
+
[PRODUCER_SEQ_HEADER]: `2`
|
|
3316
|
+
},
|
|
3317
|
+
body: `msg2`
|
|
3318
|
+
});
|
|
3319
|
+
(0, vitest.expect)(r.status).toBe(409);
|
|
3320
|
+
(0, vitest.expect)(r.headers.get(PRODUCER_EXPECTED_SEQ_HEADER)).toBe(`1`);
|
|
3321
|
+
(0, vitest.expect)(r.headers.get(PRODUCER_RECEIVED_SEQ_HEADER)).toBe(`2`);
|
|
3322
|
+
});
|
|
3323
|
+
(0, vitest.test)(`should reject epoch increase with seq != 0`, async () => {
|
|
3324
|
+
const streamPath = `/v1/stream/producer-epoch-bad-seq-${Date.now()}`;
|
|
3325
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3326
|
+
method: `PUT`,
|
|
3327
|
+
headers: { "Content-Type": `text/plain` }
|
|
3328
|
+
});
|
|
3329
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3330
|
+
method: `POST`,
|
|
3331
|
+
headers: {
|
|
3332
|
+
"Content-Type": `text/plain`,
|
|
3333
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3334
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3335
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3336
|
+
},
|
|
3337
|
+
body: `msg`
|
|
3338
|
+
});
|
|
3339
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3340
|
+
method: `POST`,
|
|
3341
|
+
headers: {
|
|
3342
|
+
"Content-Type": `text/plain`,
|
|
3343
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3344
|
+
[PRODUCER_EPOCH_HEADER]: `1`,
|
|
3345
|
+
[PRODUCER_SEQ_HEADER]: `5`
|
|
3346
|
+
},
|
|
3347
|
+
body: `bad`
|
|
3348
|
+
});
|
|
3349
|
+
(0, vitest.expect)(r.status).toBe(400);
|
|
3350
|
+
});
|
|
3351
|
+
(0, vitest.test)(`should require all producer headers together`, async () => {
|
|
3352
|
+
const streamPath = `/v1/stream/producer-partial-headers-${Date.now()}`;
|
|
3353
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3354
|
+
method: `PUT`,
|
|
3355
|
+
headers: { "Content-Type": `text/plain` }
|
|
3356
|
+
});
|
|
3357
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3358
|
+
method: `POST`,
|
|
3359
|
+
headers: {
|
|
3360
|
+
"Content-Type": `text/plain`,
|
|
3361
|
+
[PRODUCER_ID_HEADER]: `test-producer`
|
|
3362
|
+
},
|
|
3363
|
+
body: `msg`
|
|
3364
|
+
});
|
|
3365
|
+
(0, vitest.expect)(r1.status).toBe(400);
|
|
3366
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3367
|
+
method: `POST`,
|
|
3368
|
+
headers: {
|
|
3369
|
+
"Content-Type": `text/plain`,
|
|
3370
|
+
[PRODUCER_EPOCH_HEADER]: `0`
|
|
3371
|
+
},
|
|
3372
|
+
body: `msg`
|
|
3373
|
+
});
|
|
3374
|
+
(0, vitest.expect)(r2.status).toBe(400);
|
|
3375
|
+
const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3376
|
+
method: `POST`,
|
|
3377
|
+
headers: {
|
|
3378
|
+
"Content-Type": `text/plain`,
|
|
3379
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3380
|
+
[PRODUCER_EPOCH_HEADER]: `0`
|
|
3381
|
+
},
|
|
3382
|
+
body: `msg`
|
|
3383
|
+
});
|
|
3384
|
+
(0, vitest.expect)(r3.status).toBe(400);
|
|
3385
|
+
});
|
|
3386
|
+
(0, vitest.test)(`should reject invalid integer formats in producer headers`, async () => {
|
|
3387
|
+
const streamPath = `/v1/stream/producer-invalid-format-${Date.now()}`;
|
|
3388
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3389
|
+
method: `PUT`,
|
|
3390
|
+
headers: { "Content-Type": `text/plain` }
|
|
3391
|
+
});
|
|
3392
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3393
|
+
method: `POST`,
|
|
3394
|
+
headers: {
|
|
3395
|
+
"Content-Type": `text/plain`,
|
|
3396
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3397
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3398
|
+
[PRODUCER_SEQ_HEADER]: `1abc`
|
|
3399
|
+
},
|
|
3400
|
+
body: `msg`
|
|
3401
|
+
});
|
|
3402
|
+
(0, vitest.expect)(r1.status).toBe(400);
|
|
3403
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3404
|
+
method: `POST`,
|
|
3405
|
+
headers: {
|
|
3406
|
+
"Content-Type": `text/plain`,
|
|
3407
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3408
|
+
[PRODUCER_EPOCH_HEADER]: `0xyz`,
|
|
3409
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3410
|
+
},
|
|
3411
|
+
body: `msg`
|
|
3412
|
+
});
|
|
3413
|
+
(0, vitest.expect)(r2.status).toBe(400);
|
|
3414
|
+
const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3415
|
+
method: `POST`,
|
|
3416
|
+
headers: {
|
|
3417
|
+
"Content-Type": `text/plain`,
|
|
3418
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3419
|
+
[PRODUCER_EPOCH_HEADER]: `1e3`,
|
|
3420
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3421
|
+
},
|
|
3422
|
+
body: `msg`
|
|
3423
|
+
});
|
|
3424
|
+
(0, vitest.expect)(r3.status).toBe(400);
|
|
3425
|
+
const r4 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3426
|
+
method: `POST`,
|
|
3427
|
+
headers: {
|
|
3428
|
+
"Content-Type": `text/plain`,
|
|
3429
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3430
|
+
[PRODUCER_EPOCH_HEADER]: `-1`,
|
|
3431
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3432
|
+
},
|
|
3433
|
+
body: `msg`
|
|
3434
|
+
});
|
|
3435
|
+
(0, vitest.expect)(r4.status).toBe(400);
|
|
3436
|
+
const r5 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3437
|
+
method: `POST`,
|
|
3438
|
+
headers: {
|
|
3439
|
+
"Content-Type": `text/plain`,
|
|
3440
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3441
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3442
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3443
|
+
},
|
|
3444
|
+
body: `msg`
|
|
3445
|
+
});
|
|
3446
|
+
(0, vitest.expect)(r5.status).toBe(200);
|
|
3447
|
+
});
|
|
3448
|
+
(0, vitest.test)(`multiple producers should have independent state`, async () => {
|
|
3449
|
+
const streamPath = `/v1/stream/producer-multi-${Date.now()}`;
|
|
3450
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3451
|
+
method: `PUT`,
|
|
3452
|
+
headers: { "Content-Type": `text/plain` }
|
|
3453
|
+
});
|
|
3454
|
+
const rA0 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3455
|
+
method: `POST`,
|
|
3456
|
+
headers: {
|
|
3457
|
+
"Content-Type": `text/plain`,
|
|
3458
|
+
[PRODUCER_ID_HEADER]: `producer-A`,
|
|
3459
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3460
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3461
|
+
},
|
|
3462
|
+
body: `A0`
|
|
3463
|
+
});
|
|
3464
|
+
(0, vitest.expect)(rA0.status).toBe(200);
|
|
3465
|
+
const rB0 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3466
|
+
method: `POST`,
|
|
3467
|
+
headers: {
|
|
3468
|
+
"Content-Type": `text/plain`,
|
|
3469
|
+
[PRODUCER_ID_HEADER]: `producer-B`,
|
|
3470
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3471
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3472
|
+
},
|
|
3473
|
+
body: `B0`
|
|
3474
|
+
});
|
|
3475
|
+
(0, vitest.expect)(rB0.status).toBe(200);
|
|
3476
|
+
const rA1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3477
|
+
method: `POST`,
|
|
3478
|
+
headers: {
|
|
3479
|
+
"Content-Type": `text/plain`,
|
|
3480
|
+
[PRODUCER_ID_HEADER]: `producer-A`,
|
|
3481
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3482
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3483
|
+
},
|
|
3484
|
+
body: `A1`
|
|
3485
|
+
});
|
|
3486
|
+
(0, vitest.expect)(rA1.status).toBe(200);
|
|
3487
|
+
const rB1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3488
|
+
method: `POST`,
|
|
3489
|
+
headers: {
|
|
3490
|
+
"Content-Type": `text/plain`,
|
|
3491
|
+
[PRODUCER_ID_HEADER]: `producer-B`,
|
|
3492
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3493
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3494
|
+
},
|
|
3495
|
+
body: `B1`
|
|
3496
|
+
});
|
|
3497
|
+
(0, vitest.expect)(rB1.status).toBe(200);
|
|
3498
|
+
});
|
|
3499
|
+
(0, vitest.test)(`duplicate of seq=0 should not corrupt state`, async () => {
|
|
3500
|
+
const streamPath = `/v1/stream/producer-dup-seq0-${Date.now()}`;
|
|
3501
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3502
|
+
method: `PUT`,
|
|
3503
|
+
headers: { "Content-Type": `text/plain` }
|
|
3504
|
+
});
|
|
3505
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3506
|
+
method: `POST`,
|
|
3507
|
+
headers: {
|
|
3508
|
+
"Content-Type": `text/plain`,
|
|
3509
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3510
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3511
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3512
|
+
},
|
|
3513
|
+
body: `first`
|
|
3514
|
+
});
|
|
3515
|
+
(0, vitest.expect)(r1.status).toBe(200);
|
|
3516
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3517
|
+
method: `POST`,
|
|
3518
|
+
headers: {
|
|
3519
|
+
"Content-Type": `text/plain`,
|
|
3520
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3521
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3522
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3523
|
+
},
|
|
3524
|
+
body: `first`
|
|
3525
|
+
});
|
|
3526
|
+
(0, vitest.expect)(r2.status).toBe(204);
|
|
3527
|
+
const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3528
|
+
method: `POST`,
|
|
3529
|
+
headers: {
|
|
3530
|
+
"Content-Type": `text/plain`,
|
|
3531
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3532
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3533
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3534
|
+
},
|
|
3535
|
+
body: `second`
|
|
3536
|
+
});
|
|
3537
|
+
(0, vitest.expect)(r3.status).toBe(200);
|
|
3538
|
+
});
|
|
3539
|
+
(0, vitest.test)(`duplicate response should return highest accepted seq, not request seq`, async () => {
|
|
3540
|
+
const streamPath = `/v1/stream/producer-dup-highest-seq-${Date.now()}`;
|
|
3541
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3542
|
+
method: `PUT`,
|
|
3543
|
+
headers: { "Content-Type": `text/plain` }
|
|
3544
|
+
});
|
|
3545
|
+
for (let i = 0; i < 3; i++) {
|
|
3546
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3547
|
+
method: `POST`,
|
|
3548
|
+
headers: {
|
|
3549
|
+
"Content-Type": `text/plain`,
|
|
3550
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3551
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3552
|
+
[PRODUCER_SEQ_HEADER]: `${i}`
|
|
3553
|
+
},
|
|
3554
|
+
body: `msg-${i}`
|
|
3555
|
+
});
|
|
3556
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3557
|
+
(0, vitest.expect)(r.headers.get(PRODUCER_SEQ_HEADER)).toBe(`${i}`);
|
|
3558
|
+
}
|
|
3559
|
+
const dupResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3560
|
+
method: `POST`,
|
|
3561
|
+
headers: {
|
|
3562
|
+
"Content-Type": `text/plain`,
|
|
3563
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3564
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3565
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3566
|
+
},
|
|
3567
|
+
body: `msg-1`
|
|
3568
|
+
});
|
|
3569
|
+
(0, vitest.expect)(dupResponse.status).toBe(204);
|
|
3570
|
+
(0, vitest.expect)(dupResponse.headers.get(PRODUCER_SEQ_HEADER)).toBe(`2`);
|
|
3571
|
+
});
|
|
3572
|
+
(0, vitest.test)(`split-brain fencing scenario`, async () => {
|
|
3573
|
+
const streamPath = `/v1/stream/producer-split-brain-${Date.now()}`;
|
|
3574
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3575
|
+
method: `PUT`,
|
|
3576
|
+
headers: { "Content-Type": `text/plain` }
|
|
3577
|
+
});
|
|
3578
|
+
const rA0 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3579
|
+
method: `POST`,
|
|
3580
|
+
headers: {
|
|
3581
|
+
"Content-Type": `text/plain`,
|
|
3582
|
+
[PRODUCER_ID_HEADER]: `shared-producer`,
|
|
3583
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3584
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3585
|
+
},
|
|
3586
|
+
body: `A0`
|
|
3587
|
+
});
|
|
3588
|
+
(0, vitest.expect)(rA0.status).toBe(200);
|
|
3589
|
+
const rB0 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3590
|
+
method: `POST`,
|
|
3591
|
+
headers: {
|
|
3592
|
+
"Content-Type": `text/plain`,
|
|
3593
|
+
[PRODUCER_ID_HEADER]: `shared-producer`,
|
|
3594
|
+
[PRODUCER_EPOCH_HEADER]: `1`,
|
|
3595
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3596
|
+
},
|
|
3597
|
+
body: `B0`
|
|
3598
|
+
});
|
|
3599
|
+
(0, vitest.expect)(rB0.status).toBe(200);
|
|
3600
|
+
const rA1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3601
|
+
method: `POST`,
|
|
3602
|
+
headers: {
|
|
3603
|
+
"Content-Type": `text/plain`,
|
|
3604
|
+
[PRODUCER_ID_HEADER]: `shared-producer`,
|
|
3605
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3606
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3607
|
+
},
|
|
3608
|
+
body: `A1`
|
|
3609
|
+
});
|
|
3610
|
+
(0, vitest.expect)(rA1.status).toBe(403);
|
|
3611
|
+
(0, vitest.expect)(rA1.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
|
|
3612
|
+
});
|
|
3613
|
+
(0, vitest.test)(`epoch rollback should be rejected`, async () => {
|
|
3614
|
+
const streamPath = `/v1/stream/producer-epoch-rollback-${Date.now()}`;
|
|
3615
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3616
|
+
method: `PUT`,
|
|
3617
|
+
headers: { "Content-Type": `text/plain` }
|
|
3618
|
+
});
|
|
3619
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3620
|
+
method: `POST`,
|
|
3621
|
+
headers: {
|
|
3622
|
+
"Content-Type": `text/plain`,
|
|
3623
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3624
|
+
[PRODUCER_EPOCH_HEADER]: `2`,
|
|
3625
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3626
|
+
},
|
|
3627
|
+
body: `msg`
|
|
3628
|
+
});
|
|
3629
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3630
|
+
method: `POST`,
|
|
3631
|
+
headers: {
|
|
3632
|
+
"Content-Type": `text/plain`,
|
|
3633
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3634
|
+
[PRODUCER_EPOCH_HEADER]: `1`,
|
|
3635
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3636
|
+
},
|
|
3637
|
+
body: `rollback`
|
|
3638
|
+
});
|
|
3639
|
+
(0, vitest.expect)(r.status).toBe(403);
|
|
3640
|
+
});
|
|
3641
|
+
(0, vitest.test)(`producer headers work with Stream-Seq header`, async () => {
|
|
3642
|
+
const streamPath = `/v1/stream/producer-with-stream-seq-${Date.now()}`;
|
|
3643
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3644
|
+
method: `PUT`,
|
|
3645
|
+
headers: { "Content-Type": `text/plain` }
|
|
3646
|
+
});
|
|
3647
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3648
|
+
method: `POST`,
|
|
3649
|
+
headers: {
|
|
3650
|
+
"Content-Type": `text/plain`,
|
|
3651
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3652
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3653
|
+
[PRODUCER_SEQ_HEADER]: `0`,
|
|
3654
|
+
[__durable_streams_client.STREAM_SEQ_HEADER]: `app-seq-001`
|
|
3655
|
+
},
|
|
3656
|
+
body: `msg`
|
|
3657
|
+
});
|
|
3658
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3659
|
+
});
|
|
3660
|
+
(0, vitest.test)(`producer duplicate should return 204 even with Stream-Seq header`, async () => {
|
|
3661
|
+
const streamPath = `/v1/stream/producer-dedupe-before-stream-seq-${Date.now()}`;
|
|
3662
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3663
|
+
method: `PUT`,
|
|
3664
|
+
headers: { "Content-Type": `text/plain` }
|
|
3665
|
+
});
|
|
3666
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3667
|
+
method: `POST`,
|
|
3668
|
+
headers: {
|
|
3669
|
+
"Content-Type": `text/plain`,
|
|
3670
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3671
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3672
|
+
[PRODUCER_SEQ_HEADER]: `0`,
|
|
3673
|
+
[__durable_streams_client.STREAM_SEQ_HEADER]: `app-seq-001`
|
|
3674
|
+
},
|
|
3675
|
+
body: `msg`
|
|
3676
|
+
});
|
|
3677
|
+
(0, vitest.expect)(r1.status).toBe(200);
|
|
3678
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3679
|
+
method: `POST`,
|
|
3680
|
+
headers: {
|
|
3681
|
+
"Content-Type": `text/plain`,
|
|
3682
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3683
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3684
|
+
[PRODUCER_SEQ_HEADER]: `0`,
|
|
3685
|
+
[__durable_streams_client.STREAM_SEQ_HEADER]: `app-seq-001`
|
|
3686
|
+
},
|
|
3687
|
+
body: `msg`
|
|
3688
|
+
});
|
|
3689
|
+
(0, vitest.expect)(r2.status).toBe(204);
|
|
3690
|
+
});
|
|
3691
|
+
(0, vitest.test)(`should store and read back data correctly`, async () => {
|
|
3692
|
+
const streamPath = `/v1/stream/producer-readback-${Date.now()}`;
|
|
3693
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3694
|
+
method: `PUT`,
|
|
3695
|
+
headers: { "Content-Type": `text/plain` }
|
|
3696
|
+
});
|
|
3697
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3698
|
+
method: `POST`,
|
|
3699
|
+
headers: {
|
|
3700
|
+
"Content-Type": `text/plain`,
|
|
3701
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3702
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3703
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3704
|
+
},
|
|
3705
|
+
body: `hello world`
|
|
3706
|
+
});
|
|
3707
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3708
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3709
|
+
(0, vitest.expect)(readResponse.status).toBe(200);
|
|
3710
|
+
const content = await readResponse.text();
|
|
3711
|
+
(0, vitest.expect)(content).toBe(`hello world`);
|
|
3712
|
+
});
|
|
3713
|
+
(0, vitest.test)(`should preserve order of sequential producer writes`, async () => {
|
|
3714
|
+
const streamPath = `/v1/stream/producer-order-${Date.now()}`;
|
|
3715
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3716
|
+
method: `PUT`,
|
|
3717
|
+
headers: { "Content-Type": `text/plain` }
|
|
3718
|
+
});
|
|
3719
|
+
for (let i = 0; i < 5; i++) {
|
|
3720
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3721
|
+
method: `POST`,
|
|
3722
|
+
headers: {
|
|
3723
|
+
"Content-Type": `text/plain`,
|
|
3724
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3725
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3726
|
+
[PRODUCER_SEQ_HEADER]: `${i}`
|
|
3727
|
+
},
|
|
3728
|
+
body: `msg-${i}`
|
|
3729
|
+
});
|
|
3730
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3731
|
+
}
|
|
3732
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3733
|
+
const content = await readResponse.text();
|
|
3734
|
+
(0, vitest.expect)(content).toBe(`msg-0msg-1msg-2msg-3msg-4`);
|
|
3735
|
+
});
|
|
3736
|
+
(0, vitest.test)(`duplicate should not corrupt or duplicate data`, async () => {
|
|
3737
|
+
const streamPath = `/v1/stream/producer-dup-integrity-${Date.now()}`;
|
|
3738
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3739
|
+
method: `PUT`,
|
|
3740
|
+
headers: { "Content-Type": `text/plain` }
|
|
3741
|
+
});
|
|
3742
|
+
const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3743
|
+
method: `POST`,
|
|
3744
|
+
headers: {
|
|
3745
|
+
"Content-Type": `text/plain`,
|
|
3746
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3747
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3748
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3749
|
+
},
|
|
3750
|
+
body: `first`
|
|
3751
|
+
});
|
|
3752
|
+
(0, vitest.expect)(r1.status).toBe(200);
|
|
3753
|
+
const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3754
|
+
method: `POST`,
|
|
3755
|
+
headers: {
|
|
3756
|
+
"Content-Type": `text/plain`,
|
|
3757
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3758
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3759
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3760
|
+
},
|
|
3761
|
+
body: `first`
|
|
3762
|
+
});
|
|
3763
|
+
(0, vitest.expect)(r2.status).toBe(204);
|
|
3764
|
+
const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3765
|
+
method: `POST`,
|
|
3766
|
+
headers: {
|
|
3767
|
+
"Content-Type": `text/plain`,
|
|
3768
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3769
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3770
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3771
|
+
},
|
|
3772
|
+
body: `second`
|
|
3773
|
+
});
|
|
3774
|
+
(0, vitest.expect)(r3.status).toBe(200);
|
|
3775
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3776
|
+
const content = await readResponse.text();
|
|
3777
|
+
(0, vitest.expect)(content).toBe(`firstsecond`);
|
|
3778
|
+
});
|
|
3779
|
+
(0, vitest.test)(`multiple producers should interleave correctly`, async () => {
|
|
3780
|
+
const streamPath = `/v1/stream/producer-interleave-${Date.now()}`;
|
|
3781
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3782
|
+
method: `PUT`,
|
|
3783
|
+
headers: { "Content-Type": `text/plain` }
|
|
3784
|
+
});
|
|
3785
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3786
|
+
method: `POST`,
|
|
3787
|
+
headers: {
|
|
3788
|
+
"Content-Type": `text/plain`,
|
|
3789
|
+
[PRODUCER_ID_HEADER]: `producer-A`,
|
|
3790
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3791
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3792
|
+
},
|
|
3793
|
+
body: `A0`
|
|
3794
|
+
});
|
|
3795
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3796
|
+
method: `POST`,
|
|
3797
|
+
headers: {
|
|
3798
|
+
"Content-Type": `text/plain`,
|
|
3799
|
+
[PRODUCER_ID_HEADER]: `producer-B`,
|
|
3800
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3801
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3802
|
+
},
|
|
3803
|
+
body: `B0`
|
|
3804
|
+
});
|
|
3805
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3806
|
+
method: `POST`,
|
|
3807
|
+
headers: {
|
|
3808
|
+
"Content-Type": `text/plain`,
|
|
3809
|
+
[PRODUCER_ID_HEADER]: `producer-A`,
|
|
3810
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3811
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3812
|
+
},
|
|
3813
|
+
body: `A1`
|
|
3814
|
+
});
|
|
3815
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3816
|
+
method: `POST`,
|
|
3817
|
+
headers: {
|
|
3818
|
+
"Content-Type": `text/plain`,
|
|
3819
|
+
[PRODUCER_ID_HEADER]: `producer-B`,
|
|
3820
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3821
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3822
|
+
},
|
|
3823
|
+
body: `B1`
|
|
3824
|
+
});
|
|
3825
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3826
|
+
const content = await readResponse.text();
|
|
3827
|
+
(0, vitest.expect)(content).toBe(`A0B0A1B1`);
|
|
3828
|
+
});
|
|
3829
|
+
(0, vitest.test)(`should store and read back JSON object correctly`, async () => {
|
|
3830
|
+
const streamPath = `/v1/stream/producer-json-obj-${Date.now()}`;
|
|
3831
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3832
|
+
method: `PUT`,
|
|
3833
|
+
headers: { "Content-Type": `application/json` }
|
|
3834
|
+
});
|
|
3835
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3836
|
+
method: `POST`,
|
|
3837
|
+
headers: {
|
|
3838
|
+
"Content-Type": `application/json`,
|
|
3839
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3840
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3841
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3842
|
+
},
|
|
3843
|
+
body: JSON.stringify({
|
|
3844
|
+
event: `test`,
|
|
3845
|
+
value: 42
|
|
3846
|
+
})
|
|
3847
|
+
});
|
|
3848
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3849
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3850
|
+
const data = await readResponse.json();
|
|
3851
|
+
(0, vitest.expect)(data).toEqual([{
|
|
3852
|
+
event: `test`,
|
|
3853
|
+
value: 42
|
|
3854
|
+
}]);
|
|
3855
|
+
});
|
|
3856
|
+
(0, vitest.test)(`should preserve order of JSON appends with producer`, async () => {
|
|
3857
|
+
const streamPath = `/v1/stream/producer-json-order-${Date.now()}`;
|
|
3858
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3859
|
+
method: `PUT`,
|
|
3860
|
+
headers: { "Content-Type": `application/json` }
|
|
3861
|
+
});
|
|
3862
|
+
for (let i = 0; i < 5; i++) {
|
|
3863
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3864
|
+
method: `POST`,
|
|
3865
|
+
headers: {
|
|
3866
|
+
"Content-Type": `application/json`,
|
|
3867
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3868
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3869
|
+
[PRODUCER_SEQ_HEADER]: `${i}`
|
|
3870
|
+
},
|
|
3871
|
+
body: JSON.stringify({
|
|
3872
|
+
seq: i,
|
|
3873
|
+
data: `msg-${i}`
|
|
3874
|
+
})
|
|
3875
|
+
});
|
|
3876
|
+
(0, vitest.expect)(r.status).toBe(200);
|
|
3877
|
+
}
|
|
3878
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3879
|
+
const data = await readResponse.json();
|
|
3880
|
+
(0, vitest.expect)(data).toEqual([
|
|
3881
|
+
{
|
|
3882
|
+
seq: 0,
|
|
3883
|
+
data: `msg-0`
|
|
3884
|
+
},
|
|
3885
|
+
{
|
|
3886
|
+
seq: 1,
|
|
3887
|
+
data: `msg-1`
|
|
3888
|
+
},
|
|
3889
|
+
{
|
|
3890
|
+
seq: 2,
|
|
3891
|
+
data: `msg-2`
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
seq: 3,
|
|
3895
|
+
data: `msg-3`
|
|
3896
|
+
},
|
|
3897
|
+
{
|
|
3898
|
+
seq: 4,
|
|
3899
|
+
data: `msg-4`
|
|
3900
|
+
}
|
|
3901
|
+
]);
|
|
3902
|
+
});
|
|
3903
|
+
(0, vitest.test)(`JSON duplicate should not corrupt data`, async () => {
|
|
3904
|
+
const streamPath = `/v1/stream/producer-json-dup-${Date.now()}`;
|
|
3905
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3906
|
+
method: `PUT`,
|
|
3907
|
+
headers: { "Content-Type": `application/json` }
|
|
3908
|
+
});
|
|
3909
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3910
|
+
method: `POST`,
|
|
3911
|
+
headers: {
|
|
3912
|
+
"Content-Type": `application/json`,
|
|
3913
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3914
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3915
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3916
|
+
},
|
|
3917
|
+
body: JSON.stringify({ id: 1 })
|
|
3918
|
+
});
|
|
3919
|
+
const dup = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3920
|
+
method: `POST`,
|
|
3921
|
+
headers: {
|
|
3922
|
+
"Content-Type": `application/json`,
|
|
3923
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3924
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3925
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3926
|
+
},
|
|
3927
|
+
body: JSON.stringify({ id: 1 })
|
|
3928
|
+
});
|
|
3929
|
+
(0, vitest.expect)(dup.status).toBe(204);
|
|
3930
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3931
|
+
method: `POST`,
|
|
3932
|
+
headers: {
|
|
3933
|
+
"Content-Type": `application/json`,
|
|
3934
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3935
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3936
|
+
[PRODUCER_SEQ_HEADER]: `1`
|
|
3937
|
+
},
|
|
3938
|
+
body: JSON.stringify({ id: 2 })
|
|
3939
|
+
});
|
|
3940
|
+
const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
|
|
3941
|
+
const data = await readResponse.json();
|
|
3942
|
+
(0, vitest.expect)(data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
3943
|
+
});
|
|
3944
|
+
(0, vitest.test)(`should reject invalid JSON with producer headers`, async () => {
|
|
3945
|
+
const streamPath = `/v1/stream/producer-json-invalid-${Date.now()}`;
|
|
3946
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3947
|
+
method: `PUT`,
|
|
3948
|
+
headers: { "Content-Type": `application/json` }
|
|
3949
|
+
});
|
|
3950
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3951
|
+
method: `POST`,
|
|
3952
|
+
headers: {
|
|
3953
|
+
"Content-Type": `application/json`,
|
|
3954
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3955
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3956
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3957
|
+
},
|
|
3958
|
+
body: `{ invalid json }`
|
|
3959
|
+
});
|
|
3960
|
+
(0, vitest.expect)(r.status).toBe(400);
|
|
3961
|
+
});
|
|
3962
|
+
(0, vitest.test)(`should reject empty JSON array with producer headers`, async () => {
|
|
3963
|
+
const streamPath = `/v1/stream/producer-json-empty-${Date.now()}`;
|
|
3964
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3965
|
+
method: `PUT`,
|
|
3966
|
+
headers: { "Content-Type": `application/json` }
|
|
3967
|
+
});
|
|
3968
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3969
|
+
method: `POST`,
|
|
3970
|
+
headers: {
|
|
3971
|
+
"Content-Type": `application/json`,
|
|
3972
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3973
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3974
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3975
|
+
},
|
|
3976
|
+
body: `[]`
|
|
3977
|
+
});
|
|
3978
|
+
(0, vitest.expect)(r.status).toBe(400);
|
|
3979
|
+
});
|
|
3980
|
+
(0, vitest.test)(`should return 404 for non-existent stream`, async () => {
|
|
3981
|
+
const streamPath = `/v1/stream/producer-404-${Date.now()}`;
|
|
3982
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3983
|
+
method: `POST`,
|
|
3984
|
+
headers: {
|
|
3985
|
+
"Content-Type": `text/plain`,
|
|
3986
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
3987
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
3988
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
3989
|
+
},
|
|
3990
|
+
body: `data`
|
|
3991
|
+
});
|
|
3992
|
+
(0, vitest.expect)(r.status).toBe(404);
|
|
3993
|
+
});
|
|
3994
|
+
(0, vitest.test)(`should return 409 for content-type mismatch`, async () => {
|
|
3995
|
+
const streamPath = `/v1/stream/producer-ct-mismatch-${Date.now()}`;
|
|
3996
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
3997
|
+
method: `PUT`,
|
|
3998
|
+
headers: { "Content-Type": `text/plain` }
|
|
3999
|
+
});
|
|
4000
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
4001
|
+
method: `POST`,
|
|
4002
|
+
headers: {
|
|
4003
|
+
"Content-Type": `application/json`,
|
|
4004
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
4005
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
4006
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
4007
|
+
},
|
|
4008
|
+
body: JSON.stringify({ data: `test` })
|
|
4009
|
+
});
|
|
4010
|
+
(0, vitest.expect)(r.status).toBe(409);
|
|
4011
|
+
});
|
|
4012
|
+
(0, vitest.test)(`should return 400 for empty body`, async () => {
|
|
4013
|
+
const streamPath = `/v1/stream/producer-empty-body-${Date.now()}`;
|
|
4014
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
4015
|
+
method: `PUT`,
|
|
4016
|
+
headers: { "Content-Type": `text/plain` }
|
|
4017
|
+
});
|
|
4018
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
4019
|
+
method: `POST`,
|
|
4020
|
+
headers: {
|
|
4021
|
+
"Content-Type": `text/plain`,
|
|
4022
|
+
[PRODUCER_ID_HEADER]: `test-producer`,
|
|
4023
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
4024
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
4025
|
+
},
|
|
4026
|
+
body: ``
|
|
4027
|
+
});
|
|
4028
|
+
(0, vitest.expect)(r.status).toBe(400);
|
|
4029
|
+
});
|
|
4030
|
+
(0, vitest.test)(`should reject empty Producer-Id`, async () => {
|
|
4031
|
+
const streamPath = `/v1/stream/producer-empty-id-${Date.now()}`;
|
|
4032
|
+
await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
4033
|
+
method: `PUT`,
|
|
4034
|
+
headers: { "Content-Type": `text/plain` }
|
|
4035
|
+
});
|
|
4036
|
+
const r = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
4037
|
+
method: `POST`,
|
|
4038
|
+
headers: {
|
|
4039
|
+
"Content-Type": `text/plain`,
|
|
4040
|
+
[PRODUCER_ID_HEADER]: ``,
|
|
4041
|
+
[PRODUCER_EPOCH_HEADER]: `0`,
|
|
4042
|
+
[PRODUCER_SEQ_HEADER]: `0`
|
|
4043
|
+
},
|
|
4044
|
+
body: `data`
|
|
4045
|
+
});
|
|
4046
|
+
(0, vitest.expect)(r.status).toBe(400);
|
|
4047
|
+
});
|
|
2473
4048
|
});
|
|
2474
4049
|
}
|
|
2475
4050
|
|