@durable-streams/server-conformance-tests 0.1.2 → 0.1.4
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/cli.cjs +11 -10
- package/dist/cli.js +11 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-mPjxiipV.cjs → src-DK3GDgwo.cjs} +147 -0
- package/dist/{src-DRIMnUPk.js → src-DcbQ_SIQ.js} +147 -0
- package/dist/test-runner.cjs +1 -1
- package/dist/test-runner.js +1 -1
- package/package.json +3 -2
- package/src/cli.ts +23 -38
- package/src/index.ts +254 -0
package/dist/cli.cjs
CHANGED
|
@@ -86,14 +86,19 @@ function getTestRunnerPath() {
|
|
|
86
86
|
if ((0, node_fs.existsSync)(runnerInDist)) return runnerInDist;
|
|
87
87
|
return runnerInSrc;
|
|
88
88
|
}
|
|
89
|
+
function findVitestBinary() {
|
|
90
|
+
const possiblePaths = [
|
|
91
|
+
(0, node_path.join)(__dirname$1, `..`, `node_modules`, `.bin`, `vitest`),
|
|
92
|
+
(0, node_path.join)(__dirname$1, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
|
|
93
|
+
(0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`)
|
|
94
|
+
];
|
|
95
|
+
for (const vitestPath of possiblePaths) if ((0, node_fs.existsSync)(vitestPath)) return vitestPath;
|
|
96
|
+
return `vitest`;
|
|
97
|
+
}
|
|
89
98
|
function runTests(baseUrl) {
|
|
90
99
|
return new Promise((resolvePromise) => {
|
|
91
100
|
const runnerPath = getTestRunnerPath();
|
|
92
|
-
const
|
|
93
|
-
const vitestBinAlt = (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
94
|
-
let vitestPath = `vitest`;
|
|
95
|
-
if ((0, node_fs.existsSync)(vitestBin)) vitestPath = vitestBin;
|
|
96
|
-
else if ((0, node_fs.existsSync)(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
101
|
+
const vitestPath = findVitestBinary();
|
|
97
102
|
const args = [
|
|
98
103
|
`run`,
|
|
99
104
|
runnerPath,
|
|
@@ -130,11 +135,7 @@ async function runWatch(baseUrl, watchPaths) {
|
|
|
130
135
|
const DEBOUNCE_MS = 300;
|
|
131
136
|
const spawnTests = () => {
|
|
132
137
|
const runnerPath = getTestRunnerPath();
|
|
133
|
-
const
|
|
134
|
-
const vitestBinAlt = (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
135
|
-
let vitestPath = `vitest`;
|
|
136
|
-
if ((0, node_fs.existsSync)(vitestBin)) vitestPath = vitestBin;
|
|
137
|
-
else if ((0, node_fs.existsSync)(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
138
|
+
const vitestPath = findVitestBinary();
|
|
138
139
|
const args = [
|
|
139
140
|
`run`,
|
|
140
141
|
runnerPath,
|
package/dist/cli.js
CHANGED
|
@@ -84,14 +84,19 @@ function getTestRunnerPath() {
|
|
|
84
84
|
if (existsSync(runnerInDist)) return runnerInDist;
|
|
85
85
|
return runnerInSrc;
|
|
86
86
|
}
|
|
87
|
+
function findVitestBinary() {
|
|
88
|
+
const possiblePaths = [
|
|
89
|
+
join(__dirname, `..`, `node_modules`, `.bin`, `vitest`),
|
|
90
|
+
join(__dirname, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
|
|
91
|
+
join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`)
|
|
92
|
+
];
|
|
93
|
+
for (const vitestPath of possiblePaths) if (existsSync(vitestPath)) return vitestPath;
|
|
94
|
+
return `vitest`;
|
|
95
|
+
}
|
|
87
96
|
function runTests(baseUrl) {
|
|
88
97
|
return new Promise((resolvePromise) => {
|
|
89
98
|
const runnerPath = getTestRunnerPath();
|
|
90
|
-
const
|
|
91
|
-
const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
92
|
-
let vitestPath = `vitest`;
|
|
93
|
-
if (existsSync(vitestBin)) vitestPath = vitestBin;
|
|
94
|
-
else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
99
|
+
const vitestPath = findVitestBinary();
|
|
95
100
|
const args = [
|
|
96
101
|
`run`,
|
|
97
102
|
runnerPath,
|
|
@@ -128,11 +133,7 @@ async function runWatch(baseUrl, watchPaths) {
|
|
|
128
133
|
const DEBOUNCE_MS = 300;
|
|
129
134
|
const spawnTests = () => {
|
|
130
135
|
const runnerPath = getTestRunnerPath();
|
|
131
|
-
const
|
|
132
|
-
const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
133
|
-
let vitestPath = `vitest`;
|
|
134
|
-
if (existsSync(vitestBin)) vitestPath = vitestBin;
|
|
135
|
-
else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
136
|
+
const vitestPath = findVitestBinary();
|
|
136
137
|
const args = [
|
|
137
138
|
`run`,
|
|
138
139
|
runnerPath,
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -1212,6 +1212,153 @@ function runConformanceTests(options) {
|
|
|
1212
1212
|
if (expiresHeader) (0, vitest.expect)(expiresHeader).toBeDefined();
|
|
1213
1213
|
});
|
|
1214
1214
|
});
|
|
1215
|
+
(0, vitest.describe)(`TTL Expiration Behavior`, () => {
|
|
1216
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1217
|
+
const uniquePath = (prefix) => `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1218
|
+
vitest.test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
|
|
1219
|
+
const streamPath = uniquePath(`ttl-expire-head`);
|
|
1220
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1221
|
+
method: `PUT`,
|
|
1222
|
+
headers: {
|
|
1223
|
+
"Content-Type": `text/plain`,
|
|
1224
|
+
"Stream-TTL": `1`
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1228
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1229
|
+
(0, vitest.expect)(headBefore.status).toBe(200);
|
|
1230
|
+
await sleep(1500);
|
|
1231
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1232
|
+
(0, vitest.expect)(headAfter.status).toBe(404);
|
|
1233
|
+
});
|
|
1234
|
+
vitest.test.concurrent(`should return 404 on GET after TTL expires`, async () => {
|
|
1235
|
+
const streamPath = uniquePath(`ttl-expire-get`);
|
|
1236
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1237
|
+
method: `PUT`,
|
|
1238
|
+
headers: {
|
|
1239
|
+
"Content-Type": `text/plain`,
|
|
1240
|
+
"Stream-TTL": `1`
|
|
1241
|
+
},
|
|
1242
|
+
body: `test data`
|
|
1243
|
+
});
|
|
1244
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1245
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1246
|
+
(0, vitest.expect)(getBefore.status).toBe(200);
|
|
1247
|
+
await sleep(1500);
|
|
1248
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1249
|
+
(0, vitest.expect)(getAfter.status).toBe(404);
|
|
1250
|
+
});
|
|
1251
|
+
vitest.test.concurrent(`should return 404 on POST append after TTL expires`, async () => {
|
|
1252
|
+
const streamPath = uniquePath(`ttl-expire-post`);
|
|
1253
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1254
|
+
method: `PUT`,
|
|
1255
|
+
headers: {
|
|
1256
|
+
"Content-Type": `text/plain`,
|
|
1257
|
+
"Stream-TTL": `1`
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1261
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1262
|
+
method: `POST`,
|
|
1263
|
+
headers: { "Content-Type": `text/plain` },
|
|
1264
|
+
body: `appended data`
|
|
1265
|
+
});
|
|
1266
|
+
(0, vitest.expect)([200, 204]).toContain(postBefore.status);
|
|
1267
|
+
await sleep(1500);
|
|
1268
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1269
|
+
method: `POST`,
|
|
1270
|
+
headers: { "Content-Type": `text/plain` },
|
|
1271
|
+
body: `more data`
|
|
1272
|
+
});
|
|
1273
|
+
(0, vitest.expect)(postAfter.status).toBe(404);
|
|
1274
|
+
});
|
|
1275
|
+
vitest.test.concurrent(`should return 404 on HEAD after Expires-At passes`, async () => {
|
|
1276
|
+
const streamPath = uniquePath(`expires-at-head`);
|
|
1277
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1278
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1279
|
+
method: `PUT`,
|
|
1280
|
+
headers: {
|
|
1281
|
+
"Content-Type": `text/plain`,
|
|
1282
|
+
"Stream-Expires-At": expiresAt
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1286
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1287
|
+
(0, vitest.expect)(headBefore.status).toBe(200);
|
|
1288
|
+
await sleep(1500);
|
|
1289
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1290
|
+
(0, vitest.expect)(headAfter.status).toBe(404);
|
|
1291
|
+
});
|
|
1292
|
+
vitest.test.concurrent(`should return 404 on GET after Expires-At passes`, async () => {
|
|
1293
|
+
const streamPath = uniquePath(`expires-at-get`);
|
|
1294
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1295
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1296
|
+
method: `PUT`,
|
|
1297
|
+
headers: {
|
|
1298
|
+
"Content-Type": `text/plain`,
|
|
1299
|
+
"Stream-Expires-At": expiresAt
|
|
1300
|
+
},
|
|
1301
|
+
body: `test data`
|
|
1302
|
+
});
|
|
1303
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1304
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1305
|
+
(0, vitest.expect)(getBefore.status).toBe(200);
|
|
1306
|
+
await sleep(1500);
|
|
1307
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1308
|
+
(0, vitest.expect)(getAfter.status).toBe(404);
|
|
1309
|
+
});
|
|
1310
|
+
vitest.test.concurrent(`should return 404 on POST append after Expires-At passes`, async () => {
|
|
1311
|
+
const streamPath = uniquePath(`expires-at-post`);
|
|
1312
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1313
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1314
|
+
method: `PUT`,
|
|
1315
|
+
headers: {
|
|
1316
|
+
"Content-Type": `text/plain`,
|
|
1317
|
+
"Stream-Expires-At": expiresAt
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1321
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1322
|
+
method: `POST`,
|
|
1323
|
+
headers: { "Content-Type": `text/plain` },
|
|
1324
|
+
body: `appended data`
|
|
1325
|
+
});
|
|
1326
|
+
(0, vitest.expect)([200, 204]).toContain(postBefore.status);
|
|
1327
|
+
await sleep(1500);
|
|
1328
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1329
|
+
method: `POST`,
|
|
1330
|
+
headers: { "Content-Type": `text/plain` },
|
|
1331
|
+
body: `more data`
|
|
1332
|
+
});
|
|
1333
|
+
(0, vitest.expect)(postAfter.status).toBe(404);
|
|
1334
|
+
});
|
|
1335
|
+
vitest.test.concurrent(`should allow recreating stream after TTL expires`, async () => {
|
|
1336
|
+
const streamPath = uniquePath(`ttl-recreate`);
|
|
1337
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1338
|
+
method: `PUT`,
|
|
1339
|
+
headers: {
|
|
1340
|
+
"Content-Type": `text/plain`,
|
|
1341
|
+
"Stream-TTL": `1`
|
|
1342
|
+
},
|
|
1343
|
+
body: `original data`
|
|
1344
|
+
});
|
|
1345
|
+
(0, vitest.expect)(createResponse.status).toBe(201);
|
|
1346
|
+
await sleep(1500);
|
|
1347
|
+
const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1348
|
+
method: `PUT`,
|
|
1349
|
+
headers: {
|
|
1350
|
+
"Content-Type": `application/json`,
|
|
1351
|
+
"Stream-TTL": `3600`
|
|
1352
|
+
},
|
|
1353
|
+
body: `["new data"]`
|
|
1354
|
+
});
|
|
1355
|
+
(0, vitest.expect)(recreateResponse.status).toBe(201);
|
|
1356
|
+
const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1357
|
+
(0, vitest.expect)(getResponse.status).toBe(200);
|
|
1358
|
+
const body = await getResponse.text();
|
|
1359
|
+
(0, vitest.expect)(body).toContain(`new data`);
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1215
1362
|
(0, vitest.describe)(`Caching and ETag`, () => {
|
|
1216
1363
|
(0, vitest.test)(`should generate ETag on GET responses`, async () => {
|
|
1217
1364
|
const streamPath = `/v1/stream/etag-generate-test-${Date.now()}`;
|
|
@@ -1210,6 +1210,153 @@ function runConformanceTests(options) {
|
|
|
1210
1210
|
if (expiresHeader) expect(expiresHeader).toBeDefined();
|
|
1211
1211
|
});
|
|
1212
1212
|
});
|
|
1213
|
+
describe(`TTL Expiration Behavior`, () => {
|
|
1214
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1215
|
+
const uniquePath = (prefix) => `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1216
|
+
test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
|
|
1217
|
+
const streamPath = uniquePath(`ttl-expire-head`);
|
|
1218
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1219
|
+
method: `PUT`,
|
|
1220
|
+
headers: {
|
|
1221
|
+
"Content-Type": `text/plain`,
|
|
1222
|
+
"Stream-TTL": `1`
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
expect(createResponse.status).toBe(201);
|
|
1226
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1227
|
+
expect(headBefore.status).toBe(200);
|
|
1228
|
+
await sleep(1500);
|
|
1229
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1230
|
+
expect(headAfter.status).toBe(404);
|
|
1231
|
+
});
|
|
1232
|
+
test.concurrent(`should return 404 on GET after TTL expires`, async () => {
|
|
1233
|
+
const streamPath = uniquePath(`ttl-expire-get`);
|
|
1234
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1235
|
+
method: `PUT`,
|
|
1236
|
+
headers: {
|
|
1237
|
+
"Content-Type": `text/plain`,
|
|
1238
|
+
"Stream-TTL": `1`
|
|
1239
|
+
},
|
|
1240
|
+
body: `test data`
|
|
1241
|
+
});
|
|
1242
|
+
expect(createResponse.status).toBe(201);
|
|
1243
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1244
|
+
expect(getBefore.status).toBe(200);
|
|
1245
|
+
await sleep(1500);
|
|
1246
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1247
|
+
expect(getAfter.status).toBe(404);
|
|
1248
|
+
});
|
|
1249
|
+
test.concurrent(`should return 404 on POST append after TTL expires`, async () => {
|
|
1250
|
+
const streamPath = uniquePath(`ttl-expire-post`);
|
|
1251
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1252
|
+
method: `PUT`,
|
|
1253
|
+
headers: {
|
|
1254
|
+
"Content-Type": `text/plain`,
|
|
1255
|
+
"Stream-TTL": `1`
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
expect(createResponse.status).toBe(201);
|
|
1259
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1260
|
+
method: `POST`,
|
|
1261
|
+
headers: { "Content-Type": `text/plain` },
|
|
1262
|
+
body: `appended data`
|
|
1263
|
+
});
|
|
1264
|
+
expect([200, 204]).toContain(postBefore.status);
|
|
1265
|
+
await sleep(1500);
|
|
1266
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1267
|
+
method: `POST`,
|
|
1268
|
+
headers: { "Content-Type": `text/plain` },
|
|
1269
|
+
body: `more data`
|
|
1270
|
+
});
|
|
1271
|
+
expect(postAfter.status).toBe(404);
|
|
1272
|
+
});
|
|
1273
|
+
test.concurrent(`should return 404 on HEAD after Expires-At passes`, async () => {
|
|
1274
|
+
const streamPath = uniquePath(`expires-at-head`);
|
|
1275
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1276
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1277
|
+
method: `PUT`,
|
|
1278
|
+
headers: {
|
|
1279
|
+
"Content-Type": `text/plain`,
|
|
1280
|
+
"Stream-Expires-At": expiresAt
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
expect(createResponse.status).toBe(201);
|
|
1284
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1285
|
+
expect(headBefore.status).toBe(200);
|
|
1286
|
+
await sleep(1500);
|
|
1287
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
|
|
1288
|
+
expect(headAfter.status).toBe(404);
|
|
1289
|
+
});
|
|
1290
|
+
test.concurrent(`should return 404 on GET after Expires-At passes`, async () => {
|
|
1291
|
+
const streamPath = uniquePath(`expires-at-get`);
|
|
1292
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1293
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1294
|
+
method: `PUT`,
|
|
1295
|
+
headers: {
|
|
1296
|
+
"Content-Type": `text/plain`,
|
|
1297
|
+
"Stream-Expires-At": expiresAt
|
|
1298
|
+
},
|
|
1299
|
+
body: `test data`
|
|
1300
|
+
});
|
|
1301
|
+
expect(createResponse.status).toBe(201);
|
|
1302
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1303
|
+
expect(getBefore.status).toBe(200);
|
|
1304
|
+
await sleep(1500);
|
|
1305
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1306
|
+
expect(getAfter.status).toBe(404);
|
|
1307
|
+
});
|
|
1308
|
+
test.concurrent(`should return 404 on POST append after Expires-At passes`, async () => {
|
|
1309
|
+
const streamPath = uniquePath(`expires-at-post`);
|
|
1310
|
+
const expiresAt = new Date(Date.now() + 1e3).toISOString();
|
|
1311
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1312
|
+
method: `PUT`,
|
|
1313
|
+
headers: {
|
|
1314
|
+
"Content-Type": `text/plain`,
|
|
1315
|
+
"Stream-Expires-At": expiresAt
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
expect(createResponse.status).toBe(201);
|
|
1319
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1320
|
+
method: `POST`,
|
|
1321
|
+
headers: { "Content-Type": `text/plain` },
|
|
1322
|
+
body: `appended data`
|
|
1323
|
+
});
|
|
1324
|
+
expect([200, 204]).toContain(postBefore.status);
|
|
1325
|
+
await sleep(1500);
|
|
1326
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1327
|
+
method: `POST`,
|
|
1328
|
+
headers: { "Content-Type": `text/plain` },
|
|
1329
|
+
body: `more data`
|
|
1330
|
+
});
|
|
1331
|
+
expect(postAfter.status).toBe(404);
|
|
1332
|
+
});
|
|
1333
|
+
test.concurrent(`should allow recreating stream after TTL expires`, async () => {
|
|
1334
|
+
const streamPath = uniquePath(`ttl-recreate`);
|
|
1335
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1336
|
+
method: `PUT`,
|
|
1337
|
+
headers: {
|
|
1338
|
+
"Content-Type": `text/plain`,
|
|
1339
|
+
"Stream-TTL": `1`
|
|
1340
|
+
},
|
|
1341
|
+
body: `original data`
|
|
1342
|
+
});
|
|
1343
|
+
expect(createResponse.status).toBe(201);
|
|
1344
|
+
await sleep(1500);
|
|
1345
|
+
const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1346
|
+
method: `PUT`,
|
|
1347
|
+
headers: {
|
|
1348
|
+
"Content-Type": `application/json`,
|
|
1349
|
+
"Stream-TTL": `3600`
|
|
1350
|
+
},
|
|
1351
|
+
body: `["new data"]`
|
|
1352
|
+
});
|
|
1353
|
+
expect(recreateResponse.status).toBe(201);
|
|
1354
|
+
const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
|
|
1355
|
+
expect(getResponse.status).toBe(200);
|
|
1356
|
+
const body = await getResponse.text();
|
|
1357
|
+
expect(body).toContain(`new data`);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1213
1360
|
describe(`Caching and ETag`, () => {
|
|
1214
1361
|
test(`should generate ETag on GET responses`, async () => {
|
|
1215
1362
|
const streamPath = `/v1/stream/etag-generate-test-${Date.now()}`;
|
package/dist/test-runner.cjs
CHANGED
package/dist/test-runner.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@durable-streams/server-conformance-tests",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Conformance test suite for Durable Streams server implementations",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"module": "./dist/index.js",
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"bin": {
|
|
26
|
+
"server-conformance-tests": "./dist/cli.js",
|
|
26
27
|
"durable-streams-server-conformance": "./dist/cli.js",
|
|
27
28
|
"durable-streams-server-conformance-dev": "./bin/conformance-dev.mjs"
|
|
28
29
|
},
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
],
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"fast-check": "^4.4.0",
|
|
50
|
-
"vitest": "^
|
|
51
|
+
"vitest": "^4.0.0",
|
|
51
52
|
"@durable-streams/client": "0.1.2"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -131,28 +131,31 @@ function getTestRunnerPath(): string {
|
|
|
131
131
|
return runnerInSrc
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Find vitest binary in various possible locations
|
|
135
|
+
function findVitestBinary(): string {
|
|
136
|
+
const possiblePaths = [
|
|
137
|
+
// Package's own node_modules (bundled dependency)
|
|
138
|
+
join(__dirname, `..`, `node_modules`, `.bin`, `vitest`),
|
|
139
|
+
// Hoisted location for scoped packages (@scope/package-name/dist -> node_modules/.bin)
|
|
140
|
+
join(__dirname, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
|
|
141
|
+
// Monorepo root node_modules (for development)
|
|
142
|
+
join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
for (const vitestPath of possiblePaths) {
|
|
146
|
+
if (existsSync(vitestPath)) {
|
|
147
|
+
return vitestPath
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback to vitest in PATH
|
|
152
|
+
return `vitest`
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
function runTests(baseUrl: string): Promise<number> {
|
|
135
156
|
return new Promise((resolvePromise) => {
|
|
136
157
|
const runnerPath = getTestRunnerPath()
|
|
137
|
-
|
|
138
|
-
// Find vitest binary
|
|
139
|
-
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
|
|
140
|
-
const vitestBinAlt = join(
|
|
141
|
-
__dirname,
|
|
142
|
-
`..`,
|
|
143
|
-
`..`,
|
|
144
|
-
`..`,
|
|
145
|
-
`node_modules`,
|
|
146
|
-
`.bin`,
|
|
147
|
-
`vitest`
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
let vitestPath = `vitest`
|
|
151
|
-
if (existsSync(vitestBin)) {
|
|
152
|
-
vitestPath = vitestBin
|
|
153
|
-
} else if (existsSync(vitestBinAlt)) {
|
|
154
|
-
vitestPath = vitestBinAlt
|
|
155
|
-
}
|
|
158
|
+
const vitestPath = findVitestBinary()
|
|
156
159
|
|
|
157
160
|
const args = [
|
|
158
161
|
`run`,
|
|
@@ -199,25 +202,7 @@ async function runWatch(
|
|
|
199
202
|
|
|
200
203
|
const spawnTests = (): ChildProcess => {
|
|
201
204
|
const runnerPath = getTestRunnerPath()
|
|
202
|
-
|
|
203
|
-
// Find vitest binary
|
|
204
|
-
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
|
|
205
|
-
const vitestBinAlt = join(
|
|
206
|
-
__dirname,
|
|
207
|
-
`..`,
|
|
208
|
-
`..`,
|
|
209
|
-
`..`,
|
|
210
|
-
`node_modules`,
|
|
211
|
-
`.bin`,
|
|
212
|
-
`vitest`
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
let vitestPath = `vitest`
|
|
216
|
-
if (existsSync(vitestBin)) {
|
|
217
|
-
vitestPath = vitestBin
|
|
218
|
-
} else if (existsSync(vitestBinAlt)) {
|
|
219
|
-
vitestPath = vitestBinAlt
|
|
220
|
-
}
|
|
205
|
+
const vitestPath = findVitestBinary()
|
|
221
206
|
|
|
222
207
|
const args = [
|
|
223
208
|
`run`,
|
package/src/index.ts
CHANGED
|
@@ -1898,6 +1898,260 @@ export function runConformanceTests(options: ConformanceTestOptions): void {
|
|
|
1898
1898
|
})
|
|
1899
1899
|
})
|
|
1900
1900
|
|
|
1901
|
+
// ============================================================================
|
|
1902
|
+
// TTL Expiration Behavior Tests
|
|
1903
|
+
// ============================================================================
|
|
1904
|
+
|
|
1905
|
+
describe(`TTL Expiration Behavior`, () => {
|
|
1906
|
+
// Helper function to wait for a specified duration
|
|
1907
|
+
const sleep = (ms: number) =>
|
|
1908
|
+
new Promise((resolve) => setTimeout(resolve, ms))
|
|
1909
|
+
|
|
1910
|
+
// Helper to generate unique stream paths for concurrent tests
|
|
1911
|
+
const uniquePath = (prefix: string) =>
|
|
1912
|
+
`/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
1913
|
+
|
|
1914
|
+
// Run tests concurrently to avoid 6x 1.5s wait time
|
|
1915
|
+
test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
|
|
1916
|
+
const streamPath = uniquePath(`ttl-expire-head`)
|
|
1917
|
+
|
|
1918
|
+
// Create stream with 1 second TTL
|
|
1919
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1920
|
+
method: `PUT`,
|
|
1921
|
+
headers: {
|
|
1922
|
+
"Content-Type": `text/plain`,
|
|
1923
|
+
"Stream-TTL": `1`,
|
|
1924
|
+
},
|
|
1925
|
+
})
|
|
1926
|
+
expect(createResponse.status).toBe(201)
|
|
1927
|
+
|
|
1928
|
+
// Verify stream exists immediately
|
|
1929
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1930
|
+
method: `HEAD`,
|
|
1931
|
+
})
|
|
1932
|
+
expect(headBefore.status).toBe(200)
|
|
1933
|
+
|
|
1934
|
+
// Wait for TTL to expire (1 second + buffer)
|
|
1935
|
+
await sleep(1500)
|
|
1936
|
+
|
|
1937
|
+
// Stream should no longer exist
|
|
1938
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1939
|
+
method: `HEAD`,
|
|
1940
|
+
})
|
|
1941
|
+
expect(headAfter.status).toBe(404)
|
|
1942
|
+
})
|
|
1943
|
+
|
|
1944
|
+
test.concurrent(`should return 404 on GET after TTL expires`, async () => {
|
|
1945
|
+
const streamPath = uniquePath(`ttl-expire-get`)
|
|
1946
|
+
|
|
1947
|
+
// Create stream with 1 second TTL and some data
|
|
1948
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1949
|
+
method: `PUT`,
|
|
1950
|
+
headers: {
|
|
1951
|
+
"Content-Type": `text/plain`,
|
|
1952
|
+
"Stream-TTL": `1`,
|
|
1953
|
+
},
|
|
1954
|
+
body: `test data`,
|
|
1955
|
+
})
|
|
1956
|
+
expect(createResponse.status).toBe(201)
|
|
1957
|
+
|
|
1958
|
+
// Verify stream is readable immediately
|
|
1959
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1960
|
+
method: `GET`,
|
|
1961
|
+
})
|
|
1962
|
+
expect(getBefore.status).toBe(200)
|
|
1963
|
+
|
|
1964
|
+
// Wait for TTL to expire
|
|
1965
|
+
await sleep(1500)
|
|
1966
|
+
|
|
1967
|
+
// Stream should no longer exist
|
|
1968
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1969
|
+
method: `GET`,
|
|
1970
|
+
})
|
|
1971
|
+
expect(getAfter.status).toBe(404)
|
|
1972
|
+
})
|
|
1973
|
+
|
|
1974
|
+
test.concurrent(
|
|
1975
|
+
`should return 404 on POST append after TTL expires`,
|
|
1976
|
+
async () => {
|
|
1977
|
+
const streamPath = uniquePath(`ttl-expire-post`)
|
|
1978
|
+
|
|
1979
|
+
// Create stream with 1 second TTL
|
|
1980
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1981
|
+
method: `PUT`,
|
|
1982
|
+
headers: {
|
|
1983
|
+
"Content-Type": `text/plain`,
|
|
1984
|
+
"Stream-TTL": `1`,
|
|
1985
|
+
},
|
|
1986
|
+
})
|
|
1987
|
+
expect(createResponse.status).toBe(201)
|
|
1988
|
+
|
|
1989
|
+
// Verify append works immediately
|
|
1990
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
1991
|
+
method: `POST`,
|
|
1992
|
+
headers: { "Content-Type": `text/plain` },
|
|
1993
|
+
body: `appended data`,
|
|
1994
|
+
})
|
|
1995
|
+
expect([200, 204]).toContain(postBefore.status)
|
|
1996
|
+
|
|
1997
|
+
// Wait for TTL to expire
|
|
1998
|
+
await sleep(1500)
|
|
1999
|
+
|
|
2000
|
+
// Append should fail - stream no longer exists
|
|
2001
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2002
|
+
method: `POST`,
|
|
2003
|
+
headers: { "Content-Type": `text/plain` },
|
|
2004
|
+
body: `more data`,
|
|
2005
|
+
})
|
|
2006
|
+
expect(postAfter.status).toBe(404)
|
|
2007
|
+
}
|
|
2008
|
+
)
|
|
2009
|
+
|
|
2010
|
+
test.concurrent(
|
|
2011
|
+
`should return 404 on HEAD after Expires-At passes`,
|
|
2012
|
+
async () => {
|
|
2013
|
+
const streamPath = uniquePath(`expires-at-head`)
|
|
2014
|
+
|
|
2015
|
+
// Create stream that expires in 1 second
|
|
2016
|
+
const expiresAt = new Date(Date.now() + 1000).toISOString()
|
|
2017
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2018
|
+
method: `PUT`,
|
|
2019
|
+
headers: {
|
|
2020
|
+
"Content-Type": `text/plain`,
|
|
2021
|
+
"Stream-Expires-At": expiresAt,
|
|
2022
|
+
},
|
|
2023
|
+
})
|
|
2024
|
+
expect(createResponse.status).toBe(201)
|
|
2025
|
+
|
|
2026
|
+
// Verify stream exists immediately
|
|
2027
|
+
const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2028
|
+
method: `HEAD`,
|
|
2029
|
+
})
|
|
2030
|
+
expect(headBefore.status).toBe(200)
|
|
2031
|
+
|
|
2032
|
+
// Wait for expiry time to pass
|
|
2033
|
+
await sleep(1500)
|
|
2034
|
+
|
|
2035
|
+
// Stream should no longer exist
|
|
2036
|
+
const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2037
|
+
method: `HEAD`,
|
|
2038
|
+
})
|
|
2039
|
+
expect(headAfter.status).toBe(404)
|
|
2040
|
+
}
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
test.concurrent(
|
|
2044
|
+
`should return 404 on GET after Expires-At passes`,
|
|
2045
|
+
async () => {
|
|
2046
|
+
const streamPath = uniquePath(`expires-at-get`)
|
|
2047
|
+
|
|
2048
|
+
// Create stream that expires in 1 second
|
|
2049
|
+
const expiresAt = new Date(Date.now() + 1000).toISOString()
|
|
2050
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2051
|
+
method: `PUT`,
|
|
2052
|
+
headers: {
|
|
2053
|
+
"Content-Type": `text/plain`,
|
|
2054
|
+
"Stream-Expires-At": expiresAt,
|
|
2055
|
+
},
|
|
2056
|
+
body: `test data`,
|
|
2057
|
+
})
|
|
2058
|
+
expect(createResponse.status).toBe(201)
|
|
2059
|
+
|
|
2060
|
+
// Verify stream is readable immediately
|
|
2061
|
+
const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2062
|
+
method: `GET`,
|
|
2063
|
+
})
|
|
2064
|
+
expect(getBefore.status).toBe(200)
|
|
2065
|
+
|
|
2066
|
+
// Wait for expiry time to pass
|
|
2067
|
+
await sleep(1500)
|
|
2068
|
+
|
|
2069
|
+
// Stream should no longer exist
|
|
2070
|
+
const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2071
|
+
method: `GET`,
|
|
2072
|
+
})
|
|
2073
|
+
expect(getAfter.status).toBe(404)
|
|
2074
|
+
}
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
test.concurrent(
|
|
2078
|
+
`should return 404 on POST append after Expires-At passes`,
|
|
2079
|
+
async () => {
|
|
2080
|
+
const streamPath = uniquePath(`expires-at-post`)
|
|
2081
|
+
|
|
2082
|
+
// Create stream that expires in 1 second
|
|
2083
|
+
const expiresAt = new Date(Date.now() + 1000).toISOString()
|
|
2084
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2085
|
+
method: `PUT`,
|
|
2086
|
+
headers: {
|
|
2087
|
+
"Content-Type": `text/plain`,
|
|
2088
|
+
"Stream-Expires-At": expiresAt,
|
|
2089
|
+
},
|
|
2090
|
+
})
|
|
2091
|
+
expect(createResponse.status).toBe(201)
|
|
2092
|
+
|
|
2093
|
+
// Verify append works immediately
|
|
2094
|
+
const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2095
|
+
method: `POST`,
|
|
2096
|
+
headers: { "Content-Type": `text/plain` },
|
|
2097
|
+
body: `appended data`,
|
|
2098
|
+
})
|
|
2099
|
+
expect([200, 204]).toContain(postBefore.status)
|
|
2100
|
+
|
|
2101
|
+
// Wait for expiry time to pass
|
|
2102
|
+
await sleep(1500)
|
|
2103
|
+
|
|
2104
|
+
// Append should fail - stream no longer exists
|
|
2105
|
+
const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2106
|
+
method: `POST`,
|
|
2107
|
+
headers: { "Content-Type": `text/plain` },
|
|
2108
|
+
body: `more data`,
|
|
2109
|
+
})
|
|
2110
|
+
expect(postAfter.status).toBe(404)
|
|
2111
|
+
}
|
|
2112
|
+
)
|
|
2113
|
+
|
|
2114
|
+
test.concurrent(
|
|
2115
|
+
`should allow recreating stream after TTL expires`,
|
|
2116
|
+
async () => {
|
|
2117
|
+
const streamPath = uniquePath(`ttl-recreate`)
|
|
2118
|
+
|
|
2119
|
+
// Create stream with 1 second TTL
|
|
2120
|
+
const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2121
|
+
method: `PUT`,
|
|
2122
|
+
headers: {
|
|
2123
|
+
"Content-Type": `text/plain`,
|
|
2124
|
+
"Stream-TTL": `1`,
|
|
2125
|
+
},
|
|
2126
|
+
body: `original data`,
|
|
2127
|
+
})
|
|
2128
|
+
expect(createResponse.status).toBe(201)
|
|
2129
|
+
|
|
2130
|
+
// Wait for TTL to expire
|
|
2131
|
+
await sleep(1500)
|
|
2132
|
+
|
|
2133
|
+
// Recreate stream with different config - should succeed (201)
|
|
2134
|
+
const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2135
|
+
method: `PUT`,
|
|
2136
|
+
headers: {
|
|
2137
|
+
"Content-Type": `application/json`,
|
|
2138
|
+
"Stream-TTL": `3600`,
|
|
2139
|
+
},
|
|
2140
|
+
body: `["new data"]`,
|
|
2141
|
+
})
|
|
2142
|
+
expect(recreateResponse.status).toBe(201)
|
|
2143
|
+
|
|
2144
|
+
// Verify the new stream is accessible
|
|
2145
|
+
const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
|
|
2146
|
+
method: `GET`,
|
|
2147
|
+
})
|
|
2148
|
+
expect(getResponse.status).toBe(200)
|
|
2149
|
+
const body = await getResponse.text()
|
|
2150
|
+
expect(body).toContain(`new data`)
|
|
2151
|
+
}
|
|
2152
|
+
)
|
|
2153
|
+
})
|
|
2154
|
+
|
|
1901
2155
|
// ============================================================================
|
|
1902
2156
|
// Caching and ETag Tests
|
|
1903
2157
|
// ============================================================================
|