@aerostack/core 0.8.3 → 0.8.5

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,1240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const client_1 = require("../client");
5
+ // Mock fetch globally
6
+ const mockFetch = vitest_1.vi.fn();
7
+ global.fetch = mockFetch;
8
+ // Mock crypto.randomUUID
9
+ vitest_1.vi.stubGlobal('crypto', { randomUUID: () => 'test-uuid-1234' });
10
+ (0, vitest_1.describe)('AerostackClient', () => {
11
+ let client;
12
+ (0, vitest_1.beforeEach)(() => {
13
+ vitest_1.vi.clearAllMocks();
14
+ });
15
+ // ─── Constructor ──────────────────────────────────────────────
16
+ (0, vitest_1.describe)('constructor', () => {
17
+ (0, vitest_1.it)('should use default baseUrl when none provided', () => {
18
+ client = new client_1.AerostackClient({});
19
+ // We verify via the request URL in subsequent tests
20
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
21
+ });
22
+ (0, vitest_1.it)('should accept custom baseUrl', () => {
23
+ client = new client_1.AerostackClient({ baseUrl: 'https://custom.api.com' });
24
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
25
+ });
26
+ (0, vitest_1.it)('should accept apiKey', () => {
27
+ client = new client_1.AerostackClient({ apiKey: 'test-key' });
28
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
29
+ });
30
+ (0, vitest_1.it)('should accept jwt', () => {
31
+ client = new client_1.AerostackClient({ jwt: 'test-jwt' });
32
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
33
+ });
34
+ (0, vitest_1.it)('should accept projectId', () => {
35
+ client = new client_1.AerostackClient({ projectId: 'proj-123' });
36
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
37
+ });
38
+ (0, vitest_1.it)('should accept all config options together', () => {
39
+ client = new client_1.AerostackClient({
40
+ baseUrl: 'https://custom.api.com',
41
+ apiKey: 'key-123',
42
+ jwt: 'jwt-456',
43
+ projectId: 'proj-789',
44
+ });
45
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerostackClient);
46
+ });
47
+ });
48
+ // ─── Backward Compatibility ───────────────────────────────────
49
+ (0, vitest_1.describe)('backward compatibility', () => {
50
+ (0, vitest_1.it)('AerostackClient should extend AerocallClient', () => {
51
+ const client = new client_1.AerostackClient({});
52
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerocallClient);
53
+ });
54
+ (0, vitest_1.it)('AerocallClient should work independently', () => {
55
+ const client = new client_1.AerocallClient({});
56
+ (0, vitest_1.expect)(client).toBeInstanceOf(client_1.AerocallClient);
57
+ });
58
+ });
59
+ // ─── Headers ──────────────────────────────────────────────────
60
+ (0, vitest_1.describe)('request headers', () => {
61
+ (0, vitest_1.it)('should include X-SDK-Version header', async () => {
62
+ client = new client_1.AerostackClient({});
63
+ mockFetch.mockResolvedValueOnce({
64
+ ok: true,
65
+ json: () => Promise.resolve({ results: [] }),
66
+ });
67
+ await client.db.query('SELECT 1');
68
+ const headers = mockFetch.mock.calls[0][1].headers;
69
+ (0, vitest_1.expect)(headers['X-SDK-Version']).toBe('0.5.0');
70
+ });
71
+ (0, vitest_1.it)('should include Content-Type application/json by default', async () => {
72
+ client = new client_1.AerostackClient({});
73
+ mockFetch.mockResolvedValueOnce({
74
+ ok: true,
75
+ json: () => Promise.resolve({ results: [] }),
76
+ });
77
+ await client.db.query('SELECT 1');
78
+ const headers = mockFetch.mock.calls[0][1].headers;
79
+ (0, vitest_1.expect)(headers['Content-Type']).toBe('application/json');
80
+ });
81
+ (0, vitest_1.it)('should include X-API-Key header when apiKey provided', async () => {
82
+ client = new client_1.AerostackClient({ apiKey: 'my-api-key' });
83
+ mockFetch.mockResolvedValueOnce({
84
+ ok: true,
85
+ json: () => Promise.resolve({ results: [] }),
86
+ });
87
+ await client.db.query('SELECT 1');
88
+ const headers = mockFetch.mock.calls[0][1].headers;
89
+ (0, vitest_1.expect)(headers['X-API-Key']).toBe('my-api-key');
90
+ });
91
+ (0, vitest_1.it)('should not include X-API-Key header when apiKey not provided', async () => {
92
+ client = new client_1.AerostackClient({});
93
+ mockFetch.mockResolvedValueOnce({
94
+ ok: true,
95
+ json: () => Promise.resolve({ results: [] }),
96
+ });
97
+ await client.db.query('SELECT 1');
98
+ const headers = mockFetch.mock.calls[0][1].headers;
99
+ (0, vitest_1.expect)(headers['X-API-Key']).toBeUndefined();
100
+ });
101
+ (0, vitest_1.it)('should include Authorization Bearer header when jwt provided', async () => {
102
+ client = new client_1.AerostackClient({ jwt: 'my-jwt-token' });
103
+ mockFetch.mockResolvedValueOnce({
104
+ ok: true,
105
+ json: () => Promise.resolve({ results: [] }),
106
+ });
107
+ await client.db.query('SELECT 1');
108
+ const headers = mockFetch.mock.calls[0][1].headers;
109
+ (0, vitest_1.expect)(headers['Authorization']).toBe('Bearer my-jwt-token');
110
+ });
111
+ (0, vitest_1.it)('should not include Authorization header when jwt not provided', async () => {
112
+ client = new client_1.AerostackClient({});
113
+ mockFetch.mockResolvedValueOnce({
114
+ ok: true,
115
+ json: () => Promise.resolve({ results: [] }),
116
+ });
117
+ await client.db.query('SELECT 1');
118
+ const headers = mockFetch.mock.calls[0][1].headers;
119
+ (0, vitest_1.expect)(headers['Authorization']).toBeUndefined();
120
+ });
121
+ (0, vitest_1.it)('should include X-Project-Id header when projectId provided', async () => {
122
+ client = new client_1.AerostackClient({ projectId: 'proj-abc' });
123
+ mockFetch.mockResolvedValueOnce({
124
+ ok: true,
125
+ json: () => Promise.resolve({ results: [] }),
126
+ });
127
+ await client.db.query('SELECT 1');
128
+ const headers = mockFetch.mock.calls[0][1].headers;
129
+ (0, vitest_1.expect)(headers['X-Project-Id']).toBe('proj-abc');
130
+ });
131
+ (0, vitest_1.it)('should not include X-Project-Id header when projectId not provided', async () => {
132
+ client = new client_1.AerostackClient({});
133
+ mockFetch.mockResolvedValueOnce({
134
+ ok: true,
135
+ json: () => Promise.resolve({ results: [] }),
136
+ });
137
+ await client.db.query('SELECT 1');
138
+ const headers = mockFetch.mock.calls[0][1].headers;
139
+ (0, vitest_1.expect)(headers['X-Project-Id']).toBeUndefined();
140
+ });
141
+ (0, vitest_1.it)('should include X-Request-ID header with UUID', async () => {
142
+ client = new client_1.AerostackClient({});
143
+ mockFetch.mockResolvedValueOnce({
144
+ ok: true,
145
+ json: () => Promise.resolve({ results: [] }),
146
+ });
147
+ await client.db.query('SELECT 1');
148
+ const headers = mockFetch.mock.calls[0][1].headers;
149
+ (0, vitest_1.expect)(headers['X-Request-ID']).toBe('test-uuid-1234');
150
+ });
151
+ (0, vitest_1.it)('should include both apiKey and jwt headers when both provided', async () => {
152
+ client = new client_1.AerostackClient({ apiKey: 'key', jwt: 'token' });
153
+ mockFetch.mockResolvedValueOnce({
154
+ ok: true,
155
+ json: () => Promise.resolve({ results: [] }),
156
+ });
157
+ await client.db.query('SELECT 1');
158
+ const headers = mockFetch.mock.calls[0][1].headers;
159
+ (0, vitest_1.expect)(headers['X-API-Key']).toBe('key');
160
+ (0, vitest_1.expect)(headers['Authorization']).toBe('Bearer token');
161
+ });
162
+ });
163
+ // ─── Request Method / URL Construction ─────────────────────
164
+ (0, vitest_1.describe)('request URL construction', () => {
165
+ (0, vitest_1.it)('should use default baseUrl https://api.aerostack.dev/v1', async () => {
166
+ client = new client_1.AerostackClient({});
167
+ mockFetch.mockResolvedValueOnce({
168
+ ok: true,
169
+ json: () => Promise.resolve({ results: [] }),
170
+ });
171
+ await client.db.query('SELECT 1');
172
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.aerostack.dev/v1/db/query');
173
+ });
174
+ (0, vitest_1.it)('should use custom baseUrl when provided', async () => {
175
+ client = new client_1.AerostackClient({ baseUrl: 'https://custom.api.com/api' });
176
+ mockFetch.mockResolvedValueOnce({
177
+ ok: true,
178
+ json: () => Promise.resolve({ results: [] }),
179
+ });
180
+ await client.db.query('SELECT 1');
181
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://custom.api.com/api/db/query');
182
+ });
183
+ });
184
+ // ─── Error Handling ───────────────────────────────────────────
185
+ (0, vitest_1.describe)('error handling', () => {
186
+ (0, vitest_1.beforeEach)(() => {
187
+ client = new client_1.AerostackClient({});
188
+ });
189
+ (0, vitest_1.it)('should throw Error when response is not ok with JSON error', async () => {
190
+ mockFetch.mockResolvedValueOnce({
191
+ ok: false,
192
+ status: 400,
193
+ statusText: 'Bad Request',
194
+ json: () => Promise.resolve({ error: { message: 'Invalid query' } }),
195
+ });
196
+ await (0, vitest_1.expect)(client.db.query('BAD SQL')).rejects.toThrow('Invalid query');
197
+ });
198
+ (0, vitest_1.it)('should throw Error with statusText when JSON parsing fails', async () => {
199
+ mockFetch.mockResolvedValueOnce({
200
+ ok: false,
201
+ status: 500,
202
+ statusText: 'Internal Server Error',
203
+ json: () => Promise.reject(new Error('not json')),
204
+ });
205
+ await (0, vitest_1.expect)(client.db.query('SELECT 1')).rejects.toThrow('Internal Server Error');
206
+ });
207
+ (0, vitest_1.it)('should throw Unknown error when no error message in JSON', async () => {
208
+ mockFetch.mockResolvedValueOnce({
209
+ ok: false,
210
+ status: 400,
211
+ statusText: 'Bad Request',
212
+ json: () => Promise.resolve({}),
213
+ });
214
+ await (0, vitest_1.expect)(client.db.query('SELECT 1')).rejects.toThrow('Unknown error');
215
+ });
216
+ (0, vitest_1.it)('should throw Unknown error when error object has no message', async () => {
217
+ mockFetch.mockResolvedValueOnce({
218
+ ok: false,
219
+ status: 400,
220
+ statusText: 'Bad Request',
221
+ json: () => Promise.resolve({ error: {} }),
222
+ });
223
+ await (0, vitest_1.expect)(client.db.query('SELECT 1')).rejects.toThrow('Unknown error');
224
+ });
225
+ });
226
+ // ─── DB Namespace ─────────────────────────────────────────────
227
+ (0, vitest_1.describe)('db namespace', () => {
228
+ (0, vitest_1.beforeEach)(() => {
229
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
230
+ });
231
+ (0, vitest_1.describe)('query', () => {
232
+ (0, vitest_1.it)('should POST to /db/query with sql', async () => {
233
+ mockFetch.mockResolvedValueOnce({
234
+ ok: true,
235
+ json: () => Promise.resolve({ results: [{ id: 1, name: 'test' }] }),
236
+ });
237
+ const result = await client.db.query('SELECT * FROM users');
238
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/db/query', vitest_1.expect.objectContaining({
239
+ method: 'POST',
240
+ body: JSON.stringify({ sql: 'SELECT * FROM users', params: undefined }),
241
+ }));
242
+ (0, vitest_1.expect)(result).toEqual([{ id: 1, name: 'test' }]);
243
+ });
244
+ (0, vitest_1.it)('should POST to /db/query with sql and params', async () => {
245
+ mockFetch.mockResolvedValueOnce({
246
+ ok: true,
247
+ json: () => Promise.resolve({ results: [{ id: 1 }] }),
248
+ });
249
+ await client.db.query('SELECT * FROM users WHERE id = ?', [1]);
250
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
251
+ (0, vitest_1.expect)(body.sql).toBe('SELECT * FROM users WHERE id = ?');
252
+ (0, vitest_1.expect)(body.params).toEqual([1]);
253
+ });
254
+ (0, vitest_1.it)('should return empty array when results is null', async () => {
255
+ mockFetch.mockResolvedValueOnce({
256
+ ok: true,
257
+ json: () => Promise.resolve({ results: null }),
258
+ });
259
+ const result = await client.db.query('SELECT 1');
260
+ (0, vitest_1.expect)(result).toEqual([]);
261
+ });
262
+ (0, vitest_1.it)('should return empty array when results is undefined', async () => {
263
+ mockFetch.mockResolvedValueOnce({
264
+ ok: true,
265
+ json: () => Promise.resolve({}),
266
+ });
267
+ const result = await client.db.query('SELECT 1');
268
+ (0, vitest_1.expect)(result).toEqual([]);
269
+ });
270
+ });
271
+ (0, vitest_1.describe)('batch', () => {
272
+ (0, vitest_1.it)('should POST to /db/batch with queries', async () => {
273
+ const queries = [
274
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Alice'] },
275
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Bob'] },
276
+ ];
277
+ mockFetch.mockResolvedValueOnce({
278
+ ok: true,
279
+ json: () => Promise.resolve({
280
+ results: [
281
+ { results: [], success: true },
282
+ { results: [], success: true },
283
+ ],
284
+ success: true,
285
+ }),
286
+ });
287
+ const result = await client.db.batch(queries);
288
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
289
+ (0, vitest_1.expect)(body.queries).toEqual(queries);
290
+ (0, vitest_1.expect)(result.success).toBe(true);
291
+ (0, vitest_1.expect)(result.results).toHaveLength(2);
292
+ });
293
+ (0, vitest_1.it)('should handle batch with partial failures', async () => {
294
+ mockFetch.mockResolvedValueOnce({
295
+ ok: true,
296
+ json: () => Promise.resolve({
297
+ results: [
298
+ { results: [], success: true },
299
+ { results: [], success: false, error: 'table not found' },
300
+ ],
301
+ success: false,
302
+ }),
303
+ });
304
+ const result = await client.db.batch([
305
+ { sql: 'SELECT 1' },
306
+ { sql: 'SELECT * FROM nonexistent' },
307
+ ]);
308
+ (0, vitest_1.expect)(result.success).toBe(false);
309
+ (0, vitest_1.expect)(result.results[1].error).toBe('table not found');
310
+ });
311
+ });
312
+ });
313
+ // ─── Cache Namespace ──────────────────────────────────────────
314
+ (0, vitest_1.describe)('cache namespace', () => {
315
+ (0, vitest_1.beforeEach)(() => {
316
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
317
+ });
318
+ (0, vitest_1.describe)('get', () => {
319
+ (0, vitest_1.it)('should POST to /cache/get with key', async () => {
320
+ mockFetch.mockResolvedValueOnce({
321
+ ok: true,
322
+ json: () => Promise.resolve({ value: 'hello', exists: true }),
323
+ });
324
+ const result = await client.cache.get('my-key');
325
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/cache/get');
326
+ (0, vitest_1.expect)(JSON.parse(mockFetch.mock.calls[0][1].body)).toEqual({ key: 'my-key' });
327
+ (0, vitest_1.expect)(result).toEqual({ value: 'hello', exists: true });
328
+ });
329
+ (0, vitest_1.it)('should return null value when key does not exist', async () => {
330
+ mockFetch.mockResolvedValueOnce({
331
+ ok: true,
332
+ json: () => Promise.resolve({ value: null, exists: false }),
333
+ });
334
+ const result = await client.cache.get('nonexistent');
335
+ (0, vitest_1.expect)(result.value).toBeNull();
336
+ (0, vitest_1.expect)(result.exists).toBe(false);
337
+ });
338
+ });
339
+ (0, vitest_1.describe)('set', () => {
340
+ (0, vitest_1.it)('should POST to /cache/set with key and value', async () => {
341
+ mockFetch.mockResolvedValueOnce({
342
+ ok: true,
343
+ json: () => Promise.resolve({ success: true }),
344
+ });
345
+ await client.cache.set('key', 'value');
346
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
347
+ (0, vitest_1.expect)(body).toEqual({ key: 'key', value: 'value', ttl: undefined });
348
+ });
349
+ (0, vitest_1.it)('should POST to /cache/set with key, value, and ttl', async () => {
350
+ mockFetch.mockResolvedValueOnce({
351
+ ok: true,
352
+ json: () => Promise.resolve({ success: true }),
353
+ });
354
+ await client.cache.set('key', 'value', 3600);
355
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
356
+ (0, vitest_1.expect)(body).toEqual({ key: 'key', value: 'value', ttl: 3600 });
357
+ });
358
+ (0, vitest_1.it)('should handle object values', async () => {
359
+ mockFetch.mockResolvedValueOnce({
360
+ ok: true,
361
+ json: () => Promise.resolve({ success: true }),
362
+ });
363
+ await client.cache.set('user', { name: 'Alice', age: 30 });
364
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
365
+ (0, vitest_1.expect)(body.value).toEqual({ name: 'Alice', age: 30 });
366
+ });
367
+ });
368
+ (0, vitest_1.describe)('delete', () => {
369
+ (0, vitest_1.it)('should POST to /cache/delete with key', async () => {
370
+ mockFetch.mockResolvedValueOnce({
371
+ ok: true,
372
+ json: () => Promise.resolve({ success: true }),
373
+ });
374
+ const result = await client.cache.delete('key');
375
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/cache/delete');
376
+ (0, vitest_1.expect)(result.success).toBe(true);
377
+ });
378
+ });
379
+ (0, vitest_1.describe)('list', () => {
380
+ (0, vitest_1.it)('should POST to /cache/list with no args', async () => {
381
+ mockFetch.mockResolvedValueOnce({
382
+ ok: true,
383
+ json: () => Promise.resolve({ keys: [], list_complete: true }),
384
+ });
385
+ await client.cache.list();
386
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
387
+ (0, vitest_1.expect)(body).toEqual({ prefix: undefined });
388
+ });
389
+ (0, vitest_1.it)('should POST to /cache/list with prefix and options', async () => {
390
+ mockFetch.mockResolvedValueOnce({
391
+ ok: true,
392
+ json: () => Promise.resolve({ keys: [{ key: 'user:1' }], list_complete: false, cursor: 'abc' }),
393
+ });
394
+ const result = await client.cache.list('user:', { limit: 10, cursor: 'prev' });
395
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
396
+ (0, vitest_1.expect)(body).toEqual({ prefix: 'user:', limit: 10, cursor: 'prev' });
397
+ (0, vitest_1.expect)(result.cursor).toBe('abc');
398
+ });
399
+ });
400
+ (0, vitest_1.describe)('keys', () => {
401
+ (0, vitest_1.it)('should POST to /cache/keys and return only the keys array', async () => {
402
+ mockFetch.mockResolvedValueOnce({
403
+ ok: true,
404
+ json: () => Promise.resolve({ keys: ['key1', 'key2'], truncated: false }),
405
+ });
406
+ const result = await client.cache.keys('prefix');
407
+ (0, vitest_1.expect)(result).toEqual(['key1', 'key2']);
408
+ });
409
+ });
410
+ (0, vitest_1.describe)('getMany', () => {
411
+ (0, vitest_1.it)('should POST to /cache/getMany with keys array', async () => {
412
+ mockFetch.mockResolvedValueOnce({
413
+ ok: true,
414
+ json: () => Promise.resolve({
415
+ results: [
416
+ { key: 'k1', value: 'v1', exists: true },
417
+ { key: 'k2', value: null, exists: false },
418
+ ],
419
+ }),
420
+ });
421
+ const result = await client.cache.getMany(['k1', 'k2']);
422
+ (0, vitest_1.expect)(result).toHaveLength(2);
423
+ (0, vitest_1.expect)(result[0]).toEqual({ key: 'k1', value: 'v1', exists: true });
424
+ (0, vitest_1.expect)(result[1]).toEqual({ key: 'k2', value: null, exists: false });
425
+ });
426
+ });
427
+ (0, vitest_1.describe)('setMany', () => {
428
+ (0, vitest_1.it)('should POST to /cache/setMany with entries', async () => {
429
+ mockFetch.mockResolvedValueOnce({
430
+ ok: true,
431
+ json: () => Promise.resolve({ success: true, count: 2 }),
432
+ });
433
+ const result = await client.cache.setMany([
434
+ { key: 'k1', value: 'v1' },
435
+ { key: 'k2', value: 'v2', ttl: 600 },
436
+ ]);
437
+ (0, vitest_1.expect)(result.success).toBe(true);
438
+ (0, vitest_1.expect)(result.count).toBe(2);
439
+ });
440
+ });
441
+ (0, vitest_1.describe)('deleteMany', () => {
442
+ (0, vitest_1.it)('should POST to /cache/deleteMany with keys', async () => {
443
+ mockFetch.mockResolvedValueOnce({
444
+ ok: true,
445
+ json: () => Promise.resolve({ success: true, count: 2, deleted: 2 }),
446
+ });
447
+ const result = await client.cache.deleteMany(['k1', 'k2']);
448
+ (0, vitest_1.expect)(result.deleted).toBe(2);
449
+ });
450
+ });
451
+ (0, vitest_1.describe)('flush', () => {
452
+ (0, vitest_1.it)('should POST to /cache/flush with prefix', async () => {
453
+ mockFetch.mockResolvedValueOnce({
454
+ ok: true,
455
+ json: () => Promise.resolve({ success: true, deleted: 5 }),
456
+ });
457
+ const result = await client.cache.flush('session:');
458
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
459
+ (0, vitest_1.expect)(body).toEqual({ prefix: 'session:' });
460
+ (0, vitest_1.expect)(result.deleted).toBe(5);
461
+ });
462
+ (0, vitest_1.it)('should POST to /cache/flush without prefix', async () => {
463
+ mockFetch.mockResolvedValueOnce({
464
+ ok: true,
465
+ json: () => Promise.resolve({ success: true, deleted: 100 }),
466
+ });
467
+ const result = await client.cache.flush();
468
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
469
+ (0, vitest_1.expect)(body).toEqual({ prefix: undefined });
470
+ });
471
+ });
472
+ (0, vitest_1.describe)('expire', () => {
473
+ (0, vitest_1.it)('should POST to /cache/expire with key and ttl', async () => {
474
+ mockFetch.mockResolvedValueOnce({
475
+ ok: true,
476
+ json: () => Promise.resolve({ success: true }),
477
+ });
478
+ await client.cache.expire('key', 7200);
479
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
480
+ (0, vitest_1.expect)(body).toEqual({ key: 'key', ttl: 7200 });
481
+ });
482
+ });
483
+ (0, vitest_1.describe)('increment', () => {
484
+ (0, vitest_1.it)('should POST to /cache/increment with key only', async () => {
485
+ mockFetch.mockResolvedValueOnce({
486
+ ok: true,
487
+ json: () => Promise.resolve({ value: 1 }),
488
+ });
489
+ const result = await client.cache.increment('counter');
490
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
491
+ (0, vitest_1.expect)(body).toEqual({ key: 'counter', amount: undefined });
492
+ (0, vitest_1.expect)(result.value).toBe(1);
493
+ });
494
+ (0, vitest_1.it)('should POST to /cache/increment with amount and options', async () => {
495
+ mockFetch.mockResolvedValueOnce({
496
+ ok: true,
497
+ json: () => Promise.resolve({ value: 10 }),
498
+ });
499
+ const result = await client.cache.increment('counter', 5, { initialValue: 0, ttl: 3600 });
500
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
501
+ (0, vitest_1.expect)(body).toEqual({ key: 'counter', amount: 5, initialValue: 0, ttl: 3600 });
502
+ (0, vitest_1.expect)(result.value).toBe(10);
503
+ });
504
+ });
505
+ });
506
+ // ─── Queue Namespace ──────────────────────────────────────────
507
+ (0, vitest_1.describe)('queue namespace', () => {
508
+ (0, vitest_1.beforeEach)(() => {
509
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
510
+ });
511
+ (0, vitest_1.describe)('enqueue', () => {
512
+ (0, vitest_1.it)('should POST to /queue/enqueue with type and data', async () => {
513
+ mockFetch.mockResolvedValueOnce({
514
+ ok: true,
515
+ json: () => Promise.resolve({ success: true, jobId: 'job-123' }),
516
+ });
517
+ const result = await client.queue.enqueue('email', { to: 'user@test.com' });
518
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
519
+ (0, vitest_1.expect)(body).toEqual({ type: 'email', data: { to: 'user@test.com' } });
520
+ (0, vitest_1.expect)(result.jobId).toBe('job-123');
521
+ });
522
+ });
523
+ (0, vitest_1.describe)('getJob', () => {
524
+ (0, vitest_1.it)('should POST to /queue/job with jobId', async () => {
525
+ mockFetch.mockResolvedValueOnce({
526
+ ok: true,
527
+ json: () => Promise.resolve({
528
+ job: { id: 'job-123', type: 'email', status: 'completed' },
529
+ exists: true,
530
+ }),
531
+ });
532
+ const result = await client.queue.getJob('job-123');
533
+ (0, vitest_1.expect)(result.exists).toBe(true);
534
+ (0, vitest_1.expect)(result.job?.id).toBe('job-123');
535
+ });
536
+ (0, vitest_1.it)('should return null job when not found', async () => {
537
+ mockFetch.mockResolvedValueOnce({
538
+ ok: true,
539
+ json: () => Promise.resolve({ job: null, exists: false }),
540
+ });
541
+ const result = await client.queue.getJob('nonexistent');
542
+ (0, vitest_1.expect)(result.exists).toBe(false);
543
+ (0, vitest_1.expect)(result.job).toBeNull();
544
+ });
545
+ });
546
+ (0, vitest_1.describe)('listJobs', () => {
547
+ (0, vitest_1.it)('should POST to /queue/jobs with no options', async () => {
548
+ mockFetch.mockResolvedValueOnce({
549
+ ok: true,
550
+ json: () => Promise.resolve({ jobs: [], list_complete: true }),
551
+ });
552
+ await client.queue.listJobs();
553
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
554
+ (0, vitest_1.expect)(body).toEqual({});
555
+ });
556
+ (0, vitest_1.it)('should POST to /queue/jobs with filter options', async () => {
557
+ mockFetch.mockResolvedValueOnce({
558
+ ok: true,
559
+ json: () => Promise.resolve({
560
+ jobs: [{ id: 'j1' }],
561
+ list_complete: false,
562
+ cursor: 'next',
563
+ }),
564
+ });
565
+ const result = await client.queue.listJobs({
566
+ status: 'queued',
567
+ type: 'email',
568
+ limit: 10,
569
+ cursor: 'prev',
570
+ });
571
+ (0, vitest_1.expect)(result.jobs).toHaveLength(1);
572
+ (0, vitest_1.expect)(result.cursor).toBe('next');
573
+ });
574
+ });
575
+ (0, vitest_1.describe)('cancelJob', () => {
576
+ (0, vitest_1.it)('should POST to /queue/cancel with jobId', async () => {
577
+ mockFetch.mockResolvedValueOnce({
578
+ ok: true,
579
+ json: () => Promise.resolve({ success: true }),
580
+ });
581
+ const result = await client.queue.cancelJob('job-123');
582
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
583
+ (0, vitest_1.expect)(body).toEqual({ jobId: 'job-123' });
584
+ (0, vitest_1.expect)(result.success).toBe(true);
585
+ });
586
+ (0, vitest_1.it)('should return note when job already completed', async () => {
587
+ mockFetch.mockResolvedValueOnce({
588
+ ok: true,
589
+ json: () => Promise.resolve({ success: false, note: 'Job already completed' }),
590
+ });
591
+ const result = await client.queue.cancelJob('job-done');
592
+ (0, vitest_1.expect)(result.success).toBe(false);
593
+ (0, vitest_1.expect)(result.note).toBe('Job already completed');
594
+ });
595
+ });
596
+ });
597
+ // ─── AI Namespace ─────────────────────────────────────────────
598
+ (0, vitest_1.describe)('ai namespace', () => {
599
+ (0, vitest_1.beforeEach)(() => {
600
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
601
+ });
602
+ (0, vitest_1.describe)('chat', () => {
603
+ (0, vitest_1.it)('should POST to /ai/chat with messages', async () => {
604
+ mockFetch.mockResolvedValueOnce({
605
+ ok: true,
606
+ json: () => Promise.resolve({ response: 'Hello!' }),
607
+ });
608
+ const result = await client.ai.chat([{ role: 'user', content: 'Hi' }]);
609
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
610
+ (0, vitest_1.expect)(body.messages).toEqual([{ role: 'user', content: 'Hi' }]);
611
+ (0, vitest_1.expect)(body.model).toBeUndefined();
612
+ (0, vitest_1.expect)(result.response).toBe('Hello!');
613
+ });
614
+ (0, vitest_1.it)('should pass model when provided', async () => {
615
+ mockFetch.mockResolvedValueOnce({
616
+ ok: true,
617
+ json: () => Promise.resolve({ response: 'Response' }),
618
+ });
619
+ await client.ai.chat([{ role: 'user', content: 'Hi' }], 'gpt-4');
620
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
621
+ (0, vitest_1.expect)(body.model).toBe('gpt-4');
622
+ });
623
+ (0, vitest_1.it)('should support system and assistant roles', async () => {
624
+ mockFetch.mockResolvedValueOnce({
625
+ ok: true,
626
+ json: () => Promise.resolve({ response: 'OK' }),
627
+ });
628
+ const messages = [
629
+ { role: 'system', content: 'You are helpful' },
630
+ { role: 'user', content: 'Hi' },
631
+ { role: 'assistant', content: 'Hello!' },
632
+ { role: 'user', content: 'Thanks' },
633
+ ];
634
+ await client.ai.chat(messages);
635
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
636
+ (0, vitest_1.expect)(body.messages).toEqual(messages);
637
+ });
638
+ });
639
+ (0, vitest_1.describe)('search.ingest', () => {
640
+ (0, vitest_1.it)('should POST to /ai/search/ingest', async () => {
641
+ mockFetch.mockResolvedValueOnce({
642
+ ok: true,
643
+ json: () => Promise.resolve({ success: true }),
644
+ });
645
+ await client.ai.search.ingest('Some content', { category: 'docs' }, 'documentation');
646
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
647
+ (0, vitest_1.expect)(body).toEqual({
648
+ content: 'Some content',
649
+ metadata: { category: 'docs' },
650
+ type: 'documentation',
651
+ });
652
+ });
653
+ (0, vitest_1.it)('should work without optional params', async () => {
654
+ mockFetch.mockResolvedValueOnce({
655
+ ok: true,
656
+ json: () => Promise.resolve({ success: true }),
657
+ });
658
+ await client.ai.search.ingest('Content only');
659
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
660
+ (0, vitest_1.expect)(body).toEqual({ content: 'Content only', metadata: undefined, type: undefined });
661
+ });
662
+ });
663
+ (0, vitest_1.describe)('search.query', () => {
664
+ (0, vitest_1.it)('should POST to /ai/search/query with text', async () => {
665
+ mockFetch.mockResolvedValueOnce({
666
+ ok: true,
667
+ json: () => Promise.resolve({ results: [{ id: '1', score: 0.9 }] }),
668
+ });
669
+ const result = await client.ai.search.query('search term');
670
+ (0, vitest_1.expect)(result.results).toHaveLength(1);
671
+ });
672
+ (0, vitest_1.it)('should pass search options', async () => {
673
+ mockFetch.mockResolvedValueOnce({
674
+ ok: true,
675
+ json: () => Promise.resolve({ results: [] }),
676
+ });
677
+ await client.ai.search.query('term', { limit: 5, threshold: 0.8, filter: { type: 'doc' } });
678
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
679
+ (0, vitest_1.expect)(body).toEqual({
680
+ text: 'term',
681
+ limit: 5,
682
+ threshold: 0.8,
683
+ filter: { type: 'doc' },
684
+ });
685
+ });
686
+ });
687
+ (0, vitest_1.describe)('search.delete', () => {
688
+ (0, vitest_1.it)('should POST to /ai/search/delete with id', async () => {
689
+ mockFetch.mockResolvedValueOnce({
690
+ ok: true,
691
+ json: () => Promise.resolve({ success: true }),
692
+ });
693
+ await client.ai.search.delete('doc-1');
694
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
695
+ (0, vitest_1.expect)(body).toEqual({ id: 'doc-1' });
696
+ });
697
+ });
698
+ (0, vitest_1.describe)('search.deleteByType', () => {
699
+ (0, vitest_1.it)('should POST to /ai/search/deleteByType with type', async () => {
700
+ mockFetch.mockResolvedValueOnce({
701
+ ok: true,
702
+ json: () => Promise.resolve({ success: true }),
703
+ });
704
+ await client.ai.search.deleteByType('documentation');
705
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
706
+ (0, vitest_1.expect)(body).toEqual({ type: 'documentation' });
707
+ });
708
+ });
709
+ (0, vitest_1.describe)('search.listTypes', () => {
710
+ (0, vitest_1.it)('should GET /ai/search/listTypes', async () => {
711
+ mockFetch.mockResolvedValueOnce({
712
+ ok: true,
713
+ json: () => Promise.resolve({ types: ['doc', 'faq'] }),
714
+ });
715
+ const result = await client.ai.search.listTypes();
716
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('GET');
717
+ (0, vitest_1.expect)(result.types).toEqual(['doc', 'faq']);
718
+ });
719
+ });
720
+ (0, vitest_1.describe)('search.configure', () => {
721
+ (0, vitest_1.it)('should POST to /ai/search/configure with embedding model', async () => {
722
+ mockFetch.mockResolvedValueOnce({
723
+ ok: true,
724
+ json: () => Promise.resolve({ success: true }),
725
+ });
726
+ await client.ai.search.configure({ embeddingModel: 'multilingual' });
727
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
728
+ (0, vitest_1.expect)(body).toEqual({ embeddingModel: 'multilingual' });
729
+ });
730
+ });
731
+ (0, vitest_1.describe)('search.update', () => {
732
+ (0, vitest_1.it)('should POST to /ai/search/update with id, content, and options', async () => {
733
+ mockFetch.mockResolvedValueOnce({
734
+ ok: true,
735
+ json: () => Promise.resolve({ success: true }),
736
+ });
737
+ await client.ai.search.update('doc-1', 'Updated content', {
738
+ type: 'faq',
739
+ metadata: { priority: 'high' },
740
+ });
741
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
742
+ (0, vitest_1.expect)(body).toEqual({
743
+ id: 'doc-1',
744
+ content: 'Updated content',
745
+ type: 'faq',
746
+ metadata: { priority: 'high' },
747
+ });
748
+ });
749
+ });
750
+ (0, vitest_1.describe)('search.get', () => {
751
+ (0, vitest_1.it)('should POST to /ai/search/get with id', async () => {
752
+ mockFetch.mockResolvedValueOnce({
753
+ ok: true,
754
+ json: () => Promise.resolve({ result: { id: '1', content: 'text' }, exists: true }),
755
+ });
756
+ const result = await client.ai.search.get('doc-1');
757
+ (0, vitest_1.expect)(result.exists).toBe(true);
758
+ (0, vitest_1.expect)(result.result.content).toBe('text');
759
+ });
760
+ (0, vitest_1.it)('should return null when not found', async () => {
761
+ mockFetch.mockResolvedValueOnce({
762
+ ok: true,
763
+ json: () => Promise.resolve({ result: null, exists: false }),
764
+ });
765
+ const result = await client.ai.search.get('nonexistent');
766
+ (0, vitest_1.expect)(result.exists).toBe(false);
767
+ });
768
+ });
769
+ (0, vitest_1.describe)('search.count', () => {
770
+ (0, vitest_1.it)('should POST to /ai/search/count with optional type', async () => {
771
+ mockFetch.mockResolvedValueOnce({
772
+ ok: true,
773
+ json: () => Promise.resolve({ count: 42 }),
774
+ });
775
+ const result = await client.ai.search.count('faq');
776
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
777
+ (0, vitest_1.expect)(body).toEqual({ type: 'faq' });
778
+ (0, vitest_1.expect)(result.count).toBe(42);
779
+ });
780
+ (0, vitest_1.it)('should work without type param', async () => {
781
+ mockFetch.mockResolvedValueOnce({
782
+ ok: true,
783
+ json: () => Promise.resolve({ count: 100 }),
784
+ });
785
+ await client.ai.search.count();
786
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
787
+ (0, vitest_1.expect)(body).toEqual({ type: undefined });
788
+ });
789
+ });
790
+ });
791
+ // ─── Storage Namespace ────────────────────────────────────────
792
+ (0, vitest_1.describe)('storage namespace', () => {
793
+ (0, vitest_1.beforeEach)(() => {
794
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
795
+ });
796
+ (0, vitest_1.describe)('upload', () => {
797
+ (0, vitest_1.it)('should POST multipart form data to /storage/upload', async () => {
798
+ mockFetch.mockResolvedValueOnce({
799
+ ok: true,
800
+ json: () => Promise.resolve({ url: 'https://cdn.test.com/file.jpg' }),
801
+ });
802
+ const blob = new Blob(['file content'], { type: 'text/plain' });
803
+ const result = await client.storage.upload('docs/file.txt', blob, 'text/plain');
804
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/storage/upload');
805
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('POST');
806
+ // Body should be FormData (multipart)
807
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].body).toBeInstanceOf(FormData);
808
+ (0, vitest_1.expect)(result.url).toBe('https://cdn.test.com/file.jpg');
809
+ });
810
+ (0, vitest_1.it)('should not include Content-Type header for multipart uploads', async () => {
811
+ mockFetch.mockResolvedValueOnce({
812
+ ok: true,
813
+ json: () => Promise.resolve({ url: 'https://cdn.test.com/file.jpg' }),
814
+ });
815
+ const blob = new Blob(['content']);
816
+ await client.storage.upload('file.txt', blob);
817
+ const headers = mockFetch.mock.calls[0][1].headers;
818
+ (0, vitest_1.expect)(headers['Content-Type']).toBeUndefined();
819
+ });
820
+ });
821
+ (0, vitest_1.describe)('get', () => {
822
+ (0, vitest_1.it)('should return data and exists:true on success', async () => {
823
+ const mockResponse = {
824
+ ok: true,
825
+ status: 200,
826
+ };
827
+ mockFetch.mockResolvedValueOnce(mockResponse);
828
+ const result = await client.storage.get('file.txt');
829
+ (0, vitest_1.expect)(result.exists).toBe(true);
830
+ (0, vitest_1.expect)(result.data).toBe(mockResponse);
831
+ });
832
+ (0, vitest_1.it)('should return null data and exists:false on 404', async () => {
833
+ mockFetch.mockResolvedValueOnce({
834
+ ok: false,
835
+ status: 404,
836
+ });
837
+ const result = await client.storage.get('nonexistent.txt');
838
+ (0, vitest_1.expect)(result.exists).toBe(false);
839
+ (0, vitest_1.expect)(result.data).toBeNull();
840
+ });
841
+ (0, vitest_1.it)('should throw error on non-404 failure', async () => {
842
+ mockFetch.mockResolvedValueOnce({
843
+ ok: false,
844
+ status: 500,
845
+ statusText: 'Server Error',
846
+ json: () => Promise.resolve({ error: { message: 'Storage error' } }),
847
+ });
848
+ await (0, vitest_1.expect)(client.storage.get('file.txt')).rejects.toThrow('Storage error');
849
+ });
850
+ (0, vitest_1.it)('should throw fallback error when JSON parse fails on error response', async () => {
851
+ mockFetch.mockResolvedValueOnce({
852
+ ok: false,
853
+ status: 500,
854
+ statusText: 'Server Error',
855
+ json: () => Promise.reject(new Error('not json')),
856
+ });
857
+ await (0, vitest_1.expect)(client.storage.get('file.txt')).rejects.toThrow('Server Error');
858
+ });
859
+ });
860
+ (0, vitest_1.describe)('getUrl', () => {
861
+ (0, vitest_1.it)('should POST to /storage/getUrl with key', async () => {
862
+ mockFetch.mockResolvedValueOnce({
863
+ ok: true,
864
+ json: () => Promise.resolve({ url: 'https://cdn.test.com/signed/file.txt' }),
865
+ });
866
+ const result = await client.storage.getUrl('file.txt');
867
+ (0, vitest_1.expect)(result.url).toContain('signed');
868
+ });
869
+ });
870
+ (0, vitest_1.describe)('list', () => {
871
+ (0, vitest_1.it)('should POST to /storage/list with prefix and options', async () => {
872
+ mockFetch.mockResolvedValueOnce({
873
+ ok: true,
874
+ json: () => Promise.resolve({
875
+ objects: [{ key: 'img/1.jpg', size: 1024 }],
876
+ truncated: false,
877
+ }),
878
+ });
879
+ const result = await client.storage.list('img/', { limit: 50 });
880
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
881
+ (0, vitest_1.expect)(body).toEqual({ prefix: 'img/', limit: 50 });
882
+ (0, vitest_1.expect)(result.objects).toHaveLength(1);
883
+ });
884
+ });
885
+ (0, vitest_1.describe)('delete', () => {
886
+ (0, vitest_1.it)('should POST to /storage/delete with key', async () => {
887
+ mockFetch.mockResolvedValueOnce({
888
+ ok: true,
889
+ json: () => Promise.resolve({ success: true }),
890
+ });
891
+ const result = await client.storage.delete('file.txt');
892
+ (0, vitest_1.expect)(result.success).toBe(true);
893
+ });
894
+ });
895
+ (0, vitest_1.describe)('exists', () => {
896
+ (0, vitest_1.it)('should POST to /storage/exists and return exists boolean', async () => {
897
+ mockFetch.mockResolvedValueOnce({
898
+ ok: true,
899
+ json: () => Promise.resolve({ exists: true }),
900
+ });
901
+ const result = await client.storage.exists('file.txt');
902
+ (0, vitest_1.expect)(result.exists).toBe(true);
903
+ });
904
+ });
905
+ (0, vitest_1.describe)('getMetadata', () => {
906
+ (0, vitest_1.it)('should POST to /storage/getMetadata and return metadata', async () => {
907
+ mockFetch.mockResolvedValueOnce({
908
+ ok: true,
909
+ json: () => Promise.resolve({
910
+ exists: true,
911
+ size: 2048,
912
+ contentType: 'image/png',
913
+ etag: '"abc123"',
914
+ }),
915
+ });
916
+ const result = await client.storage.getMetadata('img.png');
917
+ (0, vitest_1.expect)(result.exists).toBe(true);
918
+ (0, vitest_1.expect)(result.size).toBe(2048);
919
+ (0, vitest_1.expect)(result.contentType).toBe('image/png');
920
+ });
921
+ });
922
+ (0, vitest_1.describe)('copy', () => {
923
+ (0, vitest_1.it)('should POST to /storage/copy with source and dest keys', async () => {
924
+ mockFetch.mockResolvedValueOnce({
925
+ ok: true,
926
+ json: () => Promise.resolve({ success: true, url: 'https://cdn.test.com/copy.txt' }),
927
+ });
928
+ const result = await client.storage.copy('orig.txt', 'copy.txt');
929
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
930
+ (0, vitest_1.expect)(body).toEqual({ sourceKey: 'orig.txt', destKey: 'copy.txt' });
931
+ (0, vitest_1.expect)(result.success).toBe(true);
932
+ });
933
+ });
934
+ (0, vitest_1.describe)('move', () => {
935
+ (0, vitest_1.it)('should POST to /storage/move with source and dest keys', async () => {
936
+ mockFetch.mockResolvedValueOnce({
937
+ ok: true,
938
+ json: () => Promise.resolve({ success: true, url: 'https://cdn.test.com/moved.txt' }),
939
+ });
940
+ const result = await client.storage.move('old.txt', 'new.txt');
941
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
942
+ (0, vitest_1.expect)(body).toEqual({ sourceKey: 'old.txt', destKey: 'new.txt' });
943
+ (0, vitest_1.expect)(result.success).toBe(true);
944
+ });
945
+ });
946
+ });
947
+ // ─── Services Namespace ───────────────────────────────────────
948
+ (0, vitest_1.describe)('services namespace', () => {
949
+ (0, vitest_1.beforeEach)(() => {
950
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
951
+ });
952
+ (0, vitest_1.it)('should POST to /services/invoke with service name and data', async () => {
953
+ mockFetch.mockResolvedValueOnce({
954
+ ok: true,
955
+ json: () => Promise.resolve({ success: true, result: { processed: true } }),
956
+ });
957
+ const result = await client.services.invoke('payment', { amount: 100 });
958
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
959
+ (0, vitest_1.expect)(body).toEqual({ serviceName: 'payment', data: { amount: 100 } });
960
+ (0, vitest_1.expect)(result.result.processed).toBe(true);
961
+ });
962
+ (0, vitest_1.it)('should work without data', async () => {
963
+ mockFetch.mockResolvedValueOnce({
964
+ ok: true,
965
+ json: () => Promise.resolve({ success: true, result: 'ok' }),
966
+ });
967
+ await client.services.invoke('health-check');
968
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
969
+ (0, vitest_1.expect)(body).toEqual({ serviceName: 'health-check', data: undefined });
970
+ });
971
+ });
972
+ // ─── Auth Namespace ───────────────────────────────────────────
973
+ (0, vitest_1.describe)('auth namespace', () => {
974
+ (0, vitest_1.beforeEach)(() => {
975
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
976
+ });
977
+ (0, vitest_1.describe)('signup', () => {
978
+ (0, vitest_1.it)('should POST to /auth/signup with email and password', async () => {
979
+ mockFetch.mockResolvedValueOnce({
980
+ ok: true,
981
+ json: () => Promise.resolve({
982
+ token: 'jwt-token',
983
+ user: { id: 'u1', email: 'test@test.com' },
984
+ }),
985
+ });
986
+ const result = await client.auth.signup({
987
+ email: 'test@test.com',
988
+ password: 'Password123',
989
+ });
990
+ (0, vitest_1.expect)(result.token).toBe('jwt-token');
991
+ (0, vitest_1.expect)(result.user.email).toBe('test@test.com');
992
+ });
993
+ (0, vitest_1.it)('should pass name and customFields when provided', async () => {
994
+ mockFetch.mockResolvedValueOnce({
995
+ ok: true,
996
+ json: () => Promise.resolve({ token: 't', user: {} }),
997
+ });
998
+ await client.auth.signup({
999
+ email: 'test@test.com',
1000
+ password: 'Password123',
1001
+ name: 'Test User',
1002
+ customFields: { role: 'admin' },
1003
+ });
1004
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
1005
+ (0, vitest_1.expect)(body.name).toBe('Test User');
1006
+ (0, vitest_1.expect)(body.customFields).toEqual({ role: 'admin' });
1007
+ });
1008
+ });
1009
+ (0, vitest_1.describe)('signin', () => {
1010
+ (0, vitest_1.it)('should POST to /auth/signin with email and password', async () => {
1011
+ mockFetch.mockResolvedValueOnce({
1012
+ ok: true,
1013
+ json: () => Promise.resolve({
1014
+ token: 'jwt-token',
1015
+ user: { id: 'u1', email: 'test@test.com' },
1016
+ }),
1017
+ });
1018
+ const result = await client.auth.signin({
1019
+ email: 'test@test.com',
1020
+ password: 'Password123',
1021
+ });
1022
+ (0, vitest_1.expect)(result.token).toBe('jwt-token');
1023
+ });
1024
+ });
1025
+ });
1026
+ // ─── Collections Namespace ────────────────────────────────────
1027
+ (0, vitest_1.describe)('collections namespace', () => {
1028
+ (0, vitest_1.beforeEach)(() => {
1029
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
1030
+ });
1031
+ (0, vitest_1.describe)('list', () => {
1032
+ (0, vitest_1.it)('should GET /collections/{slug}/items', async () => {
1033
+ mockFetch.mockResolvedValueOnce({
1034
+ ok: true,
1035
+ json: () => Promise.resolve([{ id: '1', slug: 'post-1' }]),
1036
+ });
1037
+ const result = await client.collections('blog-posts').list();
1038
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/collections/blog-posts/items');
1039
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('GET');
1040
+ (0, vitest_1.expect)(result).toHaveLength(1);
1041
+ });
1042
+ (0, vitest_1.it)('should pass status and limit options', async () => {
1043
+ mockFetch.mockResolvedValueOnce({
1044
+ ok: true,
1045
+ json: () => Promise.resolve([]),
1046
+ });
1047
+ await client.collections('blog').list({ status: 'published', limit: 5 });
1048
+ });
1049
+ });
1050
+ (0, vitest_1.describe)('get', () => {
1051
+ (0, vitest_1.it)('should GET /collections/{slug}/items/{itemId}', async () => {
1052
+ mockFetch.mockResolvedValueOnce({
1053
+ ok: true,
1054
+ json: () => Promise.resolve({ id: 'item-1', slug: 'test', data: { title: 'Hello' } }),
1055
+ });
1056
+ const result = await client.collections('posts').get('item-1');
1057
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/collections/posts/items/item-1');
1058
+ (0, vitest_1.expect)(result.data.title).toBe('Hello');
1059
+ });
1060
+ });
1061
+ (0, vitest_1.describe)('create', () => {
1062
+ (0, vitest_1.it)('should POST to /collections/{slug}/items', async () => {
1063
+ mockFetch.mockResolvedValueOnce({
1064
+ ok: true,
1065
+ json: () => Promise.resolve({ id: 'new-1', slug: 'new-post' }),
1066
+ });
1067
+ await client.collections('posts').create({
1068
+ slug: 'new-post',
1069
+ data: { title: 'New Post' },
1070
+ status: 'draft',
1071
+ });
1072
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('POST');
1073
+ });
1074
+ });
1075
+ (0, vitest_1.describe)('update', () => {
1076
+ (0, vitest_1.it)('should PUT to /collections/{slug}/items/{itemId}', async () => {
1077
+ mockFetch.mockResolvedValueOnce({
1078
+ ok: true,
1079
+ json: () => Promise.resolve({ success: true }),
1080
+ });
1081
+ await client.collections('posts').update('item-1', { data: { title: 'Updated' } });
1082
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('PUT');
1083
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toContain('/items/item-1');
1084
+ });
1085
+ });
1086
+ (0, vitest_1.describe)('delete', () => {
1087
+ (0, vitest_1.it)('should DELETE /collections/{slug}/items/{itemId}', async () => {
1088
+ mockFetch.mockResolvedValueOnce({
1089
+ ok: true,
1090
+ json: () => Promise.resolve({ success: true }),
1091
+ });
1092
+ await client.collections('posts').delete('item-1');
1093
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('DELETE');
1094
+ });
1095
+ });
1096
+ });
1097
+ // ─── Ecommerce Namespace ──────────────────────────────────────
1098
+ (0, vitest_1.describe)('ecommerce namespace', () => {
1099
+ (0, vitest_1.beforeEach)(() => {
1100
+ client = new client_1.AerostackClient({ baseUrl: 'https://api.test.com' });
1101
+ });
1102
+ (0, vitest_1.describe)('config', () => {
1103
+ (0, vitest_1.it)('should GET /ecommerce/config', async () => {
1104
+ mockFetch.mockResolvedValueOnce({
1105
+ ok: true,
1106
+ json: () => Promise.resolve({
1107
+ default_currency: 'USD',
1108
+ tax_percentage: 8.5,
1109
+ shipping_enabled: true,
1110
+ }),
1111
+ });
1112
+ const result = await client.ecommerce().config();
1113
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/ecommerce/config');
1114
+ (0, vitest_1.expect)(result.default_currency).toBe('USD');
1115
+ });
1116
+ });
1117
+ (0, vitest_1.describe)('products.list', () => {
1118
+ (0, vitest_1.it)('should GET /ecommerce/products', async () => {
1119
+ mockFetch.mockResolvedValueOnce({
1120
+ ok: true,
1121
+ json: () => Promise.resolve({
1122
+ products: [{ id: 'p1', name: 'Widget' }],
1123
+ total: 1,
1124
+ page: 1,
1125
+ limit: 20,
1126
+ }),
1127
+ });
1128
+ const result = await client.ecommerce().products.list();
1129
+ (0, vitest_1.expect)(result.products).toHaveLength(1);
1130
+ });
1131
+ });
1132
+ (0, vitest_1.describe)('products.get', () => {
1133
+ (0, vitest_1.it)('should GET /ecommerce/products/{slugOrId}', async () => {
1134
+ mockFetch.mockResolvedValueOnce({
1135
+ ok: true,
1136
+ json: () => Promise.resolve({
1137
+ product: { id: 'p1', name: 'Widget', variants: [] },
1138
+ }),
1139
+ });
1140
+ const result = await client.ecommerce().products.get('widget');
1141
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/ecommerce/products/widget');
1142
+ (0, vitest_1.expect)(result.product.name).toBe('Widget');
1143
+ });
1144
+ });
1145
+ (0, vitest_1.describe)('categories.list', () => {
1146
+ (0, vitest_1.it)('should GET /ecommerce/categories', async () => {
1147
+ mockFetch.mockResolvedValueOnce({
1148
+ ok: true,
1149
+ json: () => Promise.resolve({
1150
+ categories: [{ id: 'c1', name: 'Electronics', slug: 'electronics' }],
1151
+ total: 1,
1152
+ }),
1153
+ });
1154
+ const result = await client.ecommerce().categories.list();
1155
+ (0, vitest_1.expect)(result.categories).toHaveLength(1);
1156
+ });
1157
+ });
1158
+ (0, vitest_1.describe)('cart', () => {
1159
+ (0, vitest_1.it)('should GET /ecommerce/cart/{cartId}', async () => {
1160
+ mockFetch.mockResolvedValueOnce({
1161
+ ok: true,
1162
+ json: () => Promise.resolve({
1163
+ id: 'cart-1',
1164
+ items: [],
1165
+ }),
1166
+ });
1167
+ const result = await client.ecommerce().cart('cart-1').get();
1168
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/ecommerce/cart/cart-1');
1169
+ (0, vitest_1.expect)(result.id).toBe('cart-1');
1170
+ });
1171
+ (0, vitest_1.it)('should POST to /ecommerce/cart/{cartId}/items', async () => {
1172
+ mockFetch.mockResolvedValueOnce({
1173
+ ok: true,
1174
+ json: () => Promise.resolve({
1175
+ id: 'cart-1',
1176
+ items: [{ id: 'i1', product_id: 'p1', quantity: 2 }],
1177
+ }),
1178
+ });
1179
+ const result = await client.ecommerce().cart('cart-1').addItem({
1180
+ productId: 'p1',
1181
+ quantity: 2,
1182
+ });
1183
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/ecommerce/cart/cart-1/items');
1184
+ (0, vitest_1.expect)(result.items).toHaveLength(1);
1185
+ });
1186
+ });
1187
+ (0, vitest_1.describe)('applyCoupon', () => {
1188
+ (0, vitest_1.it)('should POST to /ecommerce/coupons/validate', async () => {
1189
+ mockFetch.mockResolvedValueOnce({
1190
+ ok: true,
1191
+ json: () => Promise.resolve({
1192
+ valid: true,
1193
+ discount_amount: 10,
1194
+ message: '10% off',
1195
+ }),
1196
+ });
1197
+ const result = await client.ecommerce().applyCoupon('SAVE10');
1198
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
1199
+ (0, vitest_1.expect)(body).toEqual({ code: 'SAVE10' });
1200
+ (0, vitest_1.expect)(result.valid).toBe(true);
1201
+ (0, vitest_1.expect)(result.discount_amount).toBe(10);
1202
+ });
1203
+ (0, vitest_1.it)('should handle invalid coupon', async () => {
1204
+ mockFetch.mockResolvedValueOnce({
1205
+ ok: true,
1206
+ json: () => Promise.resolve({
1207
+ valid: false,
1208
+ discount_amount: 0,
1209
+ message: 'Coupon expired',
1210
+ }),
1211
+ });
1212
+ const result = await client.ecommerce().applyCoupon('EXPIRED');
1213
+ (0, vitest_1.expect)(result.valid).toBe(false);
1214
+ });
1215
+ });
1216
+ });
1217
+ // ─── Request Body Serialization ───────────────────────────────
1218
+ (0, vitest_1.describe)('request body serialization', () => {
1219
+ (0, vitest_1.beforeEach)(() => {
1220
+ client = new client_1.AerostackClient({});
1221
+ });
1222
+ (0, vitest_1.it)('should JSON.stringify body for non-multipart requests', async () => {
1223
+ mockFetch.mockResolvedValueOnce({
1224
+ ok: true,
1225
+ json: () => Promise.resolve({ results: [] }),
1226
+ });
1227
+ await client.db.query('SELECT 1');
1228
+ (0, vitest_1.expect)(typeof mockFetch.mock.calls[0][1].body).toBe('string');
1229
+ });
1230
+ (0, vitest_1.it)('should not stringify body for multipart requests (storage.upload)', async () => {
1231
+ mockFetch.mockResolvedValueOnce({
1232
+ ok: true,
1233
+ json: () => Promise.resolve({ url: 'test' }),
1234
+ });
1235
+ const blob = new Blob(['test']);
1236
+ await client.storage.upload('key', blob);
1237
+ (0, vitest_1.expect)(mockFetch.mock.calls[0][1].body).toBeInstanceOf(FormData);
1238
+ });
1239
+ });
1240
+ });