Skip to content

Commit 48b4e17

Browse files
committed
feat(tests): add Hono and MSW integration tests for ObjectStackClient
1 parent f09f819 commit 48b4e17

4 files changed

Lines changed: 376 additions & 2 deletions

File tree

packages/client/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@
1010
"test": "vitest run"
1111
},
1212
"dependencies": {
13-
"@objectstack/spec": "workspace:*",
14-
"@objectstack/core": "workspace:*"
13+
"@objectstack/core": "workspace:*",
14+
"@objectstack/spec": "workspace:*"
1515
},
1616
"devDependencies": {
17+
"@hono/node-server": "^1.2.0",
18+
"@objectstack/driver-memory": "workspace:*",
19+
"@objectstack/hono": "workspace:*",
20+
"@objectstack/objectql": "workspace:*",
21+
"@objectstack/plugin-hono-server": "workspace:*",
22+
"@objectstack/plugin-msw": "workspace:*",
23+
"@objectstack/runtime": "workspace:*",
24+
"msw": "^2.0.0",
1725
"typescript": "^5.0.0",
1826
"vitest": "^4.0.18"
1927
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { LiteKernel } from '@objectstack/core';
3+
import { ObjectQL, ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
4+
import { InMemoryDriver } from '@objectstack/driver-memory';
5+
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
6+
import { ObjectStackClient } from './index';
7+
8+
describe('ObjectStackClient (with Hono Server)', () => {
9+
let baseUrl: string;
10+
let kernel: LiteKernel;
11+
12+
beforeAll(async () => {
13+
// 1. Setup Kernel
14+
kernel = new LiteKernel();
15+
kernel.use(new ObjectQLPlugin());
16+
17+
// 2. Setup Hono Plugin
18+
const honoPlugin = new HonoServerPlugin({
19+
port: 0,
20+
registerStandardEndpoints: true
21+
});
22+
kernel.use(honoPlugin);
23+
24+
// --- BROKER SHIM START ---
25+
// HttpDispatcher requires a broker to function. We inject a simple shim.
26+
(kernel as any).broker = {
27+
call: async (action: string, params: any, opts: any) => {
28+
const parts = action.split('.');
29+
const service = parts[0];
30+
const method = parts[1];
31+
32+
if (service === 'data') {
33+
const ql = kernel.getService<any>('objectql'); // Use 'objectql' service name for clarity
34+
if (method === 'create') {
35+
const res = await ql.insert(params.object, params.data);
36+
// Ensure we return the full object including input data + ID
37+
return { ...params.data, ...res };
38+
}
39+
// Params from HttpDispatcher: { object, id, ...query }
40+
if (method === 'get') {
41+
// Ensure we search by 'id' explicitly for InMemoryDriver
42+
return ql.findOne(params.object, { where: { id: params.id } });
43+
}
44+
// Params from HttpDispatcher: { object, filters }
45+
if (method === 'query') {
46+
// HttpDispatcher passes filters as simple object (map)
47+
// ObjectQL expects { filter, select, sort, ... }
48+
const data = await ql.find(params.object, { filter: params.filters });
49+
// HttpDispatcher expects { data, count }
50+
return { data, count: data.length };
51+
}
52+
if (method === 'find') return ql.find(params.object, { filter: params.filters });
53+
}
54+
55+
if (service === 'metadata') {
56+
// ObjectQLPlugin registers itself as 'metadata' but doesn't implement all broker methods directly
57+
// We use SchemaRegistry for lookups
58+
if (method === 'getObject') {
59+
return SchemaRegistry.getObject(params.objectName);
60+
}
61+
if (method === 'objects') {
62+
return SchemaRegistry.getAllObjects();
63+
}
64+
}
65+
66+
if (service === 'auth' && method === 'login') {
67+
return { token: 'mock-token', user: { id: 'admin', name: 'Admin' } };
68+
}
69+
70+
console.warn(`[BrokerShim] Action not implemented: ${action}`);
71+
throw new Error(`Action ${action} not implemented in shim`);
72+
}
73+
};
74+
// --- BROKER SHIM END ---
75+
76+
await kernel.bootstrap();
77+
78+
// 3. Setup Driver
79+
const ql = kernel.getService<ObjectQL>('objectql');
80+
ql.registerDriver(new InMemoryDriver(), true);
81+
82+
// 4. Load Metadata (Schema)
83+
SchemaRegistry.registerObject({
84+
name: 'customer',
85+
label: 'Customer',
86+
fields: {
87+
name: { type: 'text', label: 'Name' },
88+
email: { type: 'text', label: 'Email' }
89+
}
90+
});
91+
92+
// 5. Get Port from Service
93+
const httpServer = kernel.getService<any>('http.server');
94+
const port = httpServer.getPort();
95+
baseUrl = `http://localhost:${port}`;
96+
97+
console.log(`Test server running at ${baseUrl}`);
98+
});
99+
100+
afterAll(async () => {
101+
if (kernel) await kernel.shutdown();
102+
});
103+
104+
it('should connect to hono server', async () => {
105+
const client = new ObjectStackClient({ baseUrl });
106+
await client.connect();
107+
108+
// Client creates URL like ${baseUrl}/api/v1
109+
expect(client).toBeDefined();
110+
});
111+
112+
it('should create and retrieve data via hono', async () => {
113+
const client = new ObjectStackClient({ baseUrl });
114+
await client.connect();
115+
116+
// Create
117+
const createdResponse = await client.data.create('customer', {
118+
name: 'Hono User',
119+
email: 'hono@example.com'
120+
});
121+
122+
expect(createdResponse.success).toBe(true);
123+
expect(createdResponse.data.name).toBe('Hono User');
124+
expect(createdResponse.data.id).toBeDefined();
125+
126+
// Retrieve
127+
const retrievedResponse = await client.data.get('customer', createdResponse.data.id);
128+
expect(retrievedResponse.success).toBe(true);
129+
expect(retrievedResponse.data.name).toBe('Hono User');
130+
});
131+
132+
it('should find data via hono', async () => {
133+
const client = new ObjectStackClient({ baseUrl });
134+
await client.connect();
135+
136+
const resultsResponse = await client.data.find('customer', {
137+
where: { name: 'Hono User' }
138+
});
139+
140+
expect(resultsResponse.success).toBe(true);
141+
expect(resultsResponse.data.length).toBeGreaterThan(0);
142+
expect(resultsResponse.data[0].name).toBe('Hono User');
143+
});
144+
});
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
2+
import { setupServer } from 'msw/node';
3+
import { http, HttpResponse } from 'msw';
4+
import { LiteKernel } from '@objectstack/core';
5+
import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
6+
import { InMemoryDriver } from '@objectstack/driver-memory';
7+
import { MSWPlugin } from '@objectstack/plugin-msw';
8+
import { ObjectStackClient } from './index';
9+
10+
// 127.0.0.1 usage logic remains
11+
const BASE_URL = 'http://127.0.0.1';
12+
13+
describe('ObjectStackClient (with MSW Plugin)', () => {
14+
let kernel: LiteKernel;
15+
let mswPlugin: MSWPlugin;
16+
let server: any;
17+
18+
beforeAll(async () => {
19+
// 1. Setup Kernel
20+
kernel = new LiteKernel();
21+
kernel.use(new ObjectQLPlugin());
22+
23+
// 2. Setup MSW Plugin (headless mode)
24+
mswPlugin = new MSWPlugin({
25+
enableBrowser: false,
26+
// baseUrl: '/api/v1' // Default is /api/v1
27+
});
28+
kernel.use(mswPlugin);
29+
30+
// --- BROKER SHIM START ---
31+
// HttpDispatcher requires a broker to function. We inject a simple shim.
32+
(kernel as any).broker = {
33+
call: async (action: string, params: any, opts: any) => {
34+
const parts = action.split('.');
35+
const service = parts[0];
36+
const method = parts[1];
37+
38+
if (service === 'data') {
39+
const ql = kernel.getService<any>('objectql');
40+
if (method === 'create') {
41+
const res = await ql.insert(params.object, params.data);
42+
return { ...params.data, ...res };
43+
}
44+
if (method === 'get') {
45+
// Ensure we search by 'id' explicitly for InMemoryDriver
46+
return ql.findOne(params.object, { where: { id: params.id } });
47+
}
48+
if (method === 'query') {
49+
const data = await ql.find(params.object, { filter: params.filters });
50+
return { data, count: data.length };
51+
}
52+
if (method === 'find') return ql.find(params.object, { filter: params.filters });
53+
}
54+
55+
if (service === 'metadata') {
56+
if (method === 'getObject') return SchemaRegistry.getObject(params.objectName);
57+
if (method === 'objects') return SchemaRegistry.getAllObjects();
58+
}
59+
60+
console.warn(`[BrokerShim] Action not implemented: ${action}`);
61+
return null;
62+
}
63+
};
64+
// --- BROKER SHIM END ---
65+
66+
await kernel.bootstrap();
67+
68+
// 3. Setup Driver & Schema
69+
const ql = kernel.getService<any>('objectql');
70+
ql.registerDriver(new InMemoryDriver(), true);
71+
72+
SchemaRegistry.registerObject({
73+
name: 'customer',
74+
fields: {
75+
name: { type: 'text' }
76+
}
77+
});
78+
79+
// Add some data
80+
await ql.insert('customer', { id: '101', name: 'Alice' });
81+
await ql.insert('customer', { id: '102', name: 'Bob' });
82+
83+
// 4. Get handlers and start MSW Node Server
84+
const handlers = mswPlugin.getHandlers();
85+
86+
// Manual handlers to cover gaps in MSWPlugin generation
87+
handlers.push(
88+
http.get('http://127.0.0.1/api/v1', () => {
89+
return HttpResponse.json({
90+
name: 'ObjectOS',
91+
version: '1.0.0',
92+
routes: {
93+
data: '/api/v1/data',
94+
metadata: '/api/v1/metadata',
95+
auth: '/api/v1/auth'
96+
},
97+
capabilities: ['data', 'metadata'],
98+
features: {}
99+
});
100+
}),
101+
102+
http.get('http://127.0.0.1/api/v1/metadata/object/:name', async ({ params }) => {
103+
try {
104+
const res = await (kernel as any).broker.call('metadata.getObject', { objectName: params.name });
105+
return HttpResponse.json({ success: true, data: res });
106+
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 404 }); }
107+
}),
108+
109+
http.get('http://127.0.0.1/api/v1/data/:object', async ({ params }) => {
110+
try {
111+
// Simplifying: ignoring query filters for this test
112+
const res = await (kernel as any).broker.call('data.find', { object: params.object, filters: {} });
113+
return HttpResponse.json({ success: true, data: res });
114+
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 500 }); }
115+
}),
116+
117+
http.post('http://127.0.0.1/api/v1/data/:object', async ({ params, request }) => {
118+
try {
119+
const body = await request.json();
120+
const res = await (kernel as any).broker.call('data.create', { object: params.object, data: body });
121+
return HttpResponse.json({ success: true, data: res }, { status: 201 });
122+
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 500 }); }
123+
}),
124+
125+
http.get('http://127.0.0.1/api/v1/data/:object/:id', async ({ params }) => {
126+
try {
127+
const res = await (kernel as any).broker.call('data.get', { object: params.object, id: params.id });
128+
return HttpResponse.json({ success: true, data: res });
129+
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 404 }); }
130+
})
131+
);
132+
133+
server = setupServer(...handlers);
134+
server.listen({ onUnhandledRequest: 'error' });
135+
});
136+
137+
// Reset handlers after each test to ensure clean state
138+
afterEach(() => server.resetHandlers());
139+
140+
afterAll(async () => {
141+
if (server) server.close();
142+
if (kernel) await kernel.shutdown();
143+
});
144+
145+
it('should connect and discover endpoints', async () => {
146+
// MSWPlugin configured with baseUrl: '' creates handlers at root.
147+
// Client connects to /api/v1.
148+
// To make them match in THIS test file where I used baseUrl: '',
149+
// I should have configured MSWPlugin with baseUrl: '/api/v1' or left default.
150+
151+
// RE-FIXING SETUP in beforeAll (via edit).
152+
// If I change MSWPlugin config to default ('/api/v1'),
153+
// then Client(BASE_URL).connect() -> fetches BASE_URL/api/v1 -> matches MSW handler /api/v1.
154+
155+
const client = new ObjectStackClient({ baseUrl: BASE_URL });
156+
await client.connect();
157+
158+
expect(client['discoveryInfo']).toBeDefined();
159+
});
160+
161+
it('should fetch object metadata', async () => {
162+
const client = new ObjectStackClient({ baseUrl: BASE_URL });
163+
await client.connect();
164+
165+
const customerRes = await client.meta.getItem('object', 'customer');
166+
expect(customerRes.success).toBe(true);
167+
expect(customerRes.data.name).toBe('customer');
168+
});
169+
170+
it('should find data records', async () => {
171+
const client = new ObjectStackClient({ baseUrl: BASE_URL });
172+
await client.connect();
173+
174+
const resultsRes = await client.data.find('customer');
175+
expect(resultsRes.success).toBe(true);
176+
expect(resultsRes.data.length).toBe(2);
177+
expect(resultsRes.data[0].name).toBe('Alice');
178+
});
179+
180+
it('should get single record', async () => {
181+
const client = new ObjectStackClient({ baseUrl: BASE_URL });
182+
await client.connect();
183+
184+
const recordRes = await client.data.get('customer', '101');
185+
expect(recordRes.success).toBe(true);
186+
expect(recordRes.data.name).toBe('Alice');
187+
});
188+
189+
it('should create record', async () => {
190+
const client = new ObjectStackClient({ baseUrl: BASE_URL });
191+
await client.connect();
192+
193+
const newRecordRes = await client.data.create('customer', { name: 'Charlie' });
194+
expect(newRecordRes.success).toBe(true);
195+
expect(newRecordRes.data.name).toBe('Charlie');
196+
expect(newRecordRes.data.id).toBeDefined();
197+
});
198+
});

pnpm-lock.yaml

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)