@durable-streams/client-conformance-tests 0.1.0
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/README.md +451 -0
- package/dist/adapters/typescript-adapter.d.ts +1 -0
- package/dist/adapters/typescript-adapter.js +586 -0
- package/dist/benchmark-runner-C_Yghc8f.js +1333 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +265 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +4 -0
- package/dist/protocol-DyEvTHPF.d.ts +472 -0
- package/dist/protocol-qb83AeUH.js +120 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +3 -0
- package/package.json +53 -0
- package/src/adapters/typescript-adapter.ts +848 -0
- package/src/benchmark-runner.ts +860 -0
- package/src/benchmark-scenarios.ts +311 -0
- package/src/cli.ts +294 -0
- package/src/index.ts +50 -0
- package/src/protocol.ts +656 -0
- package/src/runner.ts +1191 -0
- package/src/test-cases.ts +475 -0
- package/test-cases/consumer/cache-headers.yaml +150 -0
- package/test-cases/consumer/error-handling.yaml +108 -0
- package/test-cases/consumer/message-ordering.yaml +209 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/consumer/offset-resumption.yaml +197 -0
- package/test-cases/consumer/read-catchup.yaml +173 -0
- package/test-cases/consumer/read-longpoll.yaml +132 -0
- package/test-cases/consumer/read-sse.yaml +145 -0
- package/test-cases/consumer/retry-resilience.yaml +160 -0
- package/test-cases/consumer/streaming-equivalence.yaml +226 -0
- package/test-cases/lifecycle/dynamic-headers.yaml +147 -0
- package/test-cases/lifecycle/headers-params.yaml +117 -0
- package/test-cases/lifecycle/stream-lifecycle.yaml +148 -0
- package/test-cases/producer/append-data.yaml +142 -0
- package/test-cases/producer/batching.yaml +112 -0
- package/test-cases/producer/create-stream.yaml +87 -0
- package/test-cases/producer/error-handling.yaml +90 -0
- package/test-cases/producer/sequence-ordering.yaml +148 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test case types for client conformance testing.
|
|
3
|
+
*
|
|
4
|
+
* Test cases are defined in YAML files and describe operations to perform
|
|
5
|
+
* and expectations to verify.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Test Case Structure
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A suite of related test cases.
|
|
14
|
+
*/
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Test Case Loader
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
import * as fs from "node:fs"
|
|
20
|
+
import * as path from "node:path"
|
|
21
|
+
import YAML from "yaml"
|
|
22
|
+
|
|
23
|
+
export interface TestSuite {
|
|
24
|
+
/** Unique identifier for this suite */
|
|
25
|
+
id: string
|
|
26
|
+
/** Human-readable name */
|
|
27
|
+
name: string
|
|
28
|
+
/** Description of what this suite tests */
|
|
29
|
+
description: string
|
|
30
|
+
/** Category: producer, consumer, or lifecycle */
|
|
31
|
+
category: `producer` | `consumer` | `lifecycle`
|
|
32
|
+
/** Tags for filtering tests */
|
|
33
|
+
tags?: Array<string>
|
|
34
|
+
/** Required client features for all tests in this suite */
|
|
35
|
+
requires?: Array<ClientFeature>
|
|
36
|
+
/** Test cases in this suite */
|
|
37
|
+
tests: Array<TestCase>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A single test case with operations and expectations.
|
|
42
|
+
*/
|
|
43
|
+
export interface TestCase {
|
|
44
|
+
/** Unique identifier within the suite */
|
|
45
|
+
id: string
|
|
46
|
+
/** Human-readable name */
|
|
47
|
+
name: string
|
|
48
|
+
/** Description of what this test verifies */
|
|
49
|
+
description?: string
|
|
50
|
+
/** Tags for filtering */
|
|
51
|
+
tags?: Array<string>
|
|
52
|
+
/** Skip this test (with optional reason) */
|
|
53
|
+
skip?: boolean | string
|
|
54
|
+
/** Required client features for this test */
|
|
55
|
+
requires?: Array<ClientFeature>
|
|
56
|
+
/** Setup operations to run before the test */
|
|
57
|
+
setup?: Array<TestOperation>
|
|
58
|
+
/** Test operations to execute */
|
|
59
|
+
operations: Array<TestOperation>
|
|
60
|
+
/** Cleanup operations to run after the test */
|
|
61
|
+
cleanup?: Array<TestOperation>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Client features that may be required for certain tests.
|
|
66
|
+
*/
|
|
67
|
+
export type ClientFeature =
|
|
68
|
+
| `batching`
|
|
69
|
+
| `sse`
|
|
70
|
+
| `long-poll`
|
|
71
|
+
| `streaming`
|
|
72
|
+
| `dynamicHeaders`
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Test Operations
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a stream.
|
|
80
|
+
*/
|
|
81
|
+
export interface CreateOperation {
|
|
82
|
+
action: `create`
|
|
83
|
+
/** Stream path (a unique path will be generated if not specified) */
|
|
84
|
+
path?: string
|
|
85
|
+
/** Variable name to store the generated path */
|
|
86
|
+
as?: string
|
|
87
|
+
/** Content type */
|
|
88
|
+
contentType?: string
|
|
89
|
+
/** TTL in seconds */
|
|
90
|
+
ttlSeconds?: number
|
|
91
|
+
/** Absolute expiry (ISO 8601) */
|
|
92
|
+
expiresAt?: string
|
|
93
|
+
/** Custom headers */
|
|
94
|
+
headers?: Record<string, string>
|
|
95
|
+
/** Expected result */
|
|
96
|
+
expect?: CreateExpectation
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Connect to an existing stream.
|
|
101
|
+
*/
|
|
102
|
+
export interface ConnectOperation {
|
|
103
|
+
action: `connect`
|
|
104
|
+
/** Stream path or variable reference like ${streamPath} */
|
|
105
|
+
path: string
|
|
106
|
+
headers?: Record<string, string>
|
|
107
|
+
expect?: ConnectExpectation
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Append data to a stream.
|
|
112
|
+
*/
|
|
113
|
+
export interface AppendOperation {
|
|
114
|
+
action: `append`
|
|
115
|
+
/** Stream path or variable reference */
|
|
116
|
+
path: string
|
|
117
|
+
/** Data to append (string) */
|
|
118
|
+
data?: string
|
|
119
|
+
/** Binary data (base64 encoded) */
|
|
120
|
+
binaryData?: string
|
|
121
|
+
/** Sequence number for ordering */
|
|
122
|
+
seq?: number
|
|
123
|
+
headers?: Record<string, string>
|
|
124
|
+
expect?: AppendExpectation
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Append multiple items (tests batching behavior).
|
|
129
|
+
*/
|
|
130
|
+
export interface AppendBatchOperation {
|
|
131
|
+
action: `append-batch`
|
|
132
|
+
path: string
|
|
133
|
+
/** Items to append concurrently */
|
|
134
|
+
items: Array<{
|
|
135
|
+
data?: string
|
|
136
|
+
binaryData?: string
|
|
137
|
+
seq?: number
|
|
138
|
+
}>
|
|
139
|
+
headers?: Record<string, string>
|
|
140
|
+
expect?: AppendBatchExpectation
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read from a stream.
|
|
145
|
+
*/
|
|
146
|
+
export interface ReadOperation {
|
|
147
|
+
action: `read`
|
|
148
|
+
path: string
|
|
149
|
+
/** Starting offset or variable reference like ${lastOffset} */
|
|
150
|
+
offset?: string
|
|
151
|
+
/** Live mode */
|
|
152
|
+
live?: false | `long-poll` | `sse`
|
|
153
|
+
/** Timeout for long-poll in ms */
|
|
154
|
+
timeoutMs?: number
|
|
155
|
+
/** Maximum chunks to read */
|
|
156
|
+
maxChunks?: number
|
|
157
|
+
/** Wait until up-to-date */
|
|
158
|
+
waitForUpToDate?: boolean
|
|
159
|
+
headers?: Record<string, string>
|
|
160
|
+
expect?: ReadExpectation
|
|
161
|
+
/** Run in background (don't wait for completion) */
|
|
162
|
+
background?: boolean
|
|
163
|
+
/** Store reference for later await (required if background: true) */
|
|
164
|
+
as?: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get stream metadata.
|
|
169
|
+
*/
|
|
170
|
+
export interface HeadOperation {
|
|
171
|
+
action: `head`
|
|
172
|
+
path: string
|
|
173
|
+
headers?: Record<string, string>
|
|
174
|
+
expect?: HeadExpectation
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Delete a stream.
|
|
179
|
+
*/
|
|
180
|
+
export interface DeleteOperation {
|
|
181
|
+
action: `delete`
|
|
182
|
+
path: string
|
|
183
|
+
headers?: Record<string, string>
|
|
184
|
+
expect?: DeleteExpectation
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Wait for a duration (for timing-sensitive tests).
|
|
189
|
+
*/
|
|
190
|
+
export interface WaitOperation {
|
|
191
|
+
action: `wait`
|
|
192
|
+
/** Duration in milliseconds */
|
|
193
|
+
ms: number
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Store a value in a variable for later use.
|
|
198
|
+
*/
|
|
199
|
+
export interface SetOperation {
|
|
200
|
+
action: `set`
|
|
201
|
+
/** Variable name */
|
|
202
|
+
name: string
|
|
203
|
+
/** Value (can reference other variables) */
|
|
204
|
+
value: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Assert a condition using structured assertions (no eval).
|
|
209
|
+
*/
|
|
210
|
+
export interface AssertOperation {
|
|
211
|
+
action: `assert`
|
|
212
|
+
/** Check that two values are equal */
|
|
213
|
+
equals?: { left: string; right: string }
|
|
214
|
+
/** Check that two values are not equal */
|
|
215
|
+
notEquals?: { left: string; right: string }
|
|
216
|
+
/** Check that a string contains a substring */
|
|
217
|
+
contains?: { value: string; substring: string }
|
|
218
|
+
/** Check that a value matches a regex pattern */
|
|
219
|
+
matches?: { value: string; pattern: string }
|
|
220
|
+
/** Message if assertion fails */
|
|
221
|
+
message?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Append to stream via direct server HTTP (bypasses client adapter).
|
|
226
|
+
* Used for concurrent operations when adapter is blocked on a read.
|
|
227
|
+
*/
|
|
228
|
+
export interface ServerAppendOperation {
|
|
229
|
+
action: `server-append`
|
|
230
|
+
path: string
|
|
231
|
+
data: string
|
|
232
|
+
headers?: Record<string, string>
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Wait for a background operation to complete.
|
|
237
|
+
*/
|
|
238
|
+
export interface AwaitOperation {
|
|
239
|
+
action: `await`
|
|
240
|
+
/** Reference to the background operation (from 'as' field) */
|
|
241
|
+
ref: string
|
|
242
|
+
expect?: ReadExpectation
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Inject an error to be returned on the next N requests to a path.
|
|
247
|
+
* Used for testing retry/resilience behavior.
|
|
248
|
+
*/
|
|
249
|
+
export interface InjectErrorOperation {
|
|
250
|
+
action: `inject-error`
|
|
251
|
+
/** Stream path to inject error for */
|
|
252
|
+
path: string
|
|
253
|
+
/** HTTP status code to return */
|
|
254
|
+
status: number
|
|
255
|
+
/** Number of times to return this error (default: 1) */
|
|
256
|
+
count?: number
|
|
257
|
+
/** Optional Retry-After header value (seconds) */
|
|
258
|
+
retryAfter?: number
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clear all injected errors.
|
|
263
|
+
*/
|
|
264
|
+
export interface ClearErrorsOperation {
|
|
265
|
+
action: `clear-errors`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Set a dynamic header that is evaluated per-request.
|
|
270
|
+
* Useful for testing token refresh scenarios.
|
|
271
|
+
*/
|
|
272
|
+
export interface SetDynamicHeaderOperation {
|
|
273
|
+
action: `set-dynamic-header`
|
|
274
|
+
/** Header name */
|
|
275
|
+
name: string
|
|
276
|
+
/** Type of dynamic value */
|
|
277
|
+
valueType: `counter` | `timestamp` | `token`
|
|
278
|
+
/** Initial value (for token type) */
|
|
279
|
+
initialValue?: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Set a dynamic URL parameter that is evaluated per-request.
|
|
284
|
+
*/
|
|
285
|
+
export interface SetDynamicParamOperation {
|
|
286
|
+
action: `set-dynamic-param`
|
|
287
|
+
/** Param name */
|
|
288
|
+
name: string
|
|
289
|
+
/** Type of dynamic value */
|
|
290
|
+
valueType: `counter` | `timestamp`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Clear all dynamic headers and params.
|
|
295
|
+
*/
|
|
296
|
+
export interface ClearDynamicOperation {
|
|
297
|
+
action: `clear-dynamic`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* All possible test operations.
|
|
302
|
+
*/
|
|
303
|
+
export type TestOperation =
|
|
304
|
+
| CreateOperation
|
|
305
|
+
| ConnectOperation
|
|
306
|
+
| AppendOperation
|
|
307
|
+
| AppendBatchOperation
|
|
308
|
+
| ReadOperation
|
|
309
|
+
| HeadOperation
|
|
310
|
+
| DeleteOperation
|
|
311
|
+
| WaitOperation
|
|
312
|
+
| SetOperation
|
|
313
|
+
| AssertOperation
|
|
314
|
+
| ServerAppendOperation
|
|
315
|
+
| AwaitOperation
|
|
316
|
+
| InjectErrorOperation
|
|
317
|
+
| ClearErrorsOperation
|
|
318
|
+
| SetDynamicHeaderOperation
|
|
319
|
+
| SetDynamicParamOperation
|
|
320
|
+
| ClearDynamicOperation
|
|
321
|
+
|
|
322
|
+
// =============================================================================
|
|
323
|
+
// Expectations
|
|
324
|
+
// =============================================================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Base expectation fields.
|
|
328
|
+
*/
|
|
329
|
+
interface BaseExpectation {
|
|
330
|
+
/** Expected HTTP status code */
|
|
331
|
+
status?: number
|
|
332
|
+
/** Expected error code (if operation should fail) */
|
|
333
|
+
errorCode?: string
|
|
334
|
+
/** Store result in variable */
|
|
335
|
+
storeAs?: string
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface CreateExpectation extends BaseExpectation {
|
|
339
|
+
/** Status should be 201 for new, 200 for existing */
|
|
340
|
+
status?: 200 | 201 | 409 | number
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface ConnectExpectation extends BaseExpectation {
|
|
344
|
+
status?: 200 | 404 | number
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface AppendExpectation extends BaseExpectation {
|
|
348
|
+
status?: 200 | 404 | 409 | number
|
|
349
|
+
/** Store the returned offset */
|
|
350
|
+
storeOffsetAs?: string
|
|
351
|
+
/** Expected headers that were sent (for dynamic header testing) */
|
|
352
|
+
headersSent?: Record<string, string>
|
|
353
|
+
/** Expected params that were sent (for dynamic param testing) */
|
|
354
|
+
paramsSent?: Record<string, string>
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface AppendBatchExpectation extends BaseExpectation {
|
|
358
|
+
/** All items should succeed */
|
|
359
|
+
allSucceed?: boolean
|
|
360
|
+
/** Specific items should succeed (by index) */
|
|
361
|
+
succeedIndices?: Array<number>
|
|
362
|
+
/** Specific items should fail (by index) */
|
|
363
|
+
failIndices?: Array<number>
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface ReadExpectation extends BaseExpectation {
|
|
367
|
+
status?: 200 | 204 | 404 | number
|
|
368
|
+
/** Expected data content (exact match) */
|
|
369
|
+
data?: string
|
|
370
|
+
/** Expected data to contain (substring) */
|
|
371
|
+
dataContains?: string
|
|
372
|
+
/** Expected data to contain all of these substrings */
|
|
373
|
+
dataContainsAll?: Array<string>
|
|
374
|
+
/** Expected number of chunks */
|
|
375
|
+
chunkCount?: number
|
|
376
|
+
/** Minimum number of chunks */
|
|
377
|
+
minChunks?: number
|
|
378
|
+
/** Maximum number of chunks */
|
|
379
|
+
maxChunks?: number
|
|
380
|
+
/** Should be up-to-date after read */
|
|
381
|
+
upToDate?: boolean
|
|
382
|
+
/** Store final offset */
|
|
383
|
+
storeOffsetAs?: string
|
|
384
|
+
/** Store all data concatenated */
|
|
385
|
+
storeDataAs?: string
|
|
386
|
+
/** Expected headers that were sent (for dynamic header testing) */
|
|
387
|
+
headersSent?: Record<string, string>
|
|
388
|
+
/** Expected params that were sent (for dynamic param testing) */
|
|
389
|
+
paramsSent?: Record<string, string>
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface HeadExpectation extends BaseExpectation {
|
|
393
|
+
status?: 200 | 404 | number
|
|
394
|
+
/** Expected content type */
|
|
395
|
+
contentType?: string
|
|
396
|
+
/** Should have an offset */
|
|
397
|
+
hasOffset?: boolean
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface DeleteExpectation extends BaseExpectation {
|
|
401
|
+
status?: 200 | 204 | 404 | number
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Load all test suites from a directory.
|
|
406
|
+
*/
|
|
407
|
+
export function loadTestSuites(dir: string): Array<TestSuite> {
|
|
408
|
+
const suites: Array<TestSuite> = []
|
|
409
|
+
|
|
410
|
+
function walkDir(currentDir: string): void {
|
|
411
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
412
|
+
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
const fullPath = path.join(currentDir, entry.name)
|
|
415
|
+
|
|
416
|
+
if (entry.isDirectory()) {
|
|
417
|
+
walkDir(fullPath)
|
|
418
|
+
} else if (
|
|
419
|
+
entry.isFile() &&
|
|
420
|
+
(entry.name.endsWith(`.yaml`) || entry.name.endsWith(`.yml`))
|
|
421
|
+
) {
|
|
422
|
+
const content = fs.readFileSync(fullPath, `utf-8`)
|
|
423
|
+
const suite = YAML.parse(content) as TestSuite
|
|
424
|
+
suites.push(suite)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
walkDir(dir)
|
|
430
|
+
return suites
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Load test suites from the embedded test-cases directory.
|
|
435
|
+
*/
|
|
436
|
+
export function loadEmbeddedTestSuites(): Array<TestSuite> {
|
|
437
|
+
const testCasesDir = path.join(import.meta.dirname, `..`, `test-cases`)
|
|
438
|
+
return loadTestSuites(testCasesDir)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Filter test suites by category.
|
|
443
|
+
*/
|
|
444
|
+
export function filterByCategory(
|
|
445
|
+
suites: Array<TestSuite>,
|
|
446
|
+
category: TestSuite[`category`]
|
|
447
|
+
): Array<TestSuite> {
|
|
448
|
+
return suites.filter((s) => s.category === category)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Filter test cases by tags.
|
|
453
|
+
*/
|
|
454
|
+
export function filterByTags(
|
|
455
|
+
suites: Array<TestSuite>,
|
|
456
|
+
tags: Array<string>
|
|
457
|
+
): Array<TestSuite> {
|
|
458
|
+
return suites
|
|
459
|
+
.map((suite) => ({
|
|
460
|
+
...suite,
|
|
461
|
+
tests: suite.tests.filter(
|
|
462
|
+
(test) =>
|
|
463
|
+
test.tags?.some((t) => tags.includes(t)) ||
|
|
464
|
+
suite.tags?.some((t) => tags.includes(t))
|
|
465
|
+
),
|
|
466
|
+
}))
|
|
467
|
+
.filter((suite) => suite.tests.length > 0)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get total test count.
|
|
472
|
+
*/
|
|
473
|
+
export function countTests(suites: Array<TestSuite>): number {
|
|
474
|
+
return suites.reduce((sum, suite) => sum + suite.tests.length, 0)
|
|
475
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
id: consumer-cache-headers
|
|
2
|
+
name: HTTP Cache Headers
|
|
3
|
+
description: Tests for HTTP caching behavior (ETag, conditional requests)
|
|
4
|
+
category: consumer
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- headers
|
|
8
|
+
- protocol
|
|
9
|
+
|
|
10
|
+
tests:
|
|
11
|
+
- id: cache-head-returns-etag
|
|
12
|
+
name: HEAD returns ETag header
|
|
13
|
+
description: HEAD request should include an ETag for caching
|
|
14
|
+
setup:
|
|
15
|
+
- action: create
|
|
16
|
+
as: streamPath
|
|
17
|
+
- action: append
|
|
18
|
+
path: ${streamPath}
|
|
19
|
+
data: "cached-data"
|
|
20
|
+
operations:
|
|
21
|
+
- action: head
|
|
22
|
+
path: ${streamPath}
|
|
23
|
+
expect:
|
|
24
|
+
status: 200
|
|
25
|
+
hasOffset: true
|
|
26
|
+
|
|
27
|
+
- id: cache-read-idempotent
|
|
28
|
+
name: Repeated reads return same data
|
|
29
|
+
description: Multiple reads should return identical data (cache-friendly)
|
|
30
|
+
setup:
|
|
31
|
+
- action: create
|
|
32
|
+
as: streamPath
|
|
33
|
+
- action: append
|
|
34
|
+
path: ${streamPath}
|
|
35
|
+
data: "stable-content"
|
|
36
|
+
operations:
|
|
37
|
+
- action: read
|
|
38
|
+
path: ${streamPath}
|
|
39
|
+
expect:
|
|
40
|
+
data: "stable-content"
|
|
41
|
+
storeDataAs: firstRead
|
|
42
|
+
- action: read
|
|
43
|
+
path: ${streamPath}
|
|
44
|
+
expect:
|
|
45
|
+
data: "stable-content"
|
|
46
|
+
# Verify data is identical
|
|
47
|
+
- action: assert
|
|
48
|
+
equals:
|
|
49
|
+
left: ${firstRead}
|
|
50
|
+
right: "stable-content"
|
|
51
|
+
message: "Repeated reads should return identical data"
|
|
52
|
+
|
|
53
|
+
- id: cache-offset-deterministic
|
|
54
|
+
name: Same offset returns same data
|
|
55
|
+
description: Reading from a specific offset should always return the same data
|
|
56
|
+
setup:
|
|
57
|
+
- action: create
|
|
58
|
+
as: streamPath
|
|
59
|
+
- action: append
|
|
60
|
+
path: ${streamPath}
|
|
61
|
+
data: "chunk-a"
|
|
62
|
+
expect:
|
|
63
|
+
storeOffsetAs: offsetA
|
|
64
|
+
- action: append
|
|
65
|
+
path: ${streamPath}
|
|
66
|
+
data: "chunk-b"
|
|
67
|
+
operations:
|
|
68
|
+
# Multiple reads from same offset
|
|
69
|
+
- action: read
|
|
70
|
+
path: ${streamPath}
|
|
71
|
+
offset: ${offsetA}
|
|
72
|
+
expect:
|
|
73
|
+
data: "chunk-b"
|
|
74
|
+
- action: read
|
|
75
|
+
path: ${streamPath}
|
|
76
|
+
offset: ${offsetA}
|
|
77
|
+
expect:
|
|
78
|
+
data: "chunk-b"
|
|
79
|
+
|
|
80
|
+
- id: cache-immutable-historical-data
|
|
81
|
+
name: Historical data is immutable
|
|
82
|
+
description: Data at a given offset should never change after more appends
|
|
83
|
+
setup:
|
|
84
|
+
- action: create
|
|
85
|
+
as: streamPath
|
|
86
|
+
- action: append
|
|
87
|
+
path: ${streamPath}
|
|
88
|
+
data: "original"
|
|
89
|
+
operations:
|
|
90
|
+
# Read original data
|
|
91
|
+
- action: read
|
|
92
|
+
path: ${streamPath}
|
|
93
|
+
expect:
|
|
94
|
+
data: "original"
|
|
95
|
+
storeOffsetAs: afterOriginal
|
|
96
|
+
# Append more data
|
|
97
|
+
- action: append
|
|
98
|
+
path: ${streamPath}
|
|
99
|
+
data: "added"
|
|
100
|
+
# Read from start should still include original
|
|
101
|
+
- action: read
|
|
102
|
+
path: ${streamPath}
|
|
103
|
+
expect:
|
|
104
|
+
data: "originaladded"
|
|
105
|
+
# Read from afterOriginal should only get new data
|
|
106
|
+
- action: read
|
|
107
|
+
path: ${streamPath}
|
|
108
|
+
offset: ${afterOriginal}
|
|
109
|
+
expect:
|
|
110
|
+
data: "added"
|
|
111
|
+
|
|
112
|
+
- id: cache-content-type-preserved
|
|
113
|
+
name: Content-Type is preserved for caching
|
|
114
|
+
description: Stream content-type should be consistent across reads
|
|
115
|
+
setup:
|
|
116
|
+
- action: create
|
|
117
|
+
as: streamPath
|
|
118
|
+
contentType: application/json
|
|
119
|
+
- action: append
|
|
120
|
+
path: ${streamPath}
|
|
121
|
+
data: '{"key":"value"}'
|
|
122
|
+
operations:
|
|
123
|
+
- action: head
|
|
124
|
+
path: ${streamPath}
|
|
125
|
+
expect:
|
|
126
|
+
contentType: application/json
|
|
127
|
+
|
|
128
|
+
- id: cache-uptodate-stable
|
|
129
|
+
name: Up-to-date flag is consistent
|
|
130
|
+
description: Reading to end of stream should consistently report up-to-date
|
|
131
|
+
setup:
|
|
132
|
+
- action: create
|
|
133
|
+
as: streamPath
|
|
134
|
+
- action: append
|
|
135
|
+
path: ${streamPath}
|
|
136
|
+
data: "final"
|
|
137
|
+
operations:
|
|
138
|
+
# Multiple reads should all report up-to-date at end
|
|
139
|
+
- action: read
|
|
140
|
+
path: ${streamPath}
|
|
141
|
+
expect:
|
|
142
|
+
upToDate: true
|
|
143
|
+
- action: read
|
|
144
|
+
path: ${streamPath}
|
|
145
|
+
expect:
|
|
146
|
+
upToDate: true
|
|
147
|
+
- action: read
|
|
148
|
+
path: ${streamPath}
|
|
149
|
+
expect:
|
|
150
|
+
upToDate: true
|