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

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.
@@ -879,6 +879,199 @@ function runConformanceTests(options) {
879
879
  expect(text1).toBe(text2);
880
880
  expect(text1).toBe(`hello world`);
881
881
  });
882
+ test(`should accept offset=now as sentinel for current tail position`, async () => {
883
+ const streamPath = `/v1/stream/offset-now-sentinel-test-${Date.now()}`;
884
+ await fetch(`${getBaseUrl()}${streamPath}`, {
885
+ method: `PUT`,
886
+ headers: { "Content-Type": `text/plain` },
887
+ body: `historical data`
888
+ });
889
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
890
+ expect(response.status).toBe(200);
891
+ const text = await response.text();
892
+ expect(text).toBe(``);
893
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
894
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeDefined();
895
+ });
896
+ test(`should return correct tail offset for offset=now`, async () => {
897
+ const streamPath = `/v1/stream/offset-now-tail-test-${Date.now()}`;
898
+ await fetch(`${getBaseUrl()}${streamPath}`, {
899
+ method: `PUT`,
900
+ headers: { "Content-Type": `text/plain` },
901
+ body: `initial data`
902
+ });
903
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
904
+ const tailOffset = readResponse.headers.get(STREAM_OFFSET_HEADER);
905
+ expect(tailOffset).toBeDefined();
906
+ const nowResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
907
+ expect(nowResponse.headers.get(STREAM_OFFSET_HEADER)).toBe(tailOffset);
908
+ });
909
+ test(`should be able to resume from offset=now result`, async () => {
910
+ const streamPath = `/v1/stream/offset-now-resume-test-${Date.now()}`;
911
+ await fetch(`${getBaseUrl()}${streamPath}`, {
912
+ method: `PUT`,
913
+ headers: { "Content-Type": `text/plain` },
914
+ body: `old data`
915
+ });
916
+ const nowResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
917
+ const nowOffset = nowResponse.headers.get(STREAM_OFFSET_HEADER);
918
+ expect(nowOffset).toBeDefined();
919
+ await fetch(`${getBaseUrl()}${streamPath}`, {
920
+ method: `POST`,
921
+ headers: { "Content-Type": `text/plain` },
922
+ body: `new data`
923
+ });
924
+ const resumeResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=${nowOffset}`, { method: `GET` });
925
+ const resumeText = await resumeResponse.text();
926
+ expect(resumeText).toBe(`new data`);
927
+ });
928
+ test(`should work with offset=now on empty stream`, async () => {
929
+ const streamPath = `/v1/stream/offset-now-empty-test-${Date.now()}`;
930
+ await fetch(`${getBaseUrl()}${streamPath}`, {
931
+ method: `PUT`,
932
+ headers: { "Content-Type": `text/plain` }
933
+ });
934
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
935
+ expect(response.status).toBe(200);
936
+ const text = await response.text();
937
+ expect(text).toBe(``);
938
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
939
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeDefined();
940
+ });
941
+ test(`should return empty JSON array for offset=now on JSON streams`, async () => {
942
+ const streamPath = `/v1/stream/offset-now-json-body-test-${Date.now()}`;
943
+ await fetch(`${getBaseUrl()}${streamPath}`, {
944
+ method: `PUT`,
945
+ headers: { "Content-Type": `application/json` },
946
+ body: `[{"event": "historical"}]`
947
+ });
948
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
949
+ expect(response.status).toBe(200);
950
+ expect(response.headers.get(`content-type`)).toBe(`application/json`);
951
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
952
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeDefined();
953
+ const body = await response.text();
954
+ expect(body).toBe(`[]`);
955
+ });
956
+ test(`should return empty body for offset=now on non-JSON streams`, async () => {
957
+ const streamPath = `/v1/stream/offset-now-text-body-test-${Date.now()}`;
958
+ await fetch(`${getBaseUrl()}${streamPath}`, {
959
+ method: `PUT`,
960
+ headers: { "Content-Type": `text/plain` },
961
+ body: `historical data`
962
+ });
963
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
964
+ expect(response.status).toBe(200);
965
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
966
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeDefined();
967
+ const body = await response.text();
968
+ expect(body).toBe(``);
969
+ });
970
+ test(`should support offset=now with long-poll mode (waits for data)`, async () => {
971
+ const streamPath = `/v1/stream/offset-now-longpoll-test-${Date.now()}`;
972
+ await fetch(`${getBaseUrl()}${streamPath}`, {
973
+ method: `PUT`,
974
+ headers: { "Content-Type": `text/plain` },
975
+ body: `existing data`
976
+ });
977
+ const readRes = await fetch(`${getBaseUrl()}${streamPath}`);
978
+ const tailOffset = readRes.headers.get(STREAM_OFFSET_HEADER);
979
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
980
+ expect(response.status).toBe(204);
981
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
982
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBe(tailOffset);
983
+ });
984
+ test(`should receive data with offset=now long-poll when appended`, async () => {
985
+ const streamPath = `/v1/stream/offset-now-longpoll-data-test-${Date.now()}`;
986
+ await fetch(`${getBaseUrl()}${streamPath}`, {
987
+ method: `PUT`,
988
+ headers: { "Content-Type": `text/plain` },
989
+ body: `historical`
990
+ });
991
+ const longPollPromise = fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
992
+ await new Promise((r) => setTimeout(r, 100));
993
+ await fetch(`${getBaseUrl()}${streamPath}`, {
994
+ method: `POST`,
995
+ headers: { "Content-Type": `text/plain` },
996
+ body: `new data`
997
+ });
998
+ const response = await longPollPromise;
999
+ expect(response.status).toBe(200);
1000
+ const text = await response.text();
1001
+ expect(text).toBe(`new data`);
1002
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
1003
+ });
1004
+ test(`should support offset=now with SSE mode`, async () => {
1005
+ const streamPath = `/v1/stream/offset-now-sse-test-${Date.now()}`;
1006
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1007
+ method: `PUT`,
1008
+ headers: { "Content-Type": `text/plain` },
1009
+ body: `existing data`
1010
+ });
1011
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1012
+ const tailOffset = readResponse.headers.get(STREAM_OFFSET_HEADER);
1013
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { untilContent: `"upToDate"` });
1014
+ expect(response.status).toBe(200);
1015
+ const controlMatch = received.match(/event: control\s*\n\s*data: ({[^}]+})/);
1016
+ expect(controlMatch).toBeDefined();
1017
+ if (controlMatch && controlMatch[1]) {
1018
+ const controlData = JSON.parse(controlMatch[1]);
1019
+ expect(controlData[`upToDate`]).toBe(true);
1020
+ expect(controlData[`streamNextOffset`]).toBe(tailOffset);
1021
+ }
1022
+ });
1023
+ test(`should return 404 for offset=now on non-existent stream`, async () => {
1024
+ const streamPath = `/v1/stream/offset-now-404-test-${Date.now()}`;
1025
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now`, { method: `GET` });
1026
+ expect(response.status).toBe(404);
1027
+ });
1028
+ test(`should return 404 for offset=now with long-poll on non-existent stream`, async () => {
1029
+ const streamPath = `/v1/stream/offset-now-longpoll-404-test-${Date.now()}`;
1030
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
1031
+ expect(response.status).toBe(404);
1032
+ });
1033
+ test(`should return 404 for offset=now with SSE on non-existent stream`, async () => {
1034
+ const streamPath = `/v1/stream/offset-now-sse-404-test-${Date.now()}`;
1035
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { method: `GET` });
1036
+ expect(response.status).toBe(404);
1037
+ });
1038
+ test(`should support offset=now with long-poll on empty stream`, async () => {
1039
+ const streamPath = `/v1/stream/offset-now-empty-longpoll-test-${Date.now()}`;
1040
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1041
+ method: `PUT`,
1042
+ headers: { "Content-Type": `text/plain` }
1043
+ });
1044
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
1045
+ expect(response.status).toBe(204);
1046
+ expect(response.headers.get(STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
1047
+ const offset = response.headers.get(STREAM_OFFSET_HEADER);
1048
+ expect(offset).toBeDefined();
1049
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1050
+ method: `POST`,
1051
+ headers: { "Content-Type": `text/plain` },
1052
+ body: `first data`
1053
+ });
1054
+ const resumeResponse = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}`, { method: `GET` });
1055
+ expect(resumeResponse.status).toBe(200);
1056
+ const resumeText = await resumeResponse.text();
1057
+ expect(resumeText).toBe(`first data`);
1058
+ });
1059
+ test(`should support offset=now with SSE on empty stream`, async () => {
1060
+ const streamPath = `/v1/stream/offset-now-empty-sse-test-${Date.now()}`;
1061
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1062
+ method: `PUT`,
1063
+ headers: { "Content-Type": `text/plain` }
1064
+ });
1065
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=now&live=sse`, { untilContent: `"upToDate"` });
1066
+ expect(response.status).toBe(200);
1067
+ const controlMatch = received.match(/event: control\s*\n\s*data: ({[^}]+})/);
1068
+ expect(controlMatch).toBeDefined();
1069
+ if (controlMatch && controlMatch[1]) {
1070
+ const controlData = JSON.parse(controlMatch[1]);
1071
+ expect(controlData[`upToDate`]).toBe(true);
1072
+ expect(controlData[`streamNextOffset`]).toBeDefined();
1073
+ }
1074
+ });
882
1075
  test(`should reject malformed offset (contains comma)`, async () => {
883
1076
  const streamPath = `/v1/stream/offset-comma-test-${Date.now()}`;
884
1077
  await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -2518,7 +2711,7 @@ function runConformanceTests(options) {
2518
2711
  return true;
2519
2712
  }
2520
2713
  ), { numRuns: 25 });
