@durable-streams/server-conformance-tests 0.1.1 → 0.1.3

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.
@@ -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()}`;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ const require_src = require('./src-DK3GDgwo.cjs');
3
+
4
+ //#region src/test-runner.ts
5
+ const baseUrl = process.env.CONFORMANCE_TEST_URL;
6
+ if (!baseUrl) throw new Error("CONFORMANCE_TEST_URL environment variable is required. Use the CLI: npx @durable-streams/server-conformance-tests --run <url>");
7
+ require_src.runConformanceTests({ baseUrl });
8
+
9
+ //#endregion
@@ -0,0 +1 @@
1
+ export { };
@@ -1,4 +1,4 @@
1
- import { runConformanceTests } from "./src-DRIMnUPk.js";
1
+ import { runConformanceTests } from "./src-DcbQ_SIQ.js";
2
2
 
3
3
  //#region src/test-runner.ts
4
4
  const baseUrl = process.env.CONFORMANCE_TEST_URL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@durable-streams/server-conformance-tests",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Conformance test suite for Durable Streams server implementations",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,8 @@
19
19
  "typescript"
20
20
  ],
21
21
  "type": "module",
22
- "main": "./dist/index.js",
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
23
24
  "types": "./dist/index.d.ts",
24
25
  "bin": {
25
26
  "durable-streams-server-conformance": "./dist/cli.js",
@@ -27,25 +28,33 @@
27
28
  },
28
29
  "exports": {
29
30
  ".": {
30
- "import": "./dist/index.js",
31
- "types": "./dist/index.d.ts"
32
- }
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "./package.json": "./package.json"
33
41
  },
42
+ "sideEffects": false,
43
+ "files": [
44
+ "dist",
45
+ "src",
46
+ "bin"
47
+ ],
34
48
  "dependencies": {
35
49
  "fast-check": "^4.4.0",
36
50
  "vitest": "^3.2.4",
37
- "@durable-streams/client": "0.1.1"
51
+ "@durable-streams/client": "0.1.2"
38
52
  },
39
53
  "devDependencies": {
40
54
  "tsdown": "^0.9.0",
41
55
  "tsx": "^4.19.2",
42
56
  "typescript": "^5.0.0"
43
57
  },
44
- "files": [
45
- "dist",
46
- "src",
47
- "bin"
48
- ],
49
58
  "engines": {
50
59
  "node": ">=18.0.0"
51
60
  },
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
  // ============================================================================