@geekmidas/studio 9.0.2 → 10.0.0-alpha.1
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/package.json +8 -5
- package/CHANGELOG.md +0 -82
- package/src/Studio.ts +0 -367
- package/src/__tests__/Studio.spec.ts +0 -447
- package/src/data/DataBrowser.ts +0 -170
- package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
- package/src/data/__tests__/filtering.integration.spec.ts +0 -741
- package/src/data/__tests__/introspection.integration.spec.ts +0 -352
- package/src/data/__tests__/pagination.spec.ts +0 -123
- package/src/data/filtering.ts +0 -191
- package/src/data/index.ts +0 -1
- package/src/data/introspection.ts +0 -220
- package/src/data/pagination.ts +0 -33
- package/src/index.ts +0 -29
- package/src/server/__tests__/hono.integration.spec.ts +0 -619
- package/src/server/hono.ts +0 -427
- package/src/types.ts +0 -278
- package/src/ui-assets.ts +0 -37
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -13
- package/ui/CHANGELOG.md +0 -26
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -30
- package/ui/src/App.tsx +0 -100
- package/ui/src/api.ts +0 -213
- package/ui/src/components/FilterPanel.tsx +0 -213
- package/ui/src/components/NavRail.tsx +0 -183
- package/ui/src/components/RowDetail.tsx +0 -119
- package/ui/src/components/StudioHeader.tsx +0 -109
- package/ui/src/components/TableList.tsx +0 -58
- package/ui/src/components/TableView.tsx +0 -564
- package/ui/src/main.tsx +0 -10
- package/ui/src/pages/DashboardPage.tsx +0 -500
- package/ui/src/pages/DatabasePage.tsx +0 -226
- package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
- package/ui/src/pages/ExceptionsPage.tsx +0 -268
- package/ui/src/pages/LogsPage.tsx +0 -228
- package/ui/src/pages/MonitoringPage.tsx +0 -46
- package/ui/src/pages/PerformancePage.tsx +0 -307
- package/ui/src/pages/RequestsPage.tsx +0 -379
- package/ui/src/providers/StudioProvider.tsx +0 -194
- package/ui/src/styles.css +0 -105
- package/ui/src/types.ts +0 -174
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tsconfig.json +0 -21
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -12
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
import { InMemoryStorage } from '@geekmidas/telescope';
|
|
2
|
-
import {
|
|
3
|
-
CamelCasePlugin,
|
|
4
|
-
type Generated,
|
|
5
|
-
Kysely,
|
|
6
|
-
PostgresDialect,
|
|
7
|
-
sql,
|
|
8
|
-
} from 'kysely';
|
|
9
|
-
import pg from 'pg';
|
|
10
|
-
import {
|
|
11
|
-
afterAll,
|
|
12
|
-
afterEach,
|
|
13
|
-
beforeAll,
|
|
14
|
-
beforeEach,
|
|
15
|
-
describe,
|
|
16
|
-
expect,
|
|
17
|
-
it,
|
|
18
|
-
} from 'vitest';
|
|
19
|
-
import { TEST_DATABASE_CONFIG } from '../../../../testkit/test/globalSetup';
|
|
20
|
-
import { Studio } from '../../Studio';
|
|
21
|
-
import { Direction } from '../../types';
|
|
22
|
-
import { createStudioApp } from '../hono';
|
|
23
|
-
|
|
24
|
-
interface TestDatabase {
|
|
25
|
-
studioHonoProducts: {
|
|
26
|
-
id: Generated<number>;
|
|
27
|
-
name: string;
|
|
28
|
-
price: number;
|
|
29
|
-
category: string;
|
|
30
|
-
inStock: boolean;
|
|
31
|
-
createdAt: Generated<Date>;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('Hono Server Adapter Integration Tests', () => {
|
|
36
|
-
let db: Kysely<TestDatabase>;
|
|
37
|
-
let studio: Studio<TestDatabase>;
|
|
38
|
-
let storage: InMemoryStorage;
|
|
39
|
-
let app: ReturnType<typeof createStudioApp>;
|
|
40
|
-
|
|
41
|
-
beforeAll(async () => {
|
|
42
|
-
db = new Kysely<TestDatabase>({
|
|
43
|
-
dialect: new PostgresDialect({
|
|
44
|
-
pool: new pg.Pool({
|
|
45
|
-
...TEST_DATABASE_CONFIG,
|
|
46
|
-
database: 'postgres',
|
|
47
|
-
}),
|
|
48
|
-
}),
|
|
49
|
-
plugins: [new CamelCasePlugin()],
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Create test table
|
|
53
|
-
await db.schema
|
|
54
|
-
.createTable('studio_hono_products')
|
|
55
|
-
.ifNotExists()
|
|
56
|
-
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
57
|
-
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
58
|
-
.addColumn('price', 'numeric(10, 2)', (col) => col.notNull())
|
|
59
|
-
.addColumn('category', 'varchar(100)', (col) => col.notNull())
|
|
60
|
-
.addColumn('in_stock', 'boolean', (col) => col.notNull().defaultTo(true))
|
|
61
|
-
.addColumn('created_at', 'timestamptz', (col) =>
|
|
62
|
-
col.defaultTo(sql`now()`).notNull(),
|
|
63
|
-
)
|
|
64
|
-
.execute();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
beforeEach(async () => {
|
|
68
|
-
storage = new InMemoryStorage();
|
|
69
|
-
studio = new Studio({
|
|
70
|
-
monitoring: { storage },
|
|
71
|
-
data: {
|
|
72
|
-
db,
|
|
73
|
-
cursor: { field: 'id', direction: Direction.Asc },
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
app = createStudioApp(studio);
|
|
77
|
-
|
|
78
|
-
// Insert test data for database browsing
|
|
79
|
-
await db
|
|
80
|
-
.insertInto('studioHonoProducts')
|
|
81
|
-
.values([
|
|
82
|
-
{
|
|
83
|
-
name: 'Laptop',
|
|
84
|
-
price: 999.99,
|
|
85
|
-
category: 'electronics',
|
|
86
|
-
inStock: true,
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
name: 'Mouse',
|
|
90
|
-
price: 29.99,
|
|
91
|
-
category: 'electronics',
|
|
92
|
-
inStock: true,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: 'Keyboard',
|
|
96
|
-
price: 79.99,
|
|
97
|
-
category: 'electronics',
|
|
98
|
-
inStock: false,
|
|
99
|
-
},
|
|
100
|
-
{ name: 'Desk', price: 299.99, category: 'furniture', inStock: true },
|
|
101
|
-
{ name: 'Chair', price: 199.99, category: 'furniture', inStock: true },
|
|
102
|
-
])
|
|
103
|
-
.execute();
|
|
104
|
-
|
|
105
|
-
// Record some monitoring data
|
|
106
|
-
await studio.recordRequest({
|
|
107
|
-
method: 'GET',
|
|
108
|
-
path: '/api/users',
|
|
109
|
-
status: 200,
|
|
110
|
-
duration: 45,
|
|
111
|
-
});
|
|
112
|
-
await studio.recordRequest({
|
|
113
|
-
method: 'POST',
|
|
114
|
-
path: '/api/users',
|
|
115
|
-
status: 201,
|
|
116
|
-
duration: 80,
|
|
117
|
-
});
|
|
118
|
-
await studio.recordRequest({
|
|
119
|
-
method: 'GET',
|
|
120
|
-
path: '/api/products',
|
|
121
|
-
status: 500,
|
|
122
|
-
duration: 120,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await studio.info('Application started');
|
|
126
|
-
await studio.warn('High memory usage detected');
|
|
127
|
-
await studio.error('Database connection timeout');
|
|
128
|
-
|
|
129
|
-
await studio.exception(new Error('Test exception'));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
afterEach(async () => {
|
|
133
|
-
studio.destroy();
|
|
134
|
-
await db.deleteFrom('studioHonoProducts').execute();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
afterAll(async () => {
|
|
138
|
-
await db.schema.dropTable('studio_hono_products').ifExists().execute();
|
|
139
|
-
await db.destroy();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('GET /api/schema', () => {
|
|
143
|
-
it('should return database schema', async () => {
|
|
144
|
-
const res = await app.request('/api/schema');
|
|
145
|
-
|
|
146
|
-
expect(res.status).toBe(200);
|
|
147
|
-
const data = await res.json();
|
|
148
|
-
expect(data.tables).toBeDefined();
|
|
149
|
-
expect(Array.isArray(data.tables)).toBe(true);
|
|
150
|
-
expect(data.updatedAt).toBeDefined();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should force refresh when requested', async () => {
|
|
154
|
-
const res1 = await app.request('/api/schema');
|
|
155
|
-
const data1 = await res1.json();
|
|
156
|
-
|
|
157
|
-
const res2 = await app.request('/api/schema?refresh=true');
|
|
158
|
-
const data2 = await res2.json();
|
|
159
|
-
|
|
160
|
-
// Both should have tables
|
|
161
|
-
expect(data1.tables).toBeDefined();
|
|
162
|
-
expect(data2.tables).toBeDefined();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('GET /api/tables', () => {
|
|
167
|
-
it('should return list of tables', async () => {
|
|
168
|
-
const res = await app.request('/api/tables');
|
|
169
|
-
|
|
170
|
-
expect(res.status).toBe(200);
|
|
171
|
-
const data = await res.json();
|
|
172
|
-
expect(data.tables).toBeDefined();
|
|
173
|
-
|
|
174
|
-
const productTable = data.tables.find(
|
|
175
|
-
(t: any) => t.name === 'studio_hono_products',
|
|
176
|
-
);
|
|
177
|
-
expect(productTable).toBeDefined();
|
|
178
|
-
expect(productTable.columnCount).toBeGreaterThan(0);
|
|
179
|
-
expect(productTable.primaryKey).toEqual(['id']);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('GET /api/tables/:name', () => {
|
|
184
|
-
it('should return table info for existing table', async () => {
|
|
185
|
-
const res = await app.request('/api/tables/studio_hono_products');
|
|
186
|
-
|
|
187
|
-
expect(res.status).toBe(200);
|
|
188
|
-
const data = await res.json();
|
|
189
|
-
expect(data.name).toBe('studio_hono_products');
|
|
190
|
-
expect(data.columns).toBeDefined();
|
|
191
|
-
expect(data.columns.length).toBeGreaterThan(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should return 404 for non-existent table', async () => {
|
|
195
|
-
const res = await app.request('/api/tables/non_existent_table');
|
|
196
|
-
|
|
197
|
-
expect(res.status).toBe(404);
|
|
198
|
-
const data = await res.json();
|
|
199
|
-
expect(data.error).toContain('not found');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe('GET /api/tables/:name/rows', () => {
|
|
204
|
-
it('should return paginated rows', async () => {
|
|
205
|
-
const res = await app.request('/api/tables/studio_hono_products/rows');
|
|
206
|
-
|
|
207
|
-
expect(res.status).toBe(200);
|
|
208
|
-
const data = await res.json();
|
|
209
|
-
expect(data.rows).toBeDefined();
|
|
210
|
-
expect(data.rows.length).toBe(5);
|
|
211
|
-
expect(data.hasMore).toBe(false);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('should respect pageSize parameter', async () => {
|
|
215
|
-
const res = await app.request(
|
|
216
|
-
'/api/tables/studio_hono_products/rows?pageSize=2',
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
expect(res.status).toBe(200);
|
|
220
|
-
const data = await res.json();
|
|
221
|
-
expect(data.rows.length).toBe(2);
|
|
222
|
-
expect(data.hasMore).toBe(true);
|
|
223
|
-
expect(data.nextCursor).toBeDefined();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('should paginate using cursor', async () => {
|
|
227
|
-
const res1 = await app.request(
|
|
228
|
-
'/api/tables/studio_hono_products/rows?pageSize=2',
|
|
229
|
-
);
|
|
230
|
-
const data1 = await res1.json();
|
|
231
|
-
|
|
232
|
-
const res2 = await app.request(
|
|
233
|
-
`/api/tables/studio_hono_products/rows?pageSize=2&cursor=${data1.nextCursor}`,
|
|
234
|
-
);
|
|
235
|
-
const data2 = await res2.json();
|
|
236
|
-
|
|
237
|
-
expect(data2.rows.length).toBe(2);
|
|
238
|
-
|
|
239
|
-
// Rows should be different
|
|
240
|
-
const ids1 = data1.rows.map((r: any) => r.id);
|
|
241
|
-
const ids2 = data2.rows.map((r: any) => r.id);
|
|
242
|
-
expect(ids1.every((id: number) => !ids2.includes(id))).toBe(true);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should apply equality filter', async () => {
|
|
246
|
-
const res = await app.request(
|
|
247
|
-
'/api/tables/studio_hono_products/rows?filter[category][eq]=electronics',
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
expect(res.status).toBe(200);
|
|
251
|
-
const data = await res.json();
|
|
252
|
-
expect(data.rows.length).toBe(3);
|
|
253
|
-
data.rows.forEach((row: any) => {
|
|
254
|
-
expect(row.category).toBe('electronics');
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should apply greater-than filter', async () => {
|
|
259
|
-
const res = await app.request(
|
|
260
|
-
'/api/tables/studio_hono_products/rows?filter[price][gt]=100',
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
expect(res.status).toBe(200);
|
|
264
|
-
const data = await res.json();
|
|
265
|
-
expect(data.rows.length).toBe(3);
|
|
266
|
-
data.rows.forEach((row: any) => {
|
|
267
|
-
expect(Number(row.price)).toBeGreaterThan(100);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('should apply boolean filter', async () => {
|
|
272
|
-
const res = await app.request(
|
|
273
|
-
'/api/tables/studio_hono_products/rows?filter[in_stock][eq]=false',
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
expect(res.status).toBe(200);
|
|
277
|
-
const data = await res.json();
|
|
278
|
-
expect(data.rows.length).toBe(1);
|
|
279
|
-
expect(data.rows[0].name).toBe('Keyboard');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should apply IN filter', async () => {
|
|
283
|
-
const res = await app.request(
|
|
284
|
-
'/api/tables/studio_hono_products/rows?filter[name][in]=Laptop,Mouse',
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
expect(res.status).toBe(200);
|
|
288
|
-
const data = await res.json();
|
|
289
|
-
expect(data.rows.length).toBe(2);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('should apply multiple filters', async () => {
|
|
293
|
-
const res = await app.request(
|
|
294
|
-
'/api/tables/studio_hono_products/rows?filter[category][eq]=electronics&filter[in_stock][eq]=true',
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
expect(res.status).toBe(200);
|
|
298
|
-
const data = await res.json();
|
|
299
|
-
expect(data.rows.length).toBe(2);
|
|
300
|
-
data.rows.forEach((row: any) => {
|
|
301
|
-
expect(row.category).toBe('electronics');
|
|
302
|
-
expect(row.inStock).toBe(true);
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('should apply sorting', async () => {
|
|
307
|
-
const res = await app.request(
|
|
308
|
-
'/api/tables/studio_hono_products/rows?sort=price:desc',
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
expect(res.status).toBe(200);
|
|
312
|
-
const data = await res.json();
|
|
313
|
-
const prices = data.rows.map((r: any) => Number(r.price));
|
|
314
|
-
|
|
315
|
-
for (let i = 1; i < prices.length; i++) {
|
|
316
|
-
expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should apply multiple sort columns', async () => {
|
|
321
|
-
const res = await app.request(
|
|
322
|
-
'/api/tables/studio_hono_products/rows?sort=category:asc,price:desc',
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
expect(res.status).toBe(200);
|
|
326
|
-
const data = await res.json();
|
|
327
|
-
expect(data.rows.length).toBe(5);
|
|
328
|
-
|
|
329
|
-
// Electronics should come first (alphabetically)
|
|
330
|
-
const electronics = data.rows.filter(
|
|
331
|
-
(r: any) => r.category === 'electronics',
|
|
332
|
-
);
|
|
333
|
-
expect(electronics.length).toBe(3);
|
|
334
|
-
|
|
335
|
-
// Within electronics, prices should be descending
|
|
336
|
-
const electronicPrices = electronics.map((r: any) => Number(r.price));
|
|
337
|
-
for (let i = 1; i < electronicPrices.length; i++) {
|
|
338
|
-
expect(electronicPrices[i]).toBeLessThanOrEqual(
|
|
339
|
-
electronicPrices[i - 1],
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('should return 404 for non-existent table', async () => {
|
|
345
|
-
const res = await app.request('/api/tables/non_existent_table/rows');
|
|
346
|
-
|
|
347
|
-
expect(res.status).toBe(404);
|
|
348
|
-
const data = await res.json();
|
|
349
|
-
expect(data.error).toContain('not found');
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should cap pageSize at 100', async () => {
|
|
353
|
-
const res = await app.request(
|
|
354
|
-
'/api/tables/studio_hono_products/rows?pageSize=500',
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
expect(res.status).toBe(200);
|
|
358
|
-
// Since we only have 5 rows, we just verify the request succeeds
|
|
359
|
-
const data = await res.json();
|
|
360
|
-
expect(data.rows).toBeDefined();
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// ============================================
|
|
365
|
-
// Monitoring API Tests
|
|
366
|
-
// ============================================
|
|
367
|
-
|
|
368
|
-
describe('GET /api/stats', () => {
|
|
369
|
-
it('should return storage statistics', async () => {
|
|
370
|
-
const res = await app.request('/api/stats');
|
|
371
|
-
|
|
372
|
-
expect(res.status).toBe(200);
|
|
373
|
-
const data = await res.json();
|
|
374
|
-
expect(data.requests).toBe(3);
|
|
375
|
-
expect(data.exceptions).toBe(1);
|
|
376
|
-
expect(data.logs).toBe(3);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
describe('GET /api/requests', () => {
|
|
381
|
-
it('should return request entries', async () => {
|
|
382
|
-
const res = await app.request('/api/requests');
|
|
383
|
-
|
|
384
|
-
expect(res.status).toBe(200);
|
|
385
|
-
const data = await res.json();
|
|
386
|
-
expect(Array.isArray(data)).toBe(true);
|
|
387
|
-
expect(data.length).toBe(3);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('should accept query parameters', async () => {
|
|
391
|
-
const res = await app.request('/api/requests?limit=2');
|
|
392
|
-
|
|
393
|
-
expect(res.status).toBe(200);
|
|
394
|
-
const data = await res.json();
|
|
395
|
-
expect(Array.isArray(data)).toBe(true);
|
|
396
|
-
expect(data.length).toBe(2);
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
describe('GET /api/requests/:id', () => {
|
|
401
|
-
it('should return a single request by ID', async () => {
|
|
402
|
-
const requests = await studio.getRequests();
|
|
403
|
-
const requestId = requests[0].id;
|
|
404
|
-
|
|
405
|
-
const res = await app.request(`/api/requests/${requestId}`);
|
|
406
|
-
|
|
407
|
-
expect(res.status).toBe(200);
|
|
408
|
-
const data = await res.json();
|
|
409
|
-
expect(data.id).toBe(requestId);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('should return 404 for non-existent request', async () => {
|
|
413
|
-
const res = await app.request('/api/requests/non-existent');
|
|
414
|
-
|
|
415
|
-
expect(res.status).toBe(404);
|
|
416
|
-
const data = await res.json();
|
|
417
|
-
expect(data.error).toBe('Request not found');
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
describe('GET /api/exceptions', () => {
|
|
422
|
-
it('should return exception entries', async () => {
|
|
423
|
-
const res = await app.request('/api/exceptions');
|
|
424
|
-
|
|
425
|
-
expect(res.status).toBe(200);
|
|
426
|
-
const data = await res.json();
|
|
427
|
-
expect(Array.isArray(data)).toBe(true);
|
|
428
|
-
expect(data.length).toBe(1);
|
|
429
|
-
expect(data[0].message).toBe('Test exception');
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
describe('GET /api/exceptions/:id', () => {
|
|
434
|
-
it('should return a single exception by ID', async () => {
|
|
435
|
-
const exceptions = await studio.getExceptions();
|
|
436
|
-
const exceptionId = exceptions[0].id;
|
|
437
|
-
|
|
438
|
-
const res = await app.request(`/api/exceptions/${exceptionId}`);
|
|
439
|
-
|
|
440
|
-
expect(res.status).toBe(200);
|
|
441
|
-
const data = await res.json();
|
|
442
|
-
expect(data.id).toBe(exceptionId);
|
|
443
|
-
expect(data.message).toBe('Test exception');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('should return 404 for non-existent exception', async () => {
|
|
447
|
-
const res = await app.request('/api/exceptions/non-existent');
|
|
448
|
-
|
|
449
|
-
expect(res.status).toBe(404);
|
|
450
|
-
const data = await res.json();
|
|
451
|
-
expect(data.error).toBe('Exception not found');
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
describe('GET /api/logs', () => {
|
|
456
|
-
it('should return log entries', async () => {
|
|
457
|
-
const res = await app.request('/api/logs');
|
|
458
|
-
|
|
459
|
-
expect(res.status).toBe(200);
|
|
460
|
-
const data = await res.json();
|
|
461
|
-
expect(Array.isArray(data)).toBe(true);
|
|
462
|
-
expect(data.length).toBe(3);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('should accept level filter', async () => {
|
|
466
|
-
const res = await app.request('/api/logs?level=error');
|
|
467
|
-
|
|
468
|
-
expect(res.status).toBe(200);
|
|
469
|
-
const data = await res.json();
|
|
470
|
-
expect(Array.isArray(data)).toBe(true);
|
|
471
|
-
expect(data.length).toBe(1);
|
|
472
|
-
expect(data[0].message).toBe('Database connection timeout');
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// ============================================
|
|
477
|
-
// Metrics API Tests
|
|
478
|
-
// ============================================
|
|
479
|
-
|
|
480
|
-
describe('GET /api/metrics', () => {
|
|
481
|
-
it('should return aggregated metrics', async () => {
|
|
482
|
-
const res = await app.request('/api/metrics');
|
|
483
|
-
|
|
484
|
-
expect(res.status).toBe(200);
|
|
485
|
-
const data = await res.json();
|
|
486
|
-
expect(data.totalRequests).toBe(3);
|
|
487
|
-
expect(typeof data.avgDuration).toBe('number');
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it('should accept time range parameters', async () => {
|
|
491
|
-
const start = new Date(Date.now() - 3600000).toISOString();
|
|
492
|
-
const end = new Date().toISOString();
|
|
493
|
-
const res = await app.request(`/api/metrics?start=${start}&end=${end}`);
|
|
494
|
-
|
|
495
|
-
expect(res.status).toBe(200);
|
|
496
|
-
const data = await res.json();
|
|
497
|
-
expect(data.totalRequests).toBeDefined();
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
describe('GET /api/metrics/endpoints', () => {
|
|
502
|
-
it('should return endpoint metrics', async () => {
|
|
503
|
-
const res = await app.request('/api/metrics/endpoints');
|
|
504
|
-
|
|
505
|
-
expect(res.status).toBe(200);
|
|
506
|
-
const data = await res.json();
|
|
507
|
-
expect(Array.isArray(data)).toBe(true);
|
|
508
|
-
expect(data.length).toBeGreaterThan(0);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it('should accept limit parameter', async () => {
|
|
512
|
-
const res = await app.request('/api/metrics/endpoints?limit=1');
|
|
513
|
-
|
|
514
|
-
expect(res.status).toBe(200);
|
|
515
|
-
const data = await res.json();
|
|
516
|
-
expect(Array.isArray(data)).toBe(true);
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
describe('GET /api/metrics/endpoint', () => {
|
|
521
|
-
it('should return details for a specific endpoint', async () => {
|
|
522
|
-
const res = await app.request(
|
|
523
|
-
'/api/metrics/endpoint?method=GET&path=/api/users',
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
// May return 200 with data or 404 if endpoint not tracked
|
|
527
|
-
expect([200, 404]).toContain(res.status);
|
|
528
|
-
if (res.status === 200) {
|
|
529
|
-
const data = await res.json();
|
|
530
|
-
expect(data.method).toBe('GET');
|
|
531
|
-
expect(data.path).toBe('/api/users');
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it('should return 400 if method is missing', async () => {
|
|
536
|
-
const res = await app.request('/api/metrics/endpoint?path=/api/users');
|
|
537
|
-
|
|
538
|
-
expect(res.status).toBe(400);
|
|
539
|
-
const data = await res.json();
|
|
540
|
-
expect(data.error).toBe('method and path are required');
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it('should return 400 if path is missing', async () => {
|
|
544
|
-
const res = await app.request('/api/metrics/endpoint?method=GET');
|
|
545
|
-
|
|
546
|
-
expect(res.status).toBe(400);
|
|
547
|
-
const data = await res.json();
|
|
548
|
-
expect(data.error).toBe('method and path are required');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('should return 404 for non-existent endpoint', async () => {
|
|
552
|
-
const res = await app.request(
|
|
553
|
-
'/api/metrics/endpoint?method=DELETE&path=/api/unknown',
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
expect(res.status).toBe(404);
|
|
557
|
-
const data = await res.json();
|
|
558
|
-
expect(data.error).toBe('Endpoint not found');
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
describe('GET /api/metrics/status', () => {
|
|
563
|
-
it('should return status distribution', async () => {
|
|
564
|
-
const res = await app.request('/api/metrics/status');
|
|
565
|
-
|
|
566
|
-
expect(res.status).toBe(200);
|
|
567
|
-
const data = await res.json();
|
|
568
|
-
expect(data).toBeDefined();
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
describe('DELETE /api/metrics', () => {
|
|
573
|
-
it('should reset metrics', async () => {
|
|
574
|
-
const res = await app.request('/api/metrics', { method: 'DELETE' });
|
|
575
|
-
|
|
576
|
-
expect(res.status).toBe(200);
|
|
577
|
-
const data = await res.json();
|
|
578
|
-
expect(data.success).toBe(true);
|
|
579
|
-
|
|
580
|
-
// Verify metrics are reset
|
|
581
|
-
const metricsRes = await app.request('/api/metrics');
|
|
582
|
-
const metrics = await metricsRes.json();
|
|
583
|
-
expect(metrics.totalRequests).toBe(0);
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// ============================================
|
|
588
|
-
// UI & Static Assets
|
|
589
|
-
// ============================================
|
|
590
|
-
|
|
591
|
-
describe('GET /', () => {
|
|
592
|
-
it('should return dashboard UI or API info', async () => {
|
|
593
|
-
const res = await app.request('/');
|
|
594
|
-
|
|
595
|
-
expect(res.status).toBe(200);
|
|
596
|
-
const contentType = res.headers.get('content-type') || '';
|
|
597
|
-
|
|
598
|
-
// If UI is embedded, returns HTML; otherwise returns JSON API info
|
|
599
|
-
if (contentType.includes('text/html')) {
|
|
600
|
-
const html = await res.text();
|
|
601
|
-
expect(html).toContain('<!doctype html>');
|
|
602
|
-
} else {
|
|
603
|
-
const data = await res.json();
|
|
604
|
-
expect(data.message).toBe('Studio API is running');
|
|
605
|
-
expect(data.endpoints).toBeDefined();
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
describe('API 404 handling', () => {
|
|
611
|
-
it('should return JSON 404 for unknown API routes', async () => {
|
|
612
|
-
const res = await app.request('/api/unknown-route');
|
|
613
|
-
|
|
614
|
-
expect(res.status).toBe(404);
|
|
615
|
-
const data = await res.json();
|
|
616
|
-
expect(data.error).toBe('Not found');
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
});
|