postgresai 0.15.0-dev.3 → 0.15.0-dev.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.
@@ -0,0 +1,761 @@
1
+ import { describe, test, expect, mock, afterEach, beforeEach } from "bun:test";
2
+ import { uploadFile, downloadFile, buildMarkdownLink } from "../lib/storage";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import * as os from "os";
6
+
7
+ const originalFetch = globalThis.fetch;
8
+
9
+ describe("buildMarkdownLink", () => {
10
+ const storageBaseUrl = "https://postgres.ai/storage";
11
+
12
+ test("returns image markdown for .png", () => {
13
+ const result = buildMarkdownLink("/files/123/image.png", storageBaseUrl);
14
+ expect(result).toBe("![image.png](https://postgres.ai/storage/files/123/image.png)");
15
+ });
16
+
17
+ test("returns image markdown for .jpg", () => {
18
+ const result = buildMarkdownLink("/files/123/photo.jpg", storageBaseUrl);
19
+ expect(result).toBe("![photo.jpg](https://postgres.ai/storage/files/123/photo.jpg)");
20
+ });
21
+
22
+ test("returns image markdown for .jpeg", () => {
23
+ const result = buildMarkdownLink("/files/123/photo.jpeg", storageBaseUrl);
24
+ expect(result).toBe("![photo.jpeg](https://postgres.ai/storage/files/123/photo.jpeg)");
25
+ });
26
+
27
+ test("returns image markdown for .gif", () => {
28
+ const result = buildMarkdownLink("/files/123/anim.gif", storageBaseUrl);
29
+ expect(result).toBe("![anim.gif](https://postgres.ai/storage/files/123/anim.gif)");
30
+ });
31
+
32
+ test("returns image markdown for .webp", () => {
33
+ const result = buildMarkdownLink("/files/123/pic.webp", storageBaseUrl);
34
+ expect(result).toBe("![pic.webp](https://postgres.ai/storage/files/123/pic.webp)");
35
+ });
36
+
37
+ test("returns image markdown for .svg", () => {
38
+ const result = buildMarkdownLink("/files/123/diagram.svg", storageBaseUrl);
39
+ expect(result).toBe("![diagram.svg](https://postgres.ai/storage/files/123/diagram.svg)");
40
+ });
41
+
42
+ test("returns file link for .pdf", () => {
43
+ const result = buildMarkdownLink("/files/123/report.pdf", storageBaseUrl);
44
+ expect(result).toBe("[report.pdf](https://postgres.ai/storage/files/123/report.pdf)");
45
+ });
46
+
47
+ test("returns file link for .log", () => {
48
+ const result = buildMarkdownLink("/files/123/app.log", storageBaseUrl);
49
+ expect(result).toBe("[app.log](https://postgres.ai/storage/files/123/app.log)");
50
+ });
51
+
52
+ test("returns file link for .sql", () => {
53
+ const result = buildMarkdownLink("/files/123/query.sql", storageBaseUrl);
54
+ expect(result).toBe("[query.sql](https://postgres.ai/storage/files/123/query.sql)");
55
+ });
56
+
57
+ test("uses custom filename when provided", () => {
58
+ const result = buildMarkdownLink("/files/123/abc123.png", storageBaseUrl, "screenshot.png");
59
+ expect(result).toBe("![screenshot.png](https://postgres.ai/storage/files/123/abc123.png)");
60
+ });
61
+
62
+ test("handles full URL input", () => {
63
+ const result = buildMarkdownLink("https://postgres.ai/storage/files/123/image.png", storageBaseUrl);
64
+ expect(result).toBe("![image.png](https://postgres.ai/storage/files/123/image.png)");
65
+ });
66
+
67
+ test("is case-insensitive for extension detection", () => {
68
+ const result = buildMarkdownLink("/files/123/IMAGE.PNG", storageBaseUrl);
69
+ expect(result).toBe("![IMAGE.PNG](https://postgres.ai/storage/files/123/IMAGE.PNG)");
70
+ });
71
+ });
72
+
73
+ describe("uploadFile", () => {
74
+ afterEach(() => {
75
+ globalThis.fetch = originalFetch;
76
+ });
77
+
78
+ test("throws when apiKey is missing", async () => {
79
+ await expect(
80
+ uploadFile({ apiKey: "", storageBaseUrl: "https://storage.example.com", filePath: "/tmp/test.txt" })
81
+ ).rejects.toThrow("API key is required");
82
+ });
83
+
84
+ test("throws when storageBaseUrl is missing", async () => {
85
+ await expect(
86
+ uploadFile({ apiKey: "key", storageBaseUrl: "", filePath: "/tmp/test.txt" })
87
+ ).rejects.toThrow("storageBaseUrl is required");
88
+ });
89
+
90
+ test("throws when filePath is missing", async () => {
91
+ await expect(
92
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: "" })
93
+ ).rejects.toThrow("filePath is required");
94
+ });
95
+
96
+ test("throws when file does not exist", async () => {
97
+ await expect(
98
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: "/tmp/nonexistent-file-12345.txt" })
99
+ ).rejects.toThrow("File not found");
100
+ });
101
+
102
+ test("throws on non-file path", async () => {
103
+ await expect(
104
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: os.tmpdir() })
105
+ ).rejects.toThrow("Not a file");
106
+ });
107
+
108
+ test("sends correct request and returns parsed response", async () => {
109
+ const tmpFile = path.join(os.tmpdir(), `storage-test-${Date.now()}.txt`);
110
+ fs.writeFileSync(tmpFile, "hello world");
111
+
112
+ const mockUploadResponse = {
113
+ success: true,
114
+ url: "/files/123/1707500000000_abc.txt",
115
+ metadata: {
116
+ originalName: "storage-test.txt",
117
+ size: 11,
118
+ mimeType: "text/plain",
119
+ uploadedAt: "2025-02-09T12:00:00.000Z",
120
+ duration: 50,
121
+ },
122
+ requestId: "req-123",
123
+ };
124
+
125
+ let capturedUrl = "";
126
+ let capturedHeaders: Record<string, string> = {};
127
+ let capturedBody: FormData | undefined;
128
+
129
+ globalThis.fetch = mock((url: string, init?: RequestInit) => {
130
+ capturedUrl = url;
131
+ capturedHeaders = Object.fromEntries(
132
+ Object.entries(init?.headers as Record<string, string> || {})
133
+ );
134
+ capturedBody = init?.body as FormData;
135
+ return Promise.resolve(
136
+ new Response(JSON.stringify(mockUploadResponse), {
137
+ status: 200,
138
+ headers: { "Content-Type": "application/json" },
139
+ })
140
+ );
141
+ }) as unknown as typeof fetch;
142
+
143
+ try {
144
+ const result = await uploadFile({
145
+ apiKey: "test-key",
146
+ storageBaseUrl: "https://postgres.ai/storage",
147
+ filePath: tmpFile,
148
+ });
149
+
150
+ expect(capturedUrl).toBe("https://postgres.ai/storage/upload");
151
+ expect(capturedHeaders["access-token"]).toBe("test-key");
152
+ expect(result.success).toBe(true);
153
+ expect(result.url).toBe("/files/123/1707500000000_abc.txt");
154
+ expect(result.metadata.originalName).toBe("storage-test.txt");
155
+ expect(result.metadata.size).toBe(11);
156
+ } finally {
157
+ fs.unlinkSync(tmpFile);
158
+ }
159
+ });
160
+
161
+ test("sends correct MIME type for .png file", async () => {
162
+ const tmpFile = path.join(os.tmpdir(), `storage-mime-${Date.now()}.png`);
163
+ fs.writeFileSync(tmpFile, "fake-png");
164
+
165
+ let capturedBody: FormData | undefined;
166
+
167
+ globalThis.fetch = mock((_url: string, init?: RequestInit) => {
168
+ capturedBody = init?.body as FormData;
169
+ return Promise.resolve(
170
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.png", metadata: { originalName: "a.png", size: 8, mimeType: "image/png", uploadedAt: "", duration: 0 }, requestId: "r" }), {
171
+ status: 200,
172
+ headers: { "Content-Type": "application/json" },
173
+ })
174
+ );
175
+ }) as unknown as typeof fetch;
176
+
177
+ try {
178
+ await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
179
+ const file = capturedBody!.get("file") as Blob;
180
+ expect(file.type).toBe("image/png");
181
+ } finally {
182
+ fs.unlinkSync(tmpFile);
183
+ }
184
+ });
185
+
186
+ test("sends correct MIME type for .jpg file", async () => {
187
+ const tmpFile = path.join(os.tmpdir(), `storage-mime-${Date.now()}.jpg`);
188
+ fs.writeFileSync(tmpFile, "fake-jpg");
189
+
190
+ let capturedBody: FormData | undefined;
191
+
192
+ globalThis.fetch = mock((_url: string, init?: RequestInit) => {
193
+ capturedBody = init?.body as FormData;
194
+ return Promise.resolve(
195
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.jpg", metadata: { originalName: "a.jpg", size: 8, mimeType: "image/jpeg", uploadedAt: "", duration: 0 }, requestId: "r" }), {
196
+ status: 200,
197
+ headers: { "Content-Type": "application/json" },
198
+ })
199
+ );
200
+ }) as unknown as typeof fetch;
201
+
202
+ try {
203
+ await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
204
+ const file = capturedBody!.get("file") as Blob;
205
+ expect(file.type).toBe("image/jpeg");
206
+ } finally {
207
+ fs.unlinkSync(tmpFile);
208
+ }
209
+ });
210
+
211
+ test("sends correct MIME type for .pdf file", async () => {
212
+ const tmpFile = path.join(os.tmpdir(), `storage-mime-${Date.now()}.pdf`);
213
+ fs.writeFileSync(tmpFile, "fake-pdf");
214
+
215
+ let capturedBody: FormData | undefined;
216
+
217
+ globalThis.fetch = mock((_url: string, init?: RequestInit) => {
218
+ capturedBody = init?.body as FormData;
219
+ return Promise.resolve(
220
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.pdf", metadata: { originalName: "a.pdf", size: 8, mimeType: "application/pdf", uploadedAt: "", duration: 0 }, requestId: "r" }), {
221
+ status: 200,
222
+ headers: { "Content-Type": "application/json" },
223
+ })
224
+ );
225
+ }) as unknown as typeof fetch;
226
+
227
+ try {
228
+ await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
229
+ const file = capturedBody!.get("file") as Blob;
230
+ expect(file.type).toBe("application/pdf");
231
+ } finally {
232
+ fs.unlinkSync(tmpFile);
233
+ }
234
+ });
235
+
236
+ test("sends correct MIME type for .sql file", async () => {
237
+ const tmpFile = path.join(os.tmpdir(), `storage-mime-${Date.now()}.sql`);
238
+ fs.writeFileSync(tmpFile, "SELECT 1");
239
+
240
+ let capturedBody: FormData | undefined;
241
+
242
+ globalThis.fetch = mock((_url: string, init?: RequestInit) => {
243
+ capturedBody = init?.body as FormData;
244
+ return Promise.resolve(
245
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.sql", metadata: { originalName: "a.sql", size: 8, mimeType: "application/sql", uploadedAt: "", duration: 0 }, requestId: "r" }), {
246
+ status: 200,
247
+ headers: { "Content-Type": "application/json" },
248
+ })
249
+ );
250
+ }) as unknown as typeof fetch;
251
+
252
+ try {
253
+ await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
254
+ const file = capturedBody!.get("file") as Blob;
255
+ expect(file.type).toBe("application/sql");
256
+ } finally {
257
+ fs.unlinkSync(tmpFile);
258
+ }
259
+ });
260
+
261
+ test("falls back to application/octet-stream for unknown extension", async () => {
262
+ const tmpFile = path.join(os.tmpdir(), `storage-mime-${Date.now()}.xyz`);
263
+ fs.writeFileSync(tmpFile, "data");
264
+
265
+ let capturedBody: FormData | undefined;
266
+
267
+ globalThis.fetch = mock((_url: string, init?: RequestInit) => {
268
+ capturedBody = init?.body as FormData;
269
+ return Promise.resolve(
270
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.xyz", metadata: { originalName: "a.xyz", size: 4, mimeType: "application/octet-stream", uploadedAt: "", duration: 0 }, requestId: "r" }), {
271
+ status: 200,
272
+ headers: { "Content-Type": "application/json" },
273
+ })
274
+ );
275
+ }) as unknown as typeof fetch;
276
+
277
+ try {
278
+ await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
279
+ const file = capturedBody!.get("file") as Blob;
280
+ expect(file.type).toBe("application/octet-stream");
281
+ } finally {
282
+ fs.unlinkSync(tmpFile);
283
+ }
284
+ });
285
+
286
+ test("throws on HTTP error response", async () => {
287
+ const tmpFile = path.join(os.tmpdir(), `storage-test-err-${Date.now()}.txt`);
288
+ fs.writeFileSync(tmpFile, "hello");
289
+
290
+ globalThis.fetch = mock(() =>
291
+ Promise.resolve(
292
+ new Response(JSON.stringify({ code: "DANGEROUS_EXTENSION", message: "Extension not allowed" }), {
293
+ status: 400,
294
+ headers: { "Content-Type": "application/json" },
295
+ })
296
+ )
297
+ ) as unknown as typeof fetch;
298
+
299
+ try {
300
+ await expect(
301
+ uploadFile({ apiKey: "test-key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile })
302
+ ).rejects.toThrow("Failed to upload file");
303
+ } finally {
304
+ fs.unlinkSync(tmpFile);
305
+ }
306
+ });
307
+ });
308
+
309
+ describe("downloadFile", () => {
310
+ let tmpDir: string;
311
+
312
+ beforeEach(() => {
313
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "storage-dl-test-"));
314
+ });
315
+
316
+ afterEach(() => {
317
+ globalThis.fetch = originalFetch;
318
+ fs.rmSync(tmpDir, { recursive: true, force: true });
319
+ });
320
+
321
+ test("throws when apiKey is missing", async () => {
322
+ await expect(
323
+ downloadFile({ apiKey: "", storageBaseUrl: "https://storage.example.com", fileUrl: "/files/123/test.png" })
324
+ ).rejects.toThrow("API key is required");
325
+ });
326
+
327
+ test("throws when fileUrl is missing", async () => {
328
+ await expect(
329
+ downloadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", fileUrl: "" })
330
+ ).rejects.toThrow("fileUrl is required");
331
+ });
332
+
333
+ test("downloads file with relative path and saves to output", async () => {
334
+ const fileContent = Buffer.from("binary content here");
335
+ const outputPath = path.join(tmpDir, "downloaded.png");
336
+
337
+ globalThis.fetch = mock((url: string, init?: RequestInit) => {
338
+ expect(url).toBe("https://postgres.ai/storage/files/123/image.png");
339
+ expect((init?.headers as Record<string, string>)["access-token"]).toBe("test-key");
340
+ return Promise.resolve(
341
+ new Response(fileContent, {
342
+ status: 200,
343
+ headers: { "Content-Type": "image/png" },
344
+ })
345
+ );
346
+ }) as unknown as typeof fetch;
347
+
348
+ const result = await downloadFile({
349
+ apiKey: "test-key",
350
+ storageBaseUrl: "https://postgres.ai/storage",
351
+ fileUrl: "/files/123/image.png",
352
+ outputPath,
353
+ });
354
+
355
+ expect(result.savedTo).toBe(outputPath);
356
+ expect(result.size).toBe(fileContent.length);
357
+ expect(result.mimeType).toBe("image/png");
358
+ expect(fs.existsSync(outputPath)).toBe(true);
359
+ expect(fs.readFileSync(outputPath).toString()).toBe("binary content here");
360
+ });
361
+
362
+ test("downloads file with full URL matching storage origin", async () => {
363
+ const outputPath = path.join(tmpDir, "out.txt");
364
+
365
+ globalThis.fetch = mock((url: string) => {
366
+ expect(url).toBe("https://postgres.ai/storage/files/1/data.txt");
367
+ return Promise.resolve(
368
+ new Response("text data", {
369
+ status: 200,
370
+ headers: { "Content-Type": "text/plain" },
371
+ })
372
+ );
373
+ }) as unknown as typeof fetch;
374
+
375
+ const result = await downloadFile({
376
+ apiKey: "test-key",
377
+ storageBaseUrl: "https://postgres.ai/storage",
378
+ fileUrl: "https://postgres.ai/storage/files/1/data.txt",
379
+ outputPath,
380
+ });
381
+
382
+ expect(result.savedTo).toBe(outputPath);
383
+ expect(fs.readFileSync(outputPath, "utf-8")).toBe("text data");
384
+ });
385
+
386
+ test("rejects full URL with mismatched origin", async () => {
387
+ await expect(
388
+ downloadFile({
389
+ apiKey: "test-key",
390
+ storageBaseUrl: "https://postgres.ai/storage",
391
+ fileUrl: "https://evil.com/files/1/data.txt",
392
+ outputPath: path.join(tmpDir, "out.txt"),
393
+ })
394
+ ).rejects.toThrow("URL must be under storage base URL");
395
+ });
396
+
397
+ test("rejects same-origin URL outside storage path", async () => {
398
+ await expect(
399
+ downloadFile({
400
+ apiKey: "test-key",
401
+ storageBaseUrl: "https://postgres.ai/storage",
402
+ fileUrl: "https://postgres.ai/malicious/files/1/data.txt",
403
+ outputPath: path.join(tmpDir, "out.txt"),
404
+ })
405
+ ).rejects.toThrow("URL must be under storage base URL");
406
+ });
407
+
408
+ test("allows explicit outputPath outside cwd", async () => {
409
+ const outputPath = path.join(tmpDir, "explicit-out.png");
410
+
411
+ globalThis.fetch = mock(() =>
412
+ Promise.resolve(new Response("ok", { status: 200, headers: { "Content-Type": "image/png" } }))
413
+ ) as unknown as typeof fetch;
414
+
415
+ const result = await downloadFile({
416
+ apiKey: "test-key",
417
+ storageBaseUrl: "https://postgres.ai/storage",
418
+ fileUrl: "/files/123/image.png",
419
+ outputPath,
420
+ });
421
+
422
+ expect(result.savedTo).toBe(outputPath);
423
+ expect(fs.existsSync(outputPath)).toBe(true);
424
+ });
425
+
426
+ test("throws when storageBaseUrl is missing", async () => {
427
+ await expect(
428
+ downloadFile({ apiKey: "key", storageBaseUrl: "", fileUrl: "/files/1/a.png" })
429
+ ).rejects.toThrow("storageBaseUrl is required");
430
+ });
431
+
432
+ test("derives filename from URL when outputPath not specified", async () => {
433
+ // We need to control cwd resolution — use an absolute output by mocking the behavior
434
+ const originalResolve = path.resolve;
435
+
436
+ globalThis.fetch = mock(() =>
437
+ Promise.resolve(
438
+ new Response("data", {
439
+ status: 200,
440
+ headers: { "Content-Type": "application/octet-stream" },
441
+ })
442
+ )
443
+ ) as unknown as typeof fetch;
444
+
445
+ const result = await downloadFile({
446
+ apiKey: "test-key",
447
+ storageBaseUrl: "https://postgres.ai/storage",
448
+ fileUrl: "/files/123/report.csv",
449
+ outputPath: path.join(tmpDir, "report.csv"),
450
+ });
451
+
452
+ expect(result.savedTo).toEndWith("report.csv");
453
+ expect(fs.existsSync(result.savedTo)).toBe(true);
454
+ });
455
+
456
+ test("throws on HTTP error response", async () => {
457
+ globalThis.fetch = mock(() =>
458
+ Promise.resolve(
459
+ new Response(JSON.stringify({ code: "FORBIDDEN", message: "Access denied" }), {
460
+ status: 403,
461
+ headers: { "Content-Type": "application/json" },
462
+ })
463
+ )
464
+ ) as unknown as typeof fetch;
465
+
466
+ await expect(
467
+ downloadFile({
468
+ apiKey: "test-key",
469
+ storageBaseUrl: "https://storage.example.com",
470
+ fileUrl: "/files/999/secret.png",
471
+ outputPath: path.join(tmpDir, "nope.png"),
472
+ })
473
+ ).rejects.toThrow("Failed to download file");
474
+ });
475
+
476
+ test("handles path without leading slash", async () => {
477
+ globalThis.fetch = mock((url: string) => {
478
+ expect(url).toBe("https://postgres.ai/storage/files/123/image.png");
479
+ return Promise.resolve(
480
+ new Response("ok", { status: 200, headers: { "Content-Type": "image/png" } })
481
+ );
482
+ }) as unknown as typeof fetch;
483
+
484
+ const outputPath = path.join(tmpDir, "out.png");
485
+ const result = await downloadFile({
486
+ apiKey: "test-key",
487
+ storageBaseUrl: "https://postgres.ai/storage",
488
+ fileUrl: "files/123/image.png",
489
+ outputPath,
490
+ });
491
+
492
+ expect(result.size).toBe(2);
493
+ });
494
+
495
+ test("path.basename strips traversal from URL-derived filename", async () => {
496
+ // path.basename("../../../etc/passwd") → "passwd", safe within cwd
497
+ globalThis.fetch = mock(() =>
498
+ Promise.resolve(new Response("safe", { status: 200, headers: { "Content-Type": "text/plain" } }))
499
+ ) as unknown as typeof fetch;
500
+
501
+ const result = await downloadFile({
502
+ apiKey: "test-key",
503
+ storageBaseUrl: "https://postgres.ai/storage",
504
+ fileUrl: "/files/123/..%2F..%2F..%2Fetc%2Fpasswd",
505
+ outputPath: path.join(tmpDir, "traversal-test.txt"),
506
+ });
507
+
508
+ // File written to explicit outputPath, not to /etc/passwd
509
+ expect(result.savedTo).toBe(path.join(tmpDir, "traversal-test.txt"));
510
+ });
511
+
512
+ test("URL-derived filename with traversal resolves safely via basename", async () => {
513
+ globalThis.fetch = mock(() =>
514
+ Promise.resolve(new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }))
515
+ ) as unknown as typeof fetch;
516
+
517
+ // basename extracts just "secret.txt" from a traversal path
518
+ const result = await downloadFile({
519
+ apiKey: "test-key",
520
+ storageBaseUrl: "https://postgres.ai/storage",
521
+ fileUrl: "/files/123/secret.txt",
522
+ outputPath: path.join(tmpDir, "safe.txt"),
523
+ });
524
+
525
+ expect(result.savedTo).toBe(path.join(tmpDir, "safe.txt"));
526
+ expect(fs.existsSync(result.savedTo)).toBe(true);
527
+ });
528
+ });
529
+
530
+ describe("uploadFile size limit", () => {
531
+ afterEach(() => {
532
+ globalThis.fetch = originalFetch;
533
+ });
534
+
535
+ test("throws when file exceeds 500MB", async () => {
536
+ const tmpFile = path.join(os.tmpdir(), `storage-big-${Date.now()}.bin`);
537
+ const fd = fs.openSync(tmpFile, "w");
538
+ fs.ftruncateSync(fd, 501 * 1024 * 1024);
539
+ fs.closeSync(fd);
540
+
541
+ try {
542
+ await expect(
543
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile })
544
+ ).rejects.toThrow("File too large");
545
+ } finally {
546
+ fs.unlinkSync(tmpFile);
547
+ }
548
+ });
549
+
550
+ test("allows file at exactly 500MB", async () => {
551
+ const tmpFile = path.join(os.tmpdir(), `storage-exact-${Date.now()}.bin`);
552
+ const fd = fs.openSync(tmpFile, "w");
553
+ fs.ftruncateSync(fd, 500 * 1024 * 1024);
554
+ fs.closeSync(fd);
555
+
556
+ globalThis.fetch = mock(() =>
557
+ Promise.resolve(
558
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.bin", metadata: { originalName: "a.bin", size: 0, mimeType: "application/octet-stream", uploadedAt: "", duration: 0 }, requestId: "r" }), {
559
+ status: 200,
560
+ headers: { "Content-Type": "application/json" },
561
+ })
562
+ )
563
+ ) as unknown as typeof fetch;
564
+
565
+ try {
566
+ const result = await uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile });
567
+ expect(result.success).toBe(true);
568
+ } finally {
569
+ fs.unlinkSync(tmpFile);
570
+ }
571
+ });
572
+
573
+ test("rejects file at 500MB + 1 byte", async () => {
574
+ const tmpFile = path.join(os.tmpdir(), `storage-over-${Date.now()}.bin`);
575
+ const fd = fs.openSync(tmpFile, "w");
576
+ fs.ftruncateSync(fd, 500 * 1024 * 1024 + 1);
577
+ fs.closeSync(fd);
578
+
579
+ try {
580
+ await expect(
581
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile })
582
+ ).rejects.toThrow("File too large");
583
+ } finally {
584
+ fs.unlinkSync(tmpFile);
585
+ }
586
+ });
587
+ });
588
+
589
+ describe("downloadFile size limit", () => {
590
+ afterEach(() => {
591
+ globalThis.fetch = originalFetch;
592
+ });
593
+
594
+ test("rejects download when Content-Length exceeds 500MB", async () => {
595
+ const tooBig = String(500 * 1024 * 1024 + 1);
596
+ globalThis.fetch = mock(() =>
597
+ Promise.resolve(
598
+ new Response("", {
599
+ status: 200,
600
+ headers: { "Content-Length": tooBig, "Content-Type": "application/octet-stream" },
601
+ })
602
+ )
603
+ ) as unknown as typeof fetch;
604
+
605
+ await expect(
606
+ downloadFile({
607
+ apiKey: "key",
608
+ storageBaseUrl: "https://postgres.ai/storage",
609
+ fileUrl: "/files/1/big.bin",
610
+ outputPath: path.join(os.tmpdir(), `dl-limit-${Date.now()}.bin`),
611
+ })
612
+ ).rejects.toThrow("File too large");
613
+ });
614
+
615
+ test("allows download when Content-Length is at 500MB", async () => {
616
+ const exact = String(500 * 1024 * 1024);
617
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dl-limit-"));
618
+ globalThis.fetch = mock(() =>
619
+ Promise.resolve(
620
+ new Response("ok", {
621
+ status: 200,
622
+ headers: { "Content-Length": exact, "Content-Type": "application/octet-stream" },
623
+ })
624
+ )
625
+ ) as unknown as typeof fetch;
626
+
627
+ try {
628
+ const result = await downloadFile({
629
+ apiKey: "key",
630
+ storageBaseUrl: "https://postgres.ai/storage",
631
+ fileUrl: "/files/1/ok.bin",
632
+ outputPath: path.join(tmpDir, "ok.bin"),
633
+ });
634
+ expect(result.savedTo).toEndWith("ok.bin");
635
+ } finally {
636
+ fs.rmSync(tmpDir, { recursive: true, force: true });
637
+ }
638
+ });
639
+ });
640
+
641
+ describe("HTTP warning output", () => {
642
+ const originalConsoleError = console.error;
643
+ let consoleOutput: string[];
644
+
645
+ beforeEach(() => {
646
+ consoleOutput = [];
647
+ console.error = (...args: unknown[]) => {
648
+ consoleOutput.push(args.map(String).join(" "));
649
+ };
650
+ });
651
+
652
+ afterEach(() => {
653
+ console.error = originalConsoleError;
654
+ globalThis.fetch = originalFetch;
655
+ });
656
+
657
+ test("uploadFile warns on HTTP storage URL", async () => {
658
+ const tmpFile = path.join(os.tmpdir(), `http-warn-up-${Date.now()}.txt`);
659
+ fs.writeFileSync(tmpFile, "test");
660
+
661
+ globalThis.fetch = mock(() =>
662
+ Promise.resolve(
663
+ new Response(JSON.stringify({ success: true, url: "/files/1/a.txt", metadata: { originalName: "a.txt", size: 4, mimeType: "text/plain", uploadedAt: "", duration: 0 }, requestId: "r" }), {
664
+ status: 200,
665
+ headers: { "Content-Type": "application/json" },
666
+ })
667
+ )
668
+ ) as unknown as typeof fetch;
669
+
670
+ try {
671
+ await uploadFile({ apiKey: "key", storageBaseUrl: "http://localhost:3000/storage", filePath: tmpFile });
672
+ expect(consoleOutput.some(line => line.includes("HTTP") && line.includes("unencrypted"))).toBe(true);
673
+ } finally {
674
+ fs.unlinkSync(tmpFile);
675
+ }
676
+ });
677
+
678
+ test("downloadFile warns on HTTP storage URL", async () => {
679
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "http-warn-dl-"));
680
+ globalThis.fetch = mock(() =>
681
+ Promise.resolve(new Response("ok", { status: 200, headers: { "Content-Type": "text/plain" } }))
682
+ ) as unknown as typeof fetch;
683
+
684
+ try {
685
+ await downloadFile({
686
+ apiKey: "key",
687
+ storageBaseUrl: "http://localhost:3000/storage",
688
+ fileUrl: "/files/1/a.txt",
689
+ outputPath: path.join(tmpDir, "a.txt"),
690
+ });
691
+ expect(consoleOutput.some(line => line.includes("HTTP") && line.includes("unencrypted"))).toBe(true);
692
+ } finally {
693
+ fs.rmSync(tmpDir, { recursive: true, force: true });
694
+ }
695
+ });
696
+ });
697
+
698
+ describe("downloadFile edge cases", () => {
699
+ afterEach(() => {
700
+ globalThis.fetch = originalFetch;
701
+ });
702
+
703
+ test("creates parent directory when it does not exist", async () => {
704
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dl-mkdir-"));
705
+ const nestedPath = path.join(tmpDir, "sub", "dir", "file.txt");
706
+
707
+ globalThis.fetch = mock(() =>
708
+ Promise.resolve(new Response("data", { status: 200, headers: { "Content-Type": "text/plain" } }))
709
+ ) as unknown as typeof fetch;
710
+
711
+ try {
712
+ const result = await downloadFile({
713
+ apiKey: "key",
714
+ storageBaseUrl: "https://postgres.ai/storage",
715
+ fileUrl: "/files/1/file.txt",
716
+ outputPath: nestedPath,
717
+ });
718
+ expect(result.savedTo).toBe(nestedPath);
719
+ expect(fs.existsSync(nestedPath)).toBe(true);
720
+ expect(fs.readFileSync(nestedPath, "utf-8")).toBe("data");
721
+ } finally {
722
+ fs.rmSync(tmpDir, { recursive: true, force: true });
723
+ }
724
+ });
725
+
726
+ test("uploadFile throws on unparseable success response", async () => {
727
+ const tmpFile = path.join(os.tmpdir(), `bad-json-${Date.now()}.txt`);
728
+ fs.writeFileSync(tmpFile, "test");
729
+
730
+ globalThis.fetch = mock(() =>
731
+ Promise.resolve(new Response("not-json", { status: 200 }))
732
+ ) as unknown as typeof fetch;
733
+
734
+ try {
735
+ await expect(
736
+ uploadFile({ apiKey: "key", storageBaseUrl: "https://storage.example.com", filePath: tmpFile })
737
+ ).rejects.toThrow("Failed to parse upload response");
738
+ } finally {
739
+ fs.unlinkSync(tmpFile);
740
+ }
741
+ });
742
+ });
743
+
744
+ describe("buildMarkdownLink escaping", () => {
745
+ const storageBaseUrl = "https://postgres.ai/storage";
746
+
747
+ test("escapes parentheses in custom filename", () => {
748
+ const result = buildMarkdownLink("/files/123/abc.png", storageBaseUrl, "report (final).png");
749
+ expect(result).toBe("![report \\(final\\).png](https://postgres.ai/storage/files/123/abc.png)");
750
+ });
751
+
752
+ test("escapes brackets in custom filename", () => {
753
+ const result = buildMarkdownLink("/files/123/abc.csv", storageBaseUrl, "data[1].csv");
754
+ expect(result).toBe("[data\\[1\\].csv](https://postgres.ai/storage/files/123/abc.csv)");
755
+ });
756
+
757
+ test("escapes special chars in custom filename", () => {
758
+ const result = buildMarkdownLink("/files/123/abc.png", storageBaseUrl, "shot [v2] (draft).png");
759
+ expect(result).toBe("![shot \\[v2\\] \\(draft\\).png](https://postgres.ai/storage/files/123/abc.png)");
760
+ });
761
+ });