2521
- });
2714
+ }, 15e3);
2522
2715
  test(`read-your-writes: data is immediately visible after append`, async () => {
2523
2716
  await fc.assert(fc.asyncProperty(fc.uint8Array({
2524
2717
  minLength: 1,
@@ -2793,8 +2986,8 @@ function runConformanceTests(options) {
2793
2986
  });
2794
2987
  const responses = await Promise.all(operations);
2795
2988
  responses.forEach((response, i) => {
2796
- const expectedStatus = i % 2 === 0 ? 204 : 200;
2797
- expect(response.status).toBe(expectedStatus);
2989
+ if (i % 2 === 0) expect([200, 204]).toContain(response.status);
2990
+ else expect(response.status).toBe(200);
2798
2991
  });
2799
2992
  const finalRead = await fetch(`${getBaseUrl()}${streamPath}`);
2800
2993
  const content = await finalRead.text();
@@ -2858,7 +3051,7 @@ function runConformanceTests(options) {
2858
3051
  return true;
2859
3052
  }
2860
3053
  ), { numRuns: 15 });
2861
- });
3054
+ }, 15e3);
2862
3055
  test(`content hash changes with each append`, async () => {
2863
3056
  const streamPath = `/v1/stream/hash-changes-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2864
3057
  await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -2932,6 +3125,925 @@ function runConformanceTests(options) {
2932
3125
  });
2933
3126
  });
2934
3127
  });
3128
+ describe(`Idempotent Producer Operations`, () => {
3129
+ const PRODUCER_ID_HEADER = `Producer-Id`;
3130
+ const PRODUCER_EPOCH_HEADER = `Producer-Epoch`;
3131
+ const PRODUCER_SEQ_HEADER = `Producer-Seq`;
3132
+ const PRODUCER_EXPECTED_SEQ_HEADER = `Producer-Expected-Seq`;
3133
+ const PRODUCER_RECEIVED_SEQ_HEADER = `Producer-Received-Seq`;
3134
+ test(`should accept first append with producer headers (epoch=0, seq=0)`, async () => {
3135
+ const streamPath = `/v1/stream/producer-basic-${Date.now()}`;
3136
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3137
+ method: `PUT`,
3138
+ headers: { "Content-Type": `text/plain` }
3139
+ });
3140
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, {
3141
+ method: `POST`,
3142
+ headers: {
3143
+ "Content-Type": `text/plain`,
3144
+ [PRODUCER_ID_HEADER]: `test-producer`,
3145
+ [PRODUCER_EPOCH_HEADER]: `0`,
3146
+ [PRODUCER_SEQ_HEADER]: `0`
3147
+ },
3148
+ body: `hello`
3149
+ });
3150
+ expect(response.status).toBe(200);
3151
+ expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeTruthy();
3152
+ expect(response.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`0`);
3153
+ });
3154
+ test(`should accept sequential producer sequences`, async () => {
3155
+ const streamPath = `/v1/stream/producer-seq-${Date.now()}`;
3156
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3157
+ method: `PUT`,
3158
+ headers: { "Content-Type": `text/plain` }
3159
+ });
3160
+ const r0 = await fetch(`${getBaseUrl()}${streamPath}`, {
3161
+ method: `POST`,
3162
+ headers: {
3163
+ "Content-Type": `text/plain`,
3164
+ [PRODUCER_ID_HEADER]: `test-producer`,
3165
+ [PRODUCER_EPOCH_HEADER]: `0`,
3166
+ [PRODUCER_SEQ_HEADER]: `0`
3167
+ },
3168
+ body: `msg0`
3169
+ });
3170
+ expect(r0.status).toBe(200);
3171
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3172
+ method: `POST`,
3173
+ headers: {
3174
+ "Content-Type": `text/plain`,
3175
+ [PRODUCER_ID_HEADER]: `test-producer`,
3176
+ [PRODUCER_EPOCH_HEADER]: `0`,
3177
+ [PRODUCER_SEQ_HEADER]: `1`
3178
+ },
3179
+ body: `msg1`
3180
+ });
3181
+ expect(r1.status).toBe(200);
3182
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3183
+ method: `POST`,
3184
+ headers: {
3185
+ "Content-Type": `text/plain`,
3186
+ [PRODUCER_ID_HEADER]: `test-producer`,
3187
+ [PRODUCER_EPOCH_HEADER]: `0`,
3188
+ [PRODUCER_SEQ_HEADER]: `2`
3189
+ },
3190
+ body: `msg2`
3191
+ });
3192
+ expect(r2.status).toBe(200);
3193
+ });
3194
+ test(`should return 204 for duplicate sequence (idempotent success)`, async () => {
3195
+ const streamPath = `/v1/stream/producer-dup-${Date.now()}`;
3196
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3197
+ method: `PUT`,
3198
+ headers: { "Content-Type": `text/plain` }
3199
+ });
3200
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3201
+ method: `POST`,
3202
+ headers: {
3203
+ "Content-Type": `text/plain`,
3204
+ [PRODUCER_ID_HEADER]: `test-producer`,
3205
+ [PRODUCER_EPOCH_HEADER]: `0`,
3206
+ [PRODUCER_SEQ_HEADER]: `0`
3207
+ },
3208
+ body: `hello`
3209
+ });
3210
+ expect(r1.status).toBe(200);
3211
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3212
+ method: `POST`,
3213
+ headers: {
3214
+ "Content-Type": `text/plain`,
3215
+ [PRODUCER_ID_HEADER]: `test-producer`,
3216
+ [PRODUCER_EPOCH_HEADER]: `0`,
3217
+ [PRODUCER_SEQ_HEADER]: `0`
3218
+ },
3219
+ body: `hello`
3220
+ });
3221
+ expect(r2.status).toBe(204);
3222
+ });
3223
+ test(`should accept epoch upgrade (new epoch starts at seq=0)`, async () => {
3224
+ const streamPath = `/v1/stream/producer-epoch-upgrade-${Date.now()}`;
3225
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3226
+ method: `PUT`,
3227
+ headers: { "Content-Type": `text/plain` }
3228
+ });
3229
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3230
+ method: `POST`,
3231
+ headers: {
3232
+ "Content-Type": `text/plain`,
3233
+ [PRODUCER_ID_HEADER]: `test-producer`,
3234
+ [PRODUCER_EPOCH_HEADER]: `0`,
3235
+ [PRODUCER_SEQ_HEADER]: `0`
3236
+ },
3237
+ body: `epoch0-msg0`
3238
+ });
3239
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3240
+ method: `POST`,
3241
+ headers: {
3242
+ "Content-Type": `text/plain`,
3243
+ [PRODUCER_ID_HEADER]: `test-producer`,
3244
+ [PRODUCER_EPOCH_HEADER]: `0`,
3245
+ [PRODUCER_SEQ_HEADER]: `1`
3246
+ },
3247
+ body: `epoch0-msg1`
3248
+ });
3249
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3250
+ method: `POST`,
3251
+ headers: {
3252
+ "Content-Type": `text/plain`,
3253
+ [PRODUCER_ID_HEADER]: `test-producer`,
3254
+ [PRODUCER_EPOCH_HEADER]: `1`,
3255
+ [PRODUCER_SEQ_HEADER]: `0`
3256
+ },
3257
+ body: `epoch1-msg0`
3258
+ });
3259
+ expect(r.status).toBe(200);
3260
+ expect(r.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
3261
+ });
3262
+ test(`should reject stale epoch with 403 (zombie fencing)`, async () => {
3263
+ const streamPath = `/v1/stream/producer-stale-epoch-${Date.now()}`;
3264
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3265
+ method: `PUT`,
3266
+ headers: { "Content-Type": `text/plain` }
3267
+ });
3268
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3269
+ method: `POST`,
3270
+ headers: {
3271
+ "Content-Type": `text/plain`,
3272
+ [PRODUCER_ID_HEADER]: `test-producer`,
3273
+ [PRODUCER_EPOCH_HEADER]: `1`,
3274
+ [PRODUCER_SEQ_HEADER]: `0`
3275
+ },
3276
+ body: `msg`
3277
+ });
3278
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3279
+ method: `POST`,
3280
+ headers: {
3281
+ "Content-Type": `text/plain`,
3282
+ [PRODUCER_ID_HEADER]: `test-producer`,
3283
+ [PRODUCER_EPOCH_HEADER]: `0`,
3284
+ [PRODUCER_SEQ_HEADER]: `0`
3285
+ },
3286
+ body: `zombie`
3287
+ });
3288
+ expect(r.status).toBe(403);
3289
+ expect(r.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
3290
+ });
3291
+ test(`should reject sequence gap with 409`, async () => {
3292
+ const streamPath = `/v1/stream/producer-seq-gap-${Date.now()}`;
3293
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3294
+ method: `PUT`,
3295
+ headers: { "Content-Type": `text/plain` }
3296
+ });
3297
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3298
+ method: `POST`,
3299
+ headers: {
3300
+ "Content-Type": `text/plain`,
3301
+ [PRODUCER_ID_HEADER]: `test-producer`,
3302
+ [PRODUCER_EPOCH_HEADER]: `0`,
3303
+ [PRODUCER_SEQ_HEADER]: `0`
3304
+ },
3305
+ body: `msg0`
3306
+ });
3307
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3308
+ method: `POST`,
3309
+ headers: {
3310
+ "Content-Type": `text/plain`,
3311
+ [PRODUCER_ID_HEADER]: `test-producer`,
3312
+ [PRODUCER_EPOCH_HEADER]: `0`,
3313
+ [PRODUCER_SEQ_HEADER]: `2`
3314
+ },
3315
+ body: `msg2`
3316
+ });
3317
+ expect(r.status).toBe(409);
3318
+ expect(r.headers.get(PRODUCER_EXPECTED_SEQ_HEADER)).toBe(`1`);
3319
+ expect(r.headers.get(PRODUCER_RECEIVED_SEQ_HEADER)).toBe(`2`);
3320
+ });
3321
+ test(`should reject epoch increase with seq != 0`, async () => {
3322
+ const streamPath = `/v1/stream/producer-epoch-bad-seq-${Date.now()}`;
3323
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3324
+ method: `PUT`,
3325
+ headers: { "Content-Type": `text/plain` }
3326
+ });
3327
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3328
+ method: `POST`,
3329
+ headers: {
3330
+ "Content-Type": `text/plain`,
3331
+ [PRODUCER_ID_HEADER]: `test-producer`,
3332
+ [PRODUCER_EPOCH_HEADER]: `0`,
3333
+ [PRODUCER_SEQ_HEADER]: `0`
3334
+ },
3335
+ body: `msg`
3336
+ });
3337
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3338
+ method: `POST`,
3339
+ headers: {
3340
+ "Content-Type": `text/plain`,
3341
+ [PRODUCER_ID_HEADER]: `test-producer`,
3342
+ [PRODUCER_EPOCH_HEADER]: `1`,
3343
+ [PRODUCER_SEQ_HEADER]: `5`
3344
+ },
3345
+ body: `bad`
3346
+ });
3347
+ expect(r.status).toBe(400);
3348
+ });
3349
+ test(`should require all producer headers together`, async () => {
3350
+ const streamPath = `/v1/stream/producer-partial-headers-${Date.now()}`;
3351
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3352
+ method: `PUT`,
3353
+ headers: { "Content-Type": `text/plain` }
3354
+ });
3355
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3356
+ method: `POST`,
3357
+ headers: {
3358
+ "Content-Type": `text/plain`,
3359
+ [PRODUCER_ID_HEADER]: `test-producer`
3360
+ },
3361
+ body: `msg`
3362
+ });
3363
+ expect(r1.status).toBe(400);
3364
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3365
+ method: `POST`,
3366
+ headers: {
3367
+ "Content-Type": `text/plain`,
3368
+ [PRODUCER_EPOCH_HEADER]: `0`
3369
+ },
3370
+ body: `msg`
3371
+ });
3372
+ expect(r2.status).toBe(400);
3373
+ const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
3374
+ method: `POST`,
3375
+ headers: {
3376
+ "Content-Type": `text/plain`,
3377
+ [PRODUCER_ID_HEADER]: `test-producer`,
3378
+ [PRODUCER_EPOCH_HEADER]: `0`
3379
+ },
3380
+ body: `msg`
3381
+ });
3382
+ expect(r3.status).toBe(400);
3383
+ });
3384
+ test(`should reject invalid integer formats in producer headers`, async () => {
3385
+ const streamPath = `/v1/stream/producer-invalid-format-${Date.now()}`;
3386
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3387
+ method: `PUT`,
3388
+ headers: { "Content-Type": `text/plain` }
3389
+ });
3390
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3391
+ method: `POST`,
3392
+ headers: {
3393
+ "Content-Type": `text/plain`,
3394
+ [PRODUCER_ID_HEADER]: `test-producer`,
3395
+ [PRODUCER_EPOCH_HEADER]: `0`,
3396
+ [PRODUCER_SEQ_HEADER]: `1abc`
3397
+ },
3398
+ body: `msg`
3399
+ });
3400
+ expect(r1.status).toBe(400);
3401
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3402
+ method: `POST`,
3403
+ headers: {
3404
+ "Content-Type": `text/plain`,
3405
+ [PRODUCER_ID_HEADER]: `test-producer`,
3406
+ [PRODUCER_EPOCH_HEADER]: `0xyz`,
3407
+ [PRODUCER_SEQ_HEADER]: `0`
3408
+ },
3409
+ body: `msg`
3410
+ });
3411
+ expect(r2.status).toBe(400);
3412
+ const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
3413
+ method: `POST`,
3414
+ headers: {
3415
+ "Content-Type": `text/plain`,
3416
+ [PRODUCER_ID_HEADER]: `test-producer`,
3417
+ [PRODUCER_EPOCH_HEADER]: `1e3`,
3418
+ [PRODUCER_SEQ_HEADER]: `0`
3419
+ },
3420
+ body: `msg`
3421
+ });
3422
+ expect(r3.status).toBe(400);
3423
+ const r4 = await fetch(`${getBaseUrl()}${streamPath}`, {
3424
+ method: `POST`,
3425
+ headers: {
3426
+ "Content-Type": `text/plain`,
3427
+ [PRODUCER_ID_HEADER]: `test-producer`,
3428
+ [PRODUCER_EPOCH_HEADER]: `-1`,
3429
+ [PRODUCER_SEQ_HEADER]: `0`
3430
+ },
3431
+ body: `msg`
3432
+ });
3433
+ expect(r4.status).toBe(400);
3434
+ const r5 = await fetch(`${getBaseUrl()}${streamPath}`, {
3435
+ method: `POST`,
3436
+ headers: {
3437
+ "Content-Type": `text/plain`,
3438
+ [PRODUCER_ID_HEADER]: `test-producer`,
3439
+ [PRODUCER_EPOCH_HEADER]: `0`,
3440
+ [PRODUCER_SEQ_HEADER]: `0`
3441
+ },
3442
+ body: `msg`
3443
+ });
3444
+ expect(r5.status).toBe(200);
3445
+ });
3446
+ test(`multiple producers should have independent state`, async () => {
3447
+ const streamPath = `/v1/stream/producer-multi-${Date.now()}`;
3448
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3449
+ method: `PUT`,
3450
+ headers: { "Content-Type": `text/plain` }
3451
+ });
3452
+ const rA0 = await fetch(`${getBaseUrl()}${streamPath}`, {
3453
+ method: `POST`,
3454
+ headers: {
3455
+ "Content-Type": `text/plain`,
3456
+ [PRODUCER_ID_HEADER]: `producer-A`,
3457
+ [PRODUCER_EPOCH_HEADER]: `0`,
3458
+ [PRODUCER_SEQ_HEADER]: `0`
3459
+ },
3460
+ body: `A0`
3461
+ });
3462
+ expect(rA0.status).toBe(200);
3463
+ const rB0 = await fetch(`${getBaseUrl()}${streamPath}`, {
3464
+ method: `POST`,
3465
+ headers: {
3466
+ "Content-Type": `text/plain`,
3467
+ [PRODUCER_ID_HEADER]: `producer-B`,
3468
+ [PRODUCER_EPOCH_HEADER]: `0`,
3469
+ [PRODUCER_SEQ_HEADER]: `0`
3470
+ },
3471
+ body: `B0`
3472
+ });
3473
+ expect(rB0.status).toBe(200);
3474
+ const rA1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3475
+ method: `POST`,
3476
+ headers: {
3477
+ "Content-Type": `text/plain`,
3478
+ [PRODUCER_ID_HEADER]: `producer-A`,
3479
+ [PRODUCER_EPOCH_HEADER]: `0`,
3480
+ [PRODUCER_SEQ_HEADER]: `1`
3481
+ },
3482
+ body: `A1`
3483
+ });
3484
+ expect(rA1.status).toBe(200);
3485
+ const rB1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3486
+ method: `POST`,
3487
+ headers: {
3488
+ "Content-Type": `text/plain`,
3489
+ [PRODUCER_ID_HEADER]: `producer-B`,
3490
+ [PRODUCER_EPOCH_HEADER]: `0`,
3491
+ [PRODUCER_SEQ_HEADER]: `1`
3492
+ },
3493
+ body: `B1`
3494
+ });
3495
+ expect(rB1.status).toBe(200);
3496
+ });
3497
+ test(`duplicate of seq=0 should not corrupt state`, async () => {
3498
+ const streamPath = `/v1/stream/producer-dup-seq0-${Date.now()}`;
3499
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3500
+ method: `PUT`,
3501
+ headers: { "Content-Type": `text/plain` }
3502
+ });
3503
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3504
+ method: `POST`,
3505
+ headers: {
3506
+ "Content-Type": `text/plain`,
3507
+ [PRODUCER_ID_HEADER]: `test-producer`,
3508
+ [PRODUCER_EPOCH_HEADER]: `0`,
3509
+ [PRODUCER_SEQ_HEADER]: `0`
3510
+ },
3511
+ body: `first`
3512
+ });
3513
+ expect(r1.status).toBe(200);
3514
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3515
+ method: `POST`,
3516
+ headers: {
3517
+ "Content-Type": `text/plain`,
3518
+ [PRODUCER_ID_HEADER]: `test-producer`,
3519
+ [PRODUCER_EPOCH_HEADER]: `0`,
3520
+ [PRODUCER_SEQ_HEADER]: `0`
3521
+ },
3522
+ body: `first`
3523
+ });
3524
+ expect(r2.status).toBe(204);
3525
+ const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
3526
+ method: `POST`,
3527
+ headers: {
3528
+ "Content-Type": `text/plain`,
3529
+ [PRODUCER_ID_HEADER]: `test-producer`,
3530
+ [PRODUCER_EPOCH_HEADER]: `0`,
3531
+ [PRODUCER_SEQ_HEADER]: `1`
3532
+ },
3533
+ body: `second`
3534
+ });
3535
+ expect(r3.status).toBe(200);
3536
+ });
3537
+ test(`duplicate response should return highest accepted seq, not request seq`, async () => {
3538
+ const streamPath = `/v1/stream/producer-dup-highest-seq-${Date.now()}`;
3539
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3540
+ method: `PUT`,
3541
+ headers: { "Content-Type": `text/plain` }
3542
+ });
3543
+ for (let i = 0; i < 3; i++) {
3544
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3545
+ method: `POST`,
3546
+ headers: {
3547
+ "Content-Type": `text/plain`,
3548
+ [PRODUCER_ID_HEADER]: `test-producer`,
3549
+ [PRODUCER_EPOCH_HEADER]: `0`,
3550
+ [PRODUCER_SEQ_HEADER]: `${i}`
3551
+ },
3552
+ body: `msg-${i}`
3553
+ });
3554
+ expect(r.status).toBe(200);
3555
+ expect(r.headers.get(PRODUCER_SEQ_HEADER)).toBe(`${i}`);
3556
+ }
3557
+ const dupResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
3558
+ method: `POST`,
3559
+ headers: {
3560
+ "Content-Type": `text/plain`,
3561
+ [PRODUCER_ID_HEADER]: `test-producer`,
3562
+ [PRODUCER_EPOCH_HEADER]: `0`,
3563
+ [PRODUCER_SEQ_HEADER]: `1`
3564
+ },
3565
+ body: `msg-1`
3566
+ });
3567
+ expect(dupResponse.status).toBe(204);
3568
+ expect(dupResponse.headers.get(PRODUCER_SEQ_HEADER)).toBe(`2`);
3569
+ });
3570
+ test(`split-brain fencing scenario`, async () => {
3571
+ const streamPath = `/v1/stream/producer-split-brain-${Date.now()}`;
3572
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3573
+ method: `PUT`,
3574
+ headers: { "Content-Type": `text/plain` }
3575
+ });
3576
+ const rA0 = await fetch(`${getBaseUrl()}${streamPath}`, {
3577
+ method: `POST`,
3578
+ headers: {
3579
+ "Content-Type": `text/plain`,
3580
+ [PRODUCER_ID_HEADER]: `shared-producer`,
3581
+ [PRODUCER_EPOCH_HEADER]: `0`,
3582
+ [PRODUCER_SEQ_HEADER]: `0`
3583
+ },
3584
+ body: `A0`
3585
+ });
3586
+ expect(rA0.status).toBe(200);
3587
+ const rB0 = await fetch(`${getBaseUrl()}${streamPath}`, {
3588
+ method: `POST`,
3589
+ headers: {
3590
+ "Content-Type": `text/plain`,
3591
+ [PRODUCER_ID_HEADER]: `shared-producer`,
3592
+ [PRODUCER_EPOCH_HEADER]: `1`,
3593
+ [PRODUCER_SEQ_HEADER]: `0`
3594
+ },
3595
+ body: `B0`
3596
+ });
3597
+ expect(rB0.status).toBe(200);
3598
+ const rA1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3599
+ method: `POST`,
3600
+ headers: {
3601
+ "Content-Type": `text/plain`,
3602
+ [PRODUCER_ID_HEADER]: `shared-producer`,
3603
+ [PRODUCER_EPOCH_HEADER]: `0`,
3604
+ [PRODUCER_SEQ_HEADER]: `1`
3605
+ },
3606
+ body: `A1`
3607
+ });
3608
+ expect(rA1.status).toBe(403);
3609
+ expect(rA1.headers.get(PRODUCER_EPOCH_HEADER)).toBe(`1`);
3610
+ });
3611
+ test(`epoch rollback should be rejected`, async () => {
3612
+ const streamPath = `/v1/stream/producer-epoch-rollback-${Date.now()}`;
3613
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3614
+ method: `PUT`,
3615
+ headers: { "Content-Type": `text/plain` }
3616
+ });
3617
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3618
+ method: `POST`,
3619
+ headers: {
3620
+ "Content-Type": `text/plain`,
3621
+ [PRODUCER_ID_HEADER]: `test-producer`,
3622
+ [PRODUCER_EPOCH_HEADER]: `2`,
3623
+ [PRODUCER_SEQ_HEADER]: `0`
3624
+ },
3625
+ body: `msg`
3626
+ });
3627
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3628
+ method: `POST`,
3629
+ headers: {
3630
+ "Content-Type": `text/plain`,
3631
+ [PRODUCER_ID_HEADER]: `test-producer`,
3632
+ [PRODUCER_EPOCH_HEADER]: `1`,
3633
+ [PRODUCER_SEQ_HEADER]: `0`
3634
+ },
3635
+ body: `rollback`
3636
+ });
3637
+ expect(r.status).toBe(403);
3638
+ });
3639
+ test(`producer headers work with Stream-Seq header`, async () => {
3640
+ const streamPath = `/v1/stream/producer-with-stream-seq-${Date.now()}`;
3641
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3642
+ method: `PUT`,
3643
+ headers: { "Content-Type": `text/plain` }
3644
+ });
3645
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3646
+ method: `POST`,
3647
+ headers: {
3648
+ "Content-Type": `text/plain`,
3649
+ [PRODUCER_ID_HEADER]: `test-producer`,
3650
+ [PRODUCER_EPOCH_HEADER]: `0`,
3651
+ [PRODUCER_SEQ_HEADER]: `0`,
3652
+ [STREAM_SEQ_HEADER]: `app-seq-001`
3653
+ },
3654
+ body: `msg`
3655
+ });
3656
+ expect(r.status).toBe(200);
3657
+ });
3658
+ test(`producer duplicate should return 204 even with Stream-Seq header`, async () => {
3659
+ const streamPath = `/v1/stream/producer-dedupe-before-stream-seq-${Date.now()}`;
3660
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3661
+ method: `PUT`,
3662
+ headers: { "Content-Type": `text/plain` }
3663
+ });
3664
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3665
+ method: `POST`,
3666
+ headers: {
3667
+ "Content-Type": `text/plain`,
3668
+ [PRODUCER_ID_HEADER]: `test-producer`,
3669
+ [PRODUCER_EPOCH_HEADER]: `0`,
3670
+ [PRODUCER_SEQ_HEADER]: `0`,
3671
+ [STREAM_SEQ_HEADER]: `app-seq-001`
3672
+ },
3673
+ body: `msg`
3674
+ });
3675
+ expect(r1.status).toBe(200);
3676
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3677
+ method: `POST`,
3678
+ headers: {
3679
+ "Content-Type": `text/plain`,
3680
+ [PRODUCER_ID_HEADER]: `test-producer`,
3681
+ [PRODUCER_EPOCH_HEADER]: `0`,
3682
+ [PRODUCER_SEQ_HEADER]: `0`,
3683
+ [STREAM_SEQ_HEADER]: `app-seq-001`
3684
+ },
3685
+ body: `msg`
3686
+ });
3687
+ expect(r2.status).toBe(204);
3688
+ });
3689
+ test(`should store and read back data correctly`, async () => {
3690
+ const streamPath = `/v1/stream/producer-readback-${Date.now()}`;
3691
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3692
+ method: `PUT`,
3693
+ headers: { "Content-Type": `text/plain` }
3694
+ });
3695
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3696
+ method: `POST`,
3697
+ headers: {
3698
+ "Content-Type": `text/plain`,
3699
+ [PRODUCER_ID_HEADER]: `test-producer`,
3700
+ [PRODUCER_EPOCH_HEADER]: `0`,
3701
+ [PRODUCER_SEQ_HEADER]: `0`
3702
+ },
3703
+ body: `hello world`
3704
+ });
3705
+ expect(r.status).toBe(200);
3706
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3707
+ expect(readResponse.status).toBe(200);
3708
+ const content = await readResponse.text();
3709
+ expect(content).toBe(`hello world`);
3710
+ });
3711
+ test(`should preserve order of sequential producer writes`, async () => {
3712
+ const streamPath = `/v1/stream/producer-order-${Date.now()}`;
3713
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3714
+ method: `PUT`,
3715
+ headers: { "Content-Type": `text/plain` }
3716
+ });
3717
+ for (let i = 0; i < 5; i++) {
3718
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3719
+ method: `POST`,
3720
+ headers: {
3721
+ "Content-Type": `text/plain`,
3722
+ [PRODUCER_ID_HEADER]: `test-producer`,
3723
+ [PRODUCER_EPOCH_HEADER]: `0`,
3724
+ [PRODUCER_SEQ_HEADER]: `${i}`
3725
+ },
3726
+ body: `msg-${i}`
3727
+ });
3728
+ expect(r.status).toBe(200);
3729
+ }
3730
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3731
+ const content = await readResponse.text();
3732
+ expect(content).toBe(`msg-0msg-1msg-2msg-3msg-4`);
3733
+ });
3734
+ test(`duplicate should not corrupt or duplicate data`, async () => {
3735
+ const streamPath = `/v1/stream/producer-dup-integrity-${Date.now()}`;
3736
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3737
+ method: `PUT`,
3738
+ headers: { "Content-Type": `text/plain` }
3739
+ });
3740
+ const r1 = await fetch(`${getBaseUrl()}${streamPath}`, {
3741
+ method: `POST`,
3742
+ headers: {
3743
+ "Content-Type": `text/plain`,
3744
+ [PRODUCER_ID_HEADER]: `test-producer`,
3745
+ [PRODUCER_EPOCH_HEADER]: `0`,
3746
+ [PRODUCER_SEQ_HEADER]: `0`
3747
+ },
3748
+ body: `first`
3749
+ });
3750
+ expect(r1.status).toBe(200);
3751
+ const r2 = await fetch(`${getBaseUrl()}${streamPath}`, {
3752
+ method: `POST`,
3753
+ headers: {
3754
+ "Content-Type": `text/plain`,
3755
+ [PRODUCER_ID_HEADER]: `test-producer`,
3756
+ [PRODUCER_EPOCH_HEADER]: `0`,
3757
+ [PRODUCER_SEQ_HEADER]: `0`
3758
+ },
3759
+ body: `first`
3760
+ });
3761
+ expect(r2.status).toBe(204);
3762
+ const r3 = await fetch(`${getBaseUrl()}${streamPath}`, {
3763
+ method: `POST`,
3764
+ headers: {
3765
+ "Content-Type": `text/plain`,
3766
+ [PRODUCER_ID_HEADER]: `test-producer`,
3767
+ [PRODUCER_EPOCH_HEADER]: `0`,
3768
+ [PRODUCER_SEQ_HEADER]: `1`
3769
+ },
3770
+ body: `second`
3771
+ });
3772
+ expect(r3.status).toBe(200);
3773
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3774
+ const content = await readResponse.text();
3775
+ expect(content).toBe(`firstsecond`);
3776
+ });
3777
+ test(`multiple producers should interleave correctly`, async () => {
3778
+ const streamPath = `/v1/stream/producer-interleave-${Date.now()}`;
3779
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3780
+ method: `PUT`,
3781
+ headers: { "Content-Type": `text/plain` }
3782
+ });
3783
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3784
+ method: `POST`,
3785
+ headers: {
3786
+ "Content-Type": `text/plain`,
3787
+ [PRODUCER_ID_HEADER]: `producer-A`,
3788
+ [PRODUCER_EPOCH_HEADER]: `0`,
3789
+ [PRODUCER_SEQ_HEADER]: `0`
3790
+ },
3791
+ body: `A0`
3792
+ });
3793
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3794
+ method: `POST`,
3795
+ headers: {
3796
+ "Content-Type": `text/plain`,
3797
+ [PRODUCER_ID_HEADER]: `producer-B`,
3798
+ [PRODUCER_EPOCH_HEADER]: `0`,
3799
+ [PRODUCER_SEQ_HEADER]: `0`
3800
+ },
3801
+ body: `B0`
3802
+ });
3803
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3804
+ method: `POST`,
3805
+ headers: {
3806
+ "Content-Type": `text/plain`,
3807
+ [PRODUCER_ID_HEADER]: `producer-A`,
3808
+ [PRODUCER_EPOCH_HEADER]: `0`,
3809
+ [PRODUCER_SEQ_HEADER]: `1`
3810
+ },
3811
+ body: `A1`
3812
+ });
3813
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3814
+ method: `POST`,
3815
+ headers: {
3816
+ "Content-Type": `text/plain`,
3817
+ [PRODUCER_ID_HEADER]: `producer-B`,
3818
+ [PRODUCER_EPOCH_HEADER]: `0`,
3819
+ [PRODUCER_SEQ_HEADER]: `1`
3820
+ },
3821
+ body: `B1`
3822
+ });
3823
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3824
+ const content = await readResponse.text();
3825
+ expect(content).toBe(`A0B0A1B1`);
3826
+ });
3827
+ test(`should store and read back JSON object correctly`, async () => {
3828
+ const streamPath = `/v1/stream/producer-json-obj-${Date.now()}`;
3829
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3830
+ method: `PUT`,
3831
+ headers: { "Content-Type": `application/json` }
3832
+ });
3833
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3834
+ method: `POST`,
3835
+ headers: {
3836
+ "Content-Type": `application/json`,
3837
+ [PRODUCER_ID_HEADER]: `test-producer`,
3838
+ [PRODUCER_EPOCH_HEADER]: `0`,
3839
+ [PRODUCER_SEQ_HEADER]: `0`
3840
+ },
3841
+ body: JSON.stringify({
3842
+ event: `test`,
3843
+ value: 42
3844
+ })
3845
+ });
3846
+ expect(r.status).toBe(200);
3847
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3848
+ const data = await readResponse.json();
3849
+ expect(data).toEqual([{
3850
+ event: `test`,
3851
+ value: 42
3852
+ }]);
3853
+ });
3854
+ test(`should preserve order of JSON appends with producer`, async () => {
3855
+ const streamPath = `/v1/stream/producer-json-order-${Date.now()}`;
3856
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3857
+ method: `PUT`,
3858
+ headers: { "Content-Type": `application/json` }
3859
+ });
3860
+ for (let i = 0; i < 5; i++) {
3861
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3862
+ method: `POST`,
3863
+ headers: {
3864
+ "Content-Type": `application/json`,
3865
+ [PRODUCER_ID_HEADER]: `test-producer`,
3866
+ [PRODUCER_EPOCH_HEADER]: `0`,
3867
+ [PRODUCER_SEQ_HEADER]: `${i}`
3868
+ },
3869
+ body: JSON.stringify({
3870
+ seq: i,
3871
+ data: `msg-${i}`
3872
+ })
3873
+ });
3874
+ expect(r.status).toBe(200);
3875
+ }
3876
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3877
+ const data = await readResponse.json();
3878
+ expect(data).toEqual([
3879
+ {
3880
+ seq: 0,
3881
+ data: `msg-0`
3882
+ },
3883
+ {
3884
+ seq: 1,
3885
+ data: `msg-1`
3886
+ },
3887
+ {
3888
+ seq: 2,
3889
+ data: `msg-2`
3890
+ },
3891
+ {
3892
+ seq: 3,
3893
+ data: `msg-3`
3894
+ },
3895
+ {
3896
+ seq: 4,
3897
+ data: `msg-4`
3898
+ }
3899
+ ]);
3900
+ });
3901
+ test(`JSON duplicate should not corrupt data`, async () => {
3902
+ const streamPath = `/v1/stream/producer-json-dup-${Date.now()}`;
3903
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3904
+ method: `PUT`,
3905
+ headers: { "Content-Type": `application/json` }
3906
+ });
3907
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3908
+ method: `POST`,
3909
+ headers: {
3910
+ "Content-Type": `application/json`,
3911
+ [PRODUCER_ID_HEADER]: `test-producer`,
3912
+ [PRODUCER_EPOCH_HEADER]: `0`,
3913
+ [PRODUCER_SEQ_HEADER]: `0`
3914
+ },
3915
+ body: JSON.stringify({ id: 1 })
3916
+ });
3917
+ const dup = await fetch(`${getBaseUrl()}${streamPath}`, {
3918
+ method: `POST`,
3919
+ headers: {
3920
+ "Content-Type": `application/json`,
3921
+ [PRODUCER_ID_HEADER]: `test-producer`,
3922
+ [PRODUCER_EPOCH_HEADER]: `0`,
3923
+ [PRODUCER_SEQ_HEADER]: `0`
3924
+ },
3925
+ body: JSON.stringify({ id: 1 })
3926
+ });
3927
+ expect(dup.status).toBe(204);
3928
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3929
+ method: `POST`,
3930
+ headers: {
3931
+ "Content-Type": `application/json`,
3932
+ [PRODUCER_ID_HEADER]: `test-producer`,
3933
+ [PRODUCER_EPOCH_HEADER]: `0`,
3934
+ [PRODUCER_SEQ_HEADER]: `1`
3935
+ },
3936
+ body: JSON.stringify({ id: 2 })
3937
+ });
3938
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
3939
+ const data = await readResponse.json();
3940
+ expect(data).toEqual([{ id: 1 }, { id: 2 }]);
3941
+ });
3942
+ test(`should reject invalid JSON with producer headers`, async () => {
3943
+ const streamPath = `/v1/stream/producer-json-invalid-${Date.now()}`;
3944
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3945
+ method: `PUT`,
3946
+ headers: { "Content-Type": `application/json` }
3947
+ });
3948
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3949
+ method: `POST`,
3950
+ headers: {
3951
+ "Content-Type": `application/json`,
3952
+ [PRODUCER_ID_HEADER]: `test-producer`,
3953
+ [PRODUCER_EPOCH_HEADER]: `0`,
3954
+ [PRODUCER_SEQ_HEADER]: `0`
3955
+ },
3956
+ body: `{ invalid json }`
3957
+ });
3958
+ expect(r.status).toBe(400);
3959
+ });
3960
+ test(`should reject empty JSON array with producer headers`, async () => {
3961
+ const streamPath = `/v1/stream/producer-json-empty-${Date.now()}`;
3962
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3963
+ method: `PUT`,
3964
+ headers: { "Content-Type": `application/json` }
3965
+ });
3966
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3967
+ method: `POST`,
3968
+ headers: {
3969
+ "Content-Type": `application/json`,
3970
+ [PRODUCER_ID_HEADER]: `test-producer`,
3971
+ [PRODUCER_EPOCH_HEADER]: `0`,
3972
+ [PRODUCER_SEQ_HEADER]: `0`
3973
+ },
3974
+ body: `[]`
3975
+ });
3976
+ expect(r.status).toBe(400);
3977
+ });
3978
+ test(`should return 404 for non-existent stream`, async () => {
3979
+ const streamPath = `/v1/stream/producer-404-${Date.now()}`;
3980
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3981
+ method: `POST`,
3982
+ headers: {
3983
+ "Content-Type": `text/plain`,
3984
+ [PRODUCER_ID_HEADER]: `test-producer`,
3985
+ [PRODUCER_EPOCH_HEADER]: `0`,
3986
+ [PRODUCER_SEQ_HEADER]: `0`
3987
+ },
3988
+ body: `data`
3989
+ });
3990
+ expect(r.status).toBe(404);
3991
+ });
3992
+ test(`should return 409 for content-type mismatch`, async () => {
3993
+ const streamPath = `/v1/stream/producer-ct-mismatch-${Date.now()}`;
3994
+ await fetch(`${getBaseUrl()}${streamPath}`, {
3995
+ method: `PUT`,
3996
+ headers: { "Content-Type": `text/plain` }
3997
+ });
3998
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
3999
+ method: `POST`,
4000
+ headers: {
4001
+ "Content-Type": `application/json`,
4002
+ [PRODUCER_ID_HEADER]: `test-producer`,
4003
+ [PRODUCER_EPOCH_HEADER]: `0`,
4004
+ [PRODUCER_SEQ_HEADER]: `0`
4005
+ },
4006
+ body: JSON.stringify({ data: `test` })
4007
+ });
4008
+ expect(r.status).toBe(409);
4009
+ });
4010
+ test(`should return 400 for empty body`, async () => {
4011
+ const streamPath = `/v1/stream/producer-empty-body-${Date.now()}`;
4012
+ await fetch(`${getBaseUrl()}${streamPath}`, {
4013
+ method: `PUT`,
4014
+ headers: { "Content-Type": `text/plain` }
4015
+ });
4016
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
4017
+ method: `POST`,
4018
+ headers: {
4019
+ "Content-Type": `text/plain`,
4020
+ [PRODUCER_ID_HEADER]: `test-producer`,
4021
+ [PRODUCER_EPOCH_HEADER]: `0`,
4022
+ [PRODUCER_SEQ_HEADER]: `0`
4023
+ },
4024
+ body: ``
4025
+ });
4026
+ expect(r.status).toBe(400);
4027
+ });
4028
+ test(`should reject empty Producer-Id`, async () => {
4029
+ const streamPath = `/v1/stream/producer-empty-id-${Date.now()}`;
4030
+ await fetch(`${getBaseUrl()}${streamPath}`, {
4031
+ method: `PUT`,
4032
+ headers: { "Content-Type": `text/plain` }
4033
+ });
4034
+ const r = await fetch(`${getBaseUrl()}${streamPath}`, {
4035
+ method: `POST`,
4036
+ headers: {
4037
+ "Content-Type": `text/plain`,
4038
+ [PRODUCER_ID_HEADER]: ``,
4039
+ [PRODUCER_EPOCH_HEADER]: `0`,
4040
+ [PRODUCER_SEQ_HEADER]: `0`
4041
+ },
4042
+ body: `data`
4043
+ });
4044
+ expect(r.status).toBe(400);
4045
+ });
4046
+ });
2935
4047
  }
2936
4048
 
2937
4049
  //#endregion