@qnsp/storage-sdk 0.3.1 → 0.3.2

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/src/events.ts DELETED
@@ -1,190 +0,0 @@
1
- import type { EventEnvelope } from "./event-envelope.js";
2
- import { createEventEnvelope } from "./event-envelope.js";
3
- import type {
4
- StorageClientTelemetry,
5
- StorageClientTelemetryConfig,
6
- StorageClientTelemetryEvent,
7
- } from "./observability.js";
8
- import { createStorageClientTelemetry, isStorageClientTelemetry } from "./observability.js";
9
-
10
- type DocumentSearchEvent = EventEnvelope<{
11
- documentId: string;
12
- tenantId: string;
13
- version: number;
14
- checksumSha3: string;
15
- occurredAt: string;
16
- }>;
17
-
18
- type UsageEvent = EventEnvelope<{
19
- tenantId: string;
20
- documentId?: string;
21
- version?: number;
22
- occurredAt: string;
23
- operation: "upload" | "download";
24
- sizeBytes: number;
25
- tier: string;
26
- durationMs?: number | null;
27
- }>;
28
-
29
- type BillingEvent = EventEnvelope<{
30
- tenantId: string;
31
- meterType: "storage" | "egress" | "api_call" | string;
32
- quantity: number;
33
- unit: string;
34
- metadata?: Record<string, unknown>;
35
- occurredAt: string;
36
- }>;
37
-
38
- export type StorageEvent = DocumentSearchEvent | UsageEvent | BillingEvent;
39
-
40
- export interface StorageEventsConfig {
41
- readonly baseUrl: string;
42
- readonly apiKey?: string;
43
- readonly timeoutMs?: number;
44
- readonly telemetry?: StorageClientTelemetry | StorageClientTelemetryConfig;
45
- }
46
-
47
- type InternalStorageEventsConfig = {
48
- readonly baseUrl: string;
49
- readonly apiKey: string;
50
- readonly timeoutMs: number;
51
- };
52
-
53
- export class StorageEventsClient {
54
- private readonly config: InternalStorageEventsConfig;
55
- private readonly telemetry: StorageClientTelemetry | null;
56
- private readonly targetService: string;
57
-
58
- constructor(config: StorageEventsConfig) {
59
- this.config = {
60
- baseUrl: config.baseUrl.replace(/\/$/, ""),
61
- apiKey: config.apiKey ?? "",
62
- timeoutMs: config.timeoutMs ?? 15_000,
63
- };
64
-
65
- this.telemetry = config.telemetry
66
- ? isStorageClientTelemetry(config.telemetry)
67
- ? config.telemetry
68
- : createStorageClientTelemetry(config.telemetry)
69
- : null;
70
-
71
- try {
72
- this.targetService = new URL(this.config.baseUrl).host;
73
- } catch {
74
- this.targetService = "storage-service";
75
- }
76
- }
77
-
78
- async fetchEvents(
79
- topic: string,
80
- options?: { since?: string; limit?: number },
81
- ): Promise<StorageEvent[]> {
82
- const params = new URLSearchParams();
83
- if (options?.since) params.set("since", options.since);
84
- if (options?.limit) params.set("limit", options.limit.toString());
85
-
86
- const url = `${this.config.baseUrl}/storage/internal/events/${encodeURIComponent(topic)}${
87
- params.size > 0 ? `?${params}` : ""
88
- }`;
89
- const headers: Record<string, string> = {
90
- Accept: "application/json",
91
- };
92
- if (this.config.apiKey) {
93
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
94
- }
95
-
96
- const controller = new AbortController();
97
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
98
- const start = performance.now();
99
- let status: "ok" | "error" = "ok";
100
- let httpStatus: number | undefined;
101
- let errorMessage: string | undefined;
102
- let bytesReceived: number | undefined;
103
- const route = "/storage/internal/events/:topic";
104
-
105
- try {
106
- const response = await fetch(url, {
107
- method: "GET",
108
- headers,
109
- signal: controller.signal,
110
- });
111
- clearTimeout(timeoutId);
112
- httpStatus = response.status;
113
- if (!response.ok) {
114
- status = "error";
115
- const errorText = await response.text().catch(() => "unknown error");
116
- errorMessage = errorText;
117
- throw new Error(
118
- `Event fetch failed: ${response.status} ${response.statusText} - ${errorText}`,
119
- );
120
- }
121
- bytesReceived = Number.parseInt(response.headers.get("Content-Length") ?? "0", 10);
122
- const payload = (await response.json()) as Array<{
123
- topic: string;
124
- version: string;
125
- payload: unknown;
126
- metadata?: Record<string, unknown>;
127
- }>;
128
- return payload.map((entry) => {
129
- const metadataSource =
130
- entry.metadata && typeof entry.metadata === "object" ? entry.metadata : undefined;
131
- const normalizedMetadata: EventEnvelope["metadata"] =
132
- metadataSource !== undefined
133
- ? {
134
- timestamp:
135
- typeof metadataSource["timestamp"] === "string"
136
- ? (metadataSource["timestamp"] as string)
137
- : new Date().toISOString(),
138
- ...(typeof metadataSource["correlationId"] === "string"
139
- ? { correlationId: metadataSource["correlationId"] as string }
140
- : {}),
141
- ...(typeof metadataSource["causationId"] === "string"
142
- ? { causationId: metadataSource["causationId"] as string }
143
- : {}),
144
- ...(typeof metadataSource["tenantId"] === "string"
145
- ? { tenantId: metadataSource["tenantId"] as string }
146
- : {}),
147
- }
148
- : { timestamp: new Date().toISOString() };
149
- return createEventEnvelope({
150
- topic: entry.topic,
151
- version: entry.version,
152
- payload: entry.payload,
153
- metadata: normalizedMetadata,
154
- }) as StorageEvent;
155
- });
156
- } catch (error) {
157
- clearTimeout(timeoutId);
158
- status = "error";
159
- if (!errorMessage && error instanceof Error) {
160
- errorMessage = error.message;
161
- }
162
- if (error instanceof Error && error.name === "AbortError") {
163
- errorMessage = `timeout after ${this.config.timeoutMs}ms`;
164
- throw new Error(`Event request timeout after ${this.config.timeoutMs}ms`);
165
- }
166
- throw error;
167
- } finally {
168
- const durationMs = performance.now() - start;
169
- const event: StorageClientTelemetryEvent = {
170
- operation: `fetchEvents(${topic})`,
171
- method: "GET",
172
- route,
173
- target: this.targetService,
174
- status,
175
- durationMs,
176
- ...(typeof httpStatus === "number" ? { httpStatus } : {}),
177
- ...(status === "error" && errorMessage ? { error: errorMessage } : {}),
178
- ...(typeof bytesReceived === "number" && bytesReceived > 0 ? { bytesReceived } : {}),
179
- };
180
- this.recordTelemetryEvent(event);
181
- }
182
- }
183
-
184
- private recordTelemetryEvent(event: StorageClientTelemetryEvent): void {
185
- if (!this.telemetry) {
186
- return;
187
- }
188
- this.telemetry.record(event);
189
- }
190
- }
package/src/index.test.ts DELETED
@@ -1,499 +0,0 @@
1
- import { clearActivationCache } from "@qnsp/sdk-activation";
2
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { StorageClient, StorageEventsClient } from "./index.js";
4
-
5
- const mockTenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
6
- const baseUrl = "https://storage.qnsp.example/";
7
-
8
- // Valid UUID test constants
9
- const mockUploadId = "11111111-1111-4111-a111-111111111111";
10
- const mockUploadId2 = "22222222-2222-4222-a222-222222222222";
11
- const mockUploadIdErr = "33333333-3333-4333-a333-333333333333";
12
- const mockDocumentId = "44444444-4444-4444-a444-444444444444";
13
-
14
- const MOCK_ACTIVATION_RESPONSE = {
15
- activated: true,
16
- tenantId: "a1b2c3d4-e5f6-4789-8abc-def012345678",
17
- tier: "dev-pro",
18
- activationToken: "tok_test",
19
- expiresInSeconds: 3600,
20
- activatedAt: new Date().toISOString(),
21
- limits: {
22
- storageGB: 50,
23
- apiCalls: 100_000,
24
- enclavesEnabled: false,
25
- aiTrainingEnabled: false,
26
- aiInferenceEnabled: true,
27
- sseEnabled: true,
28
- vaultEnabled: true,
29
- },
30
- };
31
-
32
- function jsonResponse(payload: unknown, init?: ResponseInit): Response {
33
- return new Response(JSON.stringify(payload), {
34
- status: 200,
35
- headers: {
36
- "Content-Type": "application/json",
37
- ...(init?.headers ?? {}),
38
- },
39
- ...init,
40
- });
41
- }
42
-
43
- async function collectStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
44
- const reader = stream.getReader();
45
- const chunks: Uint8Array[] = [];
46
-
47
- while (true) {
48
- const { value, done } = await reader.read();
49
- if (done) {
50
- break;
51
- }
52
- if (value) {
53
- chunks.push(value);
54
- }
55
- }
56
-
57
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
58
- const buffer = new Uint8Array(totalLength);
59
- let offset = 0;
60
-
61
- for (const chunk of chunks) {
62
- buffer.set(chunk, offset);
63
- offset += chunk.byteLength;
64
- }
65
-
66
- return buffer;
67
- }
68
-
69
- describe("StorageClient", () => {
70
- beforeEach(() => {
71
- clearActivationCache();
72
- vi.stubGlobal("fetch", vi.fn());
73
- });
74
-
75
- afterEach(() => {
76
- vi.unstubAllGlobals();
77
- vi.clearAllMocks();
78
- });
79
-
80
- it("initiates an upload with authorization and sensible defaults", async () => {
81
- const client = new StorageClient({
82
- baseUrl,
83
- apiKey: "api-key",
84
- tenantId: mockTenantId,
85
- timeoutMs: 5_000,
86
- });
87
-
88
- const expectedResponse = {
89
- uploadId: mockUploadId,
90
- documentId: mockDocumentId,
91
- tenantId: mockTenantId,
92
- chunkSizeBytes: 16_777_216,
93
- totalSizeBytes: 42_000_000,
94
- totalParts: 4,
95
- expiresAt: new Date().toISOString(),
96
- resumeToken: "resume-token",
97
- pqc: {
98
- provider: "vault-pqc",
99
- algorithm: "kyber-768",
100
- algorithmNist: "ML-KEM-768",
101
- keyId: "key-1",
102
- },
103
- };
104
-
105
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
106
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
107
- .mockResolvedValueOnce(jsonResponse(expectedResponse));
108
-
109
- const result = await client.initiateUpload({
110
- name: "contract.pdf",
111
- mimeType: "application/pdf",
112
- sizeBytes: 42_000_000,
113
- tags: ["legal", "asia"],
114
- });
115
-
116
- expect(result).toEqual(expectedResponse);
117
- expect(globalThis.fetch).toHaveBeenCalledTimes(2);
118
- const call = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(1);
119
- expect(call).toBeDefined();
120
- if (!call) throw new Error("fetch not invoked");
121
- const [url, init] = call;
122
-
123
- expect(url).toBe("https://storage.qnsp.example/storage/v1/documents");
124
- expect(init?.method).toBe("POST");
125
- expect(init?.headers).toMatchObject({
126
- "Content-Type": "application/json",
127
- Authorization: "Bearer api-key",
128
- });
129
-
130
- const body = init?.body ? JSON.parse(String(init.body)) : null;
131
- expect(body).not.toBeNull();
132
- expect(body).toMatchObject({
133
- classification: "confidential",
134
- metadata: {},
135
- tags: ["legal", "asia"],
136
- });
137
- });
138
-
139
- it("uploads binary parts using streaming semantics", async () => {
140
- const client = new StorageClient({
141
- baseUrl,
142
- apiKey: "upload-key",
143
- tenantId: mockTenantId,
144
- });
145
-
146
- const expectedPayload = {
147
- uploadId: mockUploadId,
148
- partId: 1,
149
- status: "uploaded",
150
- sizeBytes: 3,
151
- checksumSha3: "abc123",
152
- retries: 0,
153
- totalParts: 8,
154
- completedParts: 1,
155
- bytesReceived: 3,
156
- lastPartNumber: 1,
157
- resumeToken: null,
158
- };
159
-
160
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
161
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
162
- .mockResolvedValueOnce(jsonResponse(expectedPayload));
163
-
164
- const source = Buffer.from([0xde, 0xad, 0xbe]);
165
-
166
- const result = await client.uploadPart(mockUploadId, 1, source);
167
- expect(result).toEqual(expectedPayload);
168
- expect(globalThis.fetch).toHaveBeenCalledTimes(2);
169
- const uploadCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(1);
170
- expect(uploadCall).toBeDefined();
171
- if (!uploadCall) throw new Error("fetch not invoked");
172
- const [, init] = uploadCall;
173
- expect(init?.method).toBe("PUT");
174
- expect(init?.headers).toMatchObject({
175
- "Content-Type": "application/octet-stream",
176
- Authorization: "Bearer upload-key",
177
- });
178
- expect(init?.body).toBeInstanceOf(ReadableStream);
179
-
180
- const bodyBuffer = await collectStream(init?.body as ReadableStream<Uint8Array>);
181
- expect(Array.from(bodyBuffer)).toEqual([0xde, 0xad, 0xbe]);
182
- });
183
-
184
- it("parses ranged download responses and surfaces stream metadata", async () => {
185
- const client = new StorageClient({
186
- baseUrl,
187
- apiKey: "test-api-key",
188
- tenantId: mockTenantId,
189
- });
190
-
191
- const payload = new ReadableStream<Uint8Array>({
192
- start(controller) {
193
- controller.enqueue(new Uint8Array([1, 2, 3]));
194
- controller.close();
195
- },
196
- });
197
-
198
- const response = new Response(payload, {
199
- status: 206,
200
- headers: {
201
- "Content-Length": "3",
202
- "Content-Range": "bytes 0-2/10",
203
- "X-Checksum-Sha3": "sha3-test",
204
- },
205
- });
206
-
207
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
208
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
209
- .mockResolvedValueOnce(response);
210
-
211
- const result = await client.downloadStream(mockDocumentId, 2, {
212
- range: "bytes=0-2",
213
- });
214
-
215
- expect(globalThis.fetch).toHaveBeenCalledWith(
216
- `https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/versions/2/content?tenantId=${mockTenantId}&range=bytes%3D0-2`,
217
- expect.objectContaining({
218
- method: "GET",
219
- headers: expect.objectContaining({
220
- Range: "bytes=0-2",
221
- }),
222
- }),
223
- );
224
-
225
- expect(result.statusCode).toBe(206);
226
- expect(result.totalSize).toBe(10);
227
- expect(result.contentLength).toBe(3);
228
- expect(result.range).toEqual({ start: 0, end: 2 });
229
- expect(result.checksumSha3).toBe("sha3-test");
230
-
231
- const buffer = await collectStream(result.stream);
232
- expect(Array.from(buffer)).toEqual([1, 2, 3]);
233
- });
234
-
235
- it("records telemetry for successful requests", async () => {
236
- const telemetry = { record: vi.fn() };
237
- const client = new StorageClient({
238
- baseUrl,
239
- apiKey: "test-api-key",
240
- tenantId: mockTenantId,
241
- telemetry,
242
- });
243
-
244
- const statusResponse = {
245
- uploadId: mockUploadId2,
246
- documentId: mockDocumentId,
247
- tenantId: mockTenantId,
248
- status: "pending",
249
- chunkSizeBytes: 4,
250
- totalSizeBytes: 4,
251
- totalParts: 1,
252
- expiresAt: new Date().toISOString(),
253
- resumeToken: null,
254
- createdAt: new Date().toISOString(),
255
- updatedAt: new Date().toISOString(),
256
- retries: 0,
257
- bytesReceived: 0,
258
- lastPartNumber: 1,
259
- };
260
-
261
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
262
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
263
- .mockResolvedValueOnce(jsonResponse(statusResponse));
264
-
265
- await client.getUploadStatus(mockUploadId2);
266
-
267
- expect(telemetry.record).toHaveBeenCalled();
268
- const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
269
- expect(event).toBeDefined();
270
- expect(event).toMatchObject({
271
- operation: "getUploadStatus",
272
- method: "GET",
273
- status: "ok",
274
- });
275
- });
276
-
277
- it("records telemetry failures", async () => {
278
- const telemetry = { record: vi.fn() };
279
- const client = new StorageClient({
280
- baseUrl,
281
- apiKey: "test-api-key",
282
- tenantId: mockTenantId,
283
- telemetry,
284
- });
285
-
286
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
287
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
288
- .mockResolvedValueOnce(new Response("boom", { status: 500, statusText: "error" }));
289
-
290
- await expect(client.completeUpload(mockUploadIdErr)).rejects.toThrow(/Storage API error/);
291
-
292
- const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
293
- expect(event).toBeDefined();
294
- expect(event).toMatchObject({
295
- operation: "completeUpload",
296
- status: "error",
297
- httpStatus: 500,
298
- });
299
- });
300
-
301
- it("records bytes for upload parts", async () => {
302
- const telemetry = { record: vi.fn() };
303
- const client = new StorageClient({
304
- baseUrl,
305
- apiKey: "test-api-key",
306
- tenantId: mockTenantId,
307
- telemetry,
308
- });
309
-
310
- const expectedPayload = {
311
- uploadId: mockUploadId,
312
- partId: 1,
313
- status: "uploaded",
314
- sizeBytes: 3,
315
- checksumSha3: "abc123",
316
- retries: 0,
317
- totalParts: 8,
318
- completedParts: 1,
319
- bytesReceived: 3,
320
- lastPartNumber: 1,
321
- resumeToken: null,
322
- };
323
-
324
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
325
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
326
- .mockResolvedValueOnce(jsonResponse(expectedPayload));
327
-
328
- await client.uploadPart(mockUploadId, 1, Buffer.from([1, 2, 3]));
329
-
330
- const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
331
- expect(event).toBeDefined();
332
- expect(event).toMatchObject({
333
- operation: "uploadPart",
334
- bytesSent: 3,
335
- status: "ok",
336
- });
337
- });
338
-
339
- it("updates and fetches document policies with x-tenant-id header", async () => {
340
- const client = new StorageClient({
341
- baseUrl,
342
- apiKey: "key",
343
- tenantId: mockTenantId,
344
- });
345
-
346
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
347
- // activation
348
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
349
- // PATCH response
350
- .mockResolvedValueOnce(
351
- jsonResponse({
352
- documentId: mockDocumentId,
353
- tenantId: mockTenantId,
354
- compliance: {
355
- retentionMode: "compliance",
356
- retainUntil: "2026-01-01T00:00:00Z",
357
- legalHolds: ["hold-1"],
358
- wormLockExpiresAt: null,
359
- },
360
- }),
361
- )
362
- // GET response
363
- .mockResolvedValueOnce(
364
- jsonResponse({
365
- documentId: mockDocumentId,
366
- tenantId: mockTenantId,
367
- compliance: {
368
- retentionMode: "compliance",
369
- retainUntil: "2026-01-01T00:00:00Z",
370
- legalHolds: ["hold-1"],
371
- wormLockExpiresAt: null,
372
- },
373
- lifecycle: {
374
- currentTier: "hot",
375
- targetTier: null,
376
- transitionAfter: null,
377
- },
378
- }),
379
- );
380
-
381
- const updated = await client.updateDocumentPolicies(mockDocumentId, {
382
- retentionMode: "compliance",
383
- retainUntil: "2026-01-01T00:00:00Z",
384
- legalHolds: ["hold-1"],
385
- });
386
- expect(updated.documentId).toBe(mockDocumentId);
387
- const patchCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
388
- .calls[1] as unknown as [string, RequestInit];
389
- expect(patchCall[0]).toBe(
390
- `https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
391
- );
392
- expect(patchCall[1]?.headers).toMatchObject({
393
- "Content-Type": "application/json",
394
- Authorization: "Bearer key",
395
- "x-tenant-id": mockTenantId,
396
- });
397
-
398
- const policies = await client.getDocumentPolicies(mockDocumentId);
399
- expect(policies.tenantId).toBe(mockTenantId);
400
- const getCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
401
- .calls[2] as unknown as [string, RequestInit];
402
- expect(getCall[0]).toBe(
403
- `https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
404
- );
405
- expect(getCall[1]?.headers).toMatchObject({
406
- "Content-Type": "application/json",
407
- Authorization: "Bearer key",
408
- "x-tenant-id": mockTenantId,
409
- });
410
- });
411
-
412
- it("applies and releases legal holds", async () => {
413
- const client = new StorageClient({
414
- baseUrl,
415
- apiKey: "key",
416
- tenantId: mockTenantId,
417
- });
418
-
419
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
420
- // activation
421
- .mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
422
- // apply
423
- .mockResolvedValueOnce(
424
- jsonResponse({
425
- documentId: mockDocumentId,
426
- tenantId: mockTenantId,
427
- legalHolds: ["hold-9"],
428
- }),
429
- )
430
- // release (204)
431
- .mockResolvedValueOnce(new Response(null, { status: 204 }));
432
-
433
- const applied = await client.applyLegalHold(mockDocumentId, { holdId: "hold-9" });
434
- expect(applied.legalHolds).toContain("hold-9");
435
- const postCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
436
- .calls[1] as unknown as [string, RequestInit];
437
- expect(postCall[0]).toBe(
438
- `https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds`,
439
- );
440
- expect(postCall[1]?.method).toBe("POST");
441
- expect(postCall[1]?.headers).toMatchObject({
442
- "x-tenant-id": mockTenantId,
443
- });
444
-
445
- await client.releaseLegalHold(mockDocumentId, "hold-9");
446
- const delCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
447
- .calls[2] as unknown as [string, RequestInit];
448
- expect(delCall[0]).toBe(
449
- `https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds/hold-9`,
450
- );
451
- expect(delCall[1]?.method).toBe("DELETE");
452
- expect(delCall[1]?.headers).toMatchObject({
453
- "x-tenant-id": mockTenantId,
454
- });
455
- });
456
- });
457
-
458
- describe("StorageEventsClient", () => {
459
- beforeEach(() => {
460
- vi.stubGlobal("fetch", vi.fn());
461
- });
462
-
463
- afterEach(() => {
464
- vi.unstubAllGlobals();
465
- vi.clearAllMocks();
466
- });
467
-
468
- it("records telemetry when fetching events", async () => {
469
- const telemetry = { record: vi.fn() };
470
- const client = new StorageEventsClient({
471
- baseUrl,
472
- apiKey: "test-api-key",
473
- telemetry,
474
- });
475
-
476
- const eventPayload = [
477
- {
478
- topic: "storage.document.replicated",
479
- version: "1",
480
- payload: { documentId: "doc-1" },
481
- metadata: { timestamp: new Date().toISOString() },
482
- },
483
- ];
484
-
485
- (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
486
- jsonResponse(eventPayload, { headers: { "Content-Length": "256" } }),
487
- );
488
-
489
- const events = await client.fetchEvents("storage.document.replicated");
490
- expect(events).toHaveLength(1);
491
-
492
- const call = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
493
- expect(call).toBeDefined();
494
- expect(call).toMatchObject({
495
- operation: expect.stringContaining("fetchEvents"),
496
- status: "ok",
497
- });
498
- });
499
- });