Skip to content

Commit 4629810

Browse files
authored
test(proxy): add ut for proxy (#2432)
1 parent f58550e commit 4629810

File tree

2 files changed

+177
-1
lines changed

2 files changed

+177
-1
lines changed

packages/cli/src/actions/proxy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ async function createDialect(provider: string, databaseUrl: string, outputPath:
198198
}
199199
}
200200

201-
function startServer(client: ClientContract<any, any>, schema: any, options: Options) {
201+
export function createProxyApp(client: ClientContract<any, any>, schema: any): express.Application {
202202
const app = express();
203203
app.use(cors());
204204
app.use(express.json({ limit: '5mb' }));
@@ -216,6 +216,12 @@ function startServer(client: ClientContract<any, any>, schema: any, options: Opt
216216
res.json({ ...schema, zenstackVersion: getVersion() });
217217
});
218218

219+
return app;
220+
}
221+
222+
function startServer(client: ClientContract<any, any>, schema: any, options: Options) {
223+
const app = createProxyApp(client, schema);
224+
219225
const server = app.listen(options.port, () => {
220226
console.log(`ZenStack proxy server is running on port: ${options.port}`);
221227
console.log(`You can visit ZenStack Studio at: ${colors.blue('https://studio.zenstack.dev')}`);

packages/cli/test/proxy.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import http from 'node:http';
3+
import { afterEach, describe, expect, it } from 'vitest';
4+
import { createProxyApp } from '../src/actions/proxy';
5+
6+
describe('CLI proxy tests', () => {
7+
let server: http.Server | undefined;
8+
9+
afterEach(async () => {
10+
await new Promise<void>((resolve) => {
11+
if (server) {
12+
server.close(() => resolve());
13+
server = undefined;
14+
} else {
15+
resolve();
16+
}
17+
});
18+
});
19+
20+
async function startAt(app: ReturnType<typeof createProxyApp>): Promise<string> {
21+
return new Promise((resolve) => {
22+
server = app.listen(0, () => {
23+
const addr = server!.address() as { port: number };
24+
resolve(`http://localhost:${addr.port}`);
25+
});
26+
});
27+
}
28+
29+
it('should serve schema at /api/schema endpoint', async () => {
30+
const zmodel = `
31+
model User {
32+
id String @id @default(cuid())
33+
email String @unique
34+
}
35+
`;
36+
37+
const client = await createTestClient(zmodel);
38+
const app = createProxyApp(client, client.$schema);
39+
const baseUrl = await startAt(app);
40+
41+
const r = await fetch(`${baseUrl}/api/schema`);
42+
expect(r.status).toBe(200);
43+
44+
const body = await r.json();
45+
// schema fields are present
46+
expect(body).toHaveProperty('models');
47+
expect(body.models).toHaveProperty('User');
48+
expect(body).toHaveProperty('provider');
49+
// zenstackVersion is injected by the proxy; when running tests directly
50+
// from source (no built dist/) getVersion() returns undefined and the
51+
// key is omitted from JSON — tolerate that, but if present it must be a string.
52+
if ('zenstackVersion' in body) {
53+
expect(typeof body.zenstackVersion).toBe('string');
54+
}
55+
});
56+
57+
it('should omit computed fields from default query responses', async () => {
58+
// postCount is a @computed field — the proxy must not try to SELECT it
59+
// by default (it has no backing column in the DB).
60+
const zmodel = `
61+
model User {
62+
id String @id @default(cuid())
63+
name String
64+
postCount Int @computed
65+
}
66+
`;
67+
68+
// Mirror what proxy.ts does: build omit config from the schema, then
69+
// create the client with skipValidationForComputedFields.
70+
const client = await createTestClient(zmodel, {
71+
skipValidationForComputedFields: true,
72+
omit: { User: { postCount: true } },
73+
});
74+
75+
const app = createProxyApp(client, client.$schema);
76+
const baseUrl = await startAt(app);
77+
78+
// Create a user via the proxy API.
79+
const createRes = await fetch(`${baseUrl}/api/model/user/create`, {
80+
method: 'POST',
81+
headers: { 'Content-Type': 'application/json' },
82+
body: JSON.stringify({ data: { name: 'Alice' } }),
83+
});
84+
expect(createRes.status).toBe(201);
85+
const created = await createRes.json();
86+
87+
// The regular fields should be present …
88+
expect(created.data).toHaveProperty('id');
89+
expect(created.data).toHaveProperty('name', 'Alice');
90+
// … but the computed field must be absent in the default response.
91+
expect(created.data).not.toHaveProperty('postCount');
92+
93+
// A findMany should behave the same way.
94+
const listRes = await fetch(`${baseUrl}/api/model/user/findMany`);
95+
expect(listRes.status).toBe(200);
96+
const list = await listRes.json();
97+
expect(list.data).toHaveLength(1);
98+
expect(list.data[0]).not.toHaveProperty('postCount');
99+
});
100+
101+
it('should handle sequential transaction calls', async () => {
102+
const zmodel = `
103+
model User {
104+
id String @id @default(cuid())
105+
email String @unique
106+
posts Post[]
107+
108+
@@allow('all', true)
109+
}
110+
111+
model Post {
112+
id String @id @default(cuid())
113+
title String
114+
author User? @relation(fields: [authorId], references: [id])
115+
authorId String?
116+
117+
@@allow('all', true)
118+
}
119+
`;
120+
121+
const client = await createTestClient(zmodel);
122+
const app = createProxyApp(client, client.$schema);
123+
const baseUrl = await startAt(app);
124+
125+
const txRes = await fetch(`${baseUrl}/api/model/$transaction/sequential`, {
126+
method: 'POST',
127+
headers: { 'Content-Type': 'application/json' },
128+
body: JSON.stringify([
129+
{
130+
model: 'User',
131+
op: 'create',
132+
args: { data: { id: 'u1', email: 'alice@example.com' } },
133+
},
134+
{
135+
model: 'Post',
136+
op: 'create',
137+
args: { data: { id: 'p1', title: 'Hello World', authorId: 'u1' } },
138+
},
139+
{
140+
model: 'Post',
141+
op: 'findMany',
142+
args: { where: { authorId: 'u1' } },
143+
},
144+
]),
145+
});
146+
expect(txRes.status).toBe(200);
147+
const tx = await txRes.json();
148+
149+
// Should return results for each operation in the transaction.
150+
expect(Array.isArray(tx.data)).toBe(true);
151+
expect(tx.data).toHaveLength(3);
152+
153+
// First result: created user
154+
expect(tx.data[0]).toMatchObject({ id: 'u1', email: 'alice@example.com' });
155+
// Second result: created post
156+
expect(tx.data[1]).toMatchObject({ id: 'p1', title: 'Hello World', authorId: 'u1' });
157+
// Third result: findMany — should find the newly created post
158+
expect(Array.isArray(tx.data[2])).toBe(true);
159+
expect(tx.data[2]).toHaveLength(1);
160+
expect(tx.data[2][0]).toMatchObject({ id: 'p1', title: 'Hello World' });
161+
162+
// Confirm persisted outside transaction too.
163+
const userRes = await fetch(
164+
`${baseUrl}/api/model/user/findUnique?q=${encodeURIComponent(JSON.stringify({ where: { id: 'u1' } }))}`,
165+
);
166+
expect(userRes.status).toBe(200);
167+
const user = await userRes.json();
168+
expect(user.data).toMatchObject({ id: 'u1', email: 'alice@example.com' });
169+
});
170+
});

0 commit comments

Comments
 (0)