@glubean/cli 0.1.2
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/bin/gb.js +2 -0
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +842 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +75 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/patch.d.ts +8 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +73 -0
- package/dist/commands/patch.js.map +1 -0
- package/dist/commands/run.d.ts +26 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +1093 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +6 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +62 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/spec_split.d.ts +5 -0
- package/dist/commands/spec_split.d.ts.map +1 -0
- package/dist/commands/spec_split.js +56 -0
- package/dist/commands/spec_split.js.map +1 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +252 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/trigger.d.ts +13 -0
- package/dist/commands/trigger.d.ts.map +1 -0
- package/dist/commands/trigger.js +213 -0
- package/dist/commands/trigger.js.map +1 -0
- package/dist/commands/validate_metadata.d.ts +6 -0
- package/dist/commands/validate_metadata.d.ts.map +1 -0
- package/dist/commands/validate_metadata.js +103 -0
- package/dist/commands/validate_metadata.js.map +1 -0
- package/dist/commands/worker.d.ts +14 -0
- package/dist/commands/worker.d.ts.map +1 -0
- package/dist/commands/worker.js +10 -0
- package/dist/commands/worker.js.map +1 -0
- package/dist/lib/auth.d.ts +39 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +82 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/ci.d.ts +12 -0
- package/dist/lib/ci.d.ts.map +1 -0
- package/dist/lib/ci.js +42 -0
- package/dist/lib/ci.js.map +1 -0
- package/dist/lib/config.d.ts +116 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +264 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +6 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/env.d.ts +19 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +40 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/git.d.ts +8 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +68 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/openapi_patch.d.ts +23 -0
- package/dist/lib/openapi_patch.d.ts.map +1 -0
- package/dist/lib/openapi_patch.js +232 -0
- package/dist/lib/openapi_patch.js.map +1 -0
- package/dist/lib/openapi_split.d.ts +16 -0
- package/dist/lib/openapi_split.d.ts.map +1 -0
- package/dist/lib/openapi_split.js +188 -0
- package/dist/lib/openapi_split.js.map +1 -0
- package/dist/lib/upload.d.ts +44 -0
- package/dist/lib/upload.d.ts.map +1 -0
- package/dist/lib/upload.js +297 -0
- package/dist/lib/upload.js.map +1 -0
- package/dist/main.d.ts +8 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +319 -0
- package/dist/main.js.map +1 -0
- package/dist/metadata.d.ts +17 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +61 -0
- package/dist/metadata.js.map +1 -0
- package/dist/update_check.d.ts +14 -0
- package/dist/update_check.d.ts.map +1 -0
- package/dist/update_check.js +130 -0
- package/dist/update_check.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +34 -0
- package/templates/AI-INSTRUCTIONS.md +163 -0
- package/templates/README.md +226 -0
- package/templates/claude-skill-glubean-test.md +382 -0
- package/templates/data/create-user.json +14 -0
- package/templates/data/endpoints.csv +5 -0
- package/templates/data/scenarios.yaml +19 -0
- package/templates/data/search-examples.json +14 -0
- package/templates/data/users.json +17 -0
- package/templates/data-driven.test.ts.tpl +118 -0
- package/templates/demo.test.result.json +398 -0
- package/templates/demo.test.ts.tpl +226 -0
- package/templates/explore-api.test.result.json +79 -0
- package/templates/minimal/README.md +42 -0
- package/templates/minimal-api.test.ts.tpl +42 -0
- package/templates/minimal-auth.test.ts.tpl +45 -0
- package/templates/minimal-search.test.ts.tpl +34 -0
- package/templates/openapi.sample.json +97 -0
- package/templates/pick.test.result.json +165 -0
- package/templates/pick.test.ts.tpl +126 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo API tests — showcases Glubean SDK features against DummyJSON.
|
|
3
|
+
*
|
|
4
|
+
* This file is designed to produce rich, visually impressive results:
|
|
5
|
+
* - Multiple test types (simple + multi-step builder)
|
|
6
|
+
* - Auto-traced HTTP calls via ctx.http (method, URL, status, duration)
|
|
7
|
+
* - Fluent assertions, structured logs
|
|
8
|
+
* - Auth flows, data integrity checks, pagination
|
|
9
|
+
*
|
|
10
|
+
* Run: glubean run demo.test.ts --result-json
|
|
11
|
+
*/
|
|
12
|
+
import { test } from "@glubean/sdk";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// 1. Simple test — List products
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const listProducts = test(
|
|
19
|
+
{ id: "list-products", name: "List Products", tags: ["smoke"] },
|
|
20
|
+
async (ctx) => {
|
|
21
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
22
|
+
|
|
23
|
+
const data = await ctx.http.get(`${baseUrl}/products?limit=5`).json<{
|
|
24
|
+
products: unknown[];
|
|
25
|
+
total: number;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
ctx.expect(data.products.length).toBe(5);
|
|
29
|
+
ctx.expect(data.total).toBeGreaterThan(0);
|
|
30
|
+
|
|
31
|
+
ctx.log(`Found ${data.total} products total`);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// 2. Simple test — Search products
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export const searchProducts = test(
|
|
40
|
+
{ id: "search-products", name: "Search Products", tags: ["smoke"] },
|
|
41
|
+
async (ctx) => {
|
|
42
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
43
|
+
|
|
44
|
+
const data = await ctx.http
|
|
45
|
+
.get(`${baseUrl}/products/search?q=phone`)
|
|
46
|
+
.json<{ products: { title: string }[] }>();
|
|
47
|
+
|
|
48
|
+
ctx.expect(data.products.length).toBeGreaterThan(0);
|
|
49
|
+
|
|
50
|
+
const names = data.products.map((p) => p.title);
|
|
51
|
+
ctx.log(`Found ${data.products.length} products matching 'phone'`, names);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// 3. Multi-step builder — Authentication flow
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export const authFlow = test("auth-flow")
|
|
60
|
+
.meta({ name: "Authentication Flow", tags: ["auth"] })
|
|
61
|
+
.step("login", async (ctx) => {
|
|
62
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
63
|
+
const username = ctx.secrets.require("USERNAME");
|
|
64
|
+
const password = ctx.secrets.require("PASSWORD");
|
|
65
|
+
|
|
66
|
+
const data = await ctx.http
|
|
67
|
+
.post(`${baseUrl}/auth/login`, {
|
|
68
|
+
json: { username, password, expiresInMins: 1 },
|
|
69
|
+
})
|
|
70
|
+
.json<{
|
|
71
|
+
accessToken: string;
|
|
72
|
+
refreshToken: string;
|
|
73
|
+
username: string;
|
|
74
|
+
}>();
|
|
75
|
+
|
|
76
|
+
ctx.expect(data.accessToken).toBeDefined();
|
|
77
|
+
ctx.expect(data.username).toBe(username);
|
|
78
|
+
|
|
79
|
+
ctx.log(`Logged in as ${data.username}`);
|
|
80
|
+
|
|
81
|
+
return { token: data.accessToken, refreshToken: data.refreshToken };
|
|
82
|
+
})
|
|
83
|
+
.step("get profile", async (ctx, state) => {
|
|
84
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
85
|
+
|
|
86
|
+
const data = await ctx.http
|
|
87
|
+
.get(`${baseUrl}/auth/me`, {
|
|
88
|
+
headers: { Authorization: `Bearer ${state.token}` },
|
|
89
|
+
})
|
|
90
|
+
.json<{
|
|
91
|
+
email: string;
|
|
92
|
+
firstName: string;
|
|
93
|
+
lastName: string;
|
|
94
|
+
}>();
|
|
95
|
+
|
|
96
|
+
ctx.expect(data.email).toBeDefined();
|
|
97
|
+
ctx.expect(data.firstName).toBeDefined();
|
|
98
|
+
|
|
99
|
+
ctx.log(`Profile: ${data.firstName} ${data.lastName} (${data.email})`);
|
|
100
|
+
|
|
101
|
+
return state;
|
|
102
|
+
})
|
|
103
|
+
.step("refresh token", async (ctx, state) => {
|
|
104
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
105
|
+
|
|
106
|
+
const data = await ctx.http
|
|
107
|
+
.post(`${baseUrl}/auth/refresh`, {
|
|
108
|
+
json: { refreshToken: state.refreshToken, expiresInMins: 1 },
|
|
109
|
+
})
|
|
110
|
+
.json<{ accessToken: string }>();
|
|
111
|
+
|
|
112
|
+
ctx.expect(data.accessToken).toBeDefined();
|
|
113
|
+
|
|
114
|
+
ctx.log("Token refreshed successfully");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// 4. Simple test — Cart data integrity
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export const cartIntegrity = test(
|
|
122
|
+
{
|
|
123
|
+
id: "cart-integrity",
|
|
124
|
+
name: "Cart Data Integrity",
|
|
125
|
+
tags: ["data-integrity"],
|
|
126
|
+
},
|
|
127
|
+
async (ctx) => {
|
|
128
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
129
|
+
|
|
130
|
+
const cart = await ctx.http.get(`${baseUrl}/carts/1`).json<{
|
|
131
|
+
products: { quantity: number; price: number }[];
|
|
132
|
+
total: number;
|
|
133
|
+
discountedTotal: number;
|
|
134
|
+
}>();
|
|
135
|
+
|
|
136
|
+
ctx.expect(cart.products.length).toBeGreaterThan(0);
|
|
137
|
+
|
|
138
|
+
// Verify each product has valid data
|
|
139
|
+
for (const p of cart.products.slice(0, 3)) {
|
|
140
|
+
ctx.expect(p.quantity).toBeGreaterThan(0);
|
|
141
|
+
ctx.expect(p.price).toBeGreaterThan(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Verify discount math
|
|
145
|
+
ctx.assert(
|
|
146
|
+
cart.discountedTotal <= cart.total,
|
|
147
|
+
"Discounted total should be <= total",
|
|
148
|
+
{ actual: cart.discountedTotal, expected: `<= ${cart.total}` },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
ctx.log(`Cart has ${cart.products.length} items`);
|
|
152
|
+
ctx.log(`Total: $${cart.total}, After discount: $${cart.discountedTotal}`);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// 5. Simple test — Pagination consistency
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
export const paginationCheck = test(
|
|
161
|
+
{ id: "pagination-check", name: "Pagination Consistency", tags: ["data"] },
|
|
162
|
+
async (ctx) => {
|
|
163
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
164
|
+
|
|
165
|
+
const d1 = await ctx.http
|
|
166
|
+
.get(`${baseUrl}/products?limit=10&skip=0`)
|
|
167
|
+
.json<{ products: unknown[]; total: number; skip: number }>();
|
|
168
|
+
|
|
169
|
+
const d2 = await ctx.http
|
|
170
|
+
.get(`${baseUrl}/products?limit=10&skip=10`)
|
|
171
|
+
.json<{ products: unknown[]; total: number; skip: number }>();
|
|
172
|
+
|
|
173
|
+
ctx.expect(d1.products.length).toBe(10);
|
|
174
|
+
ctx.expect(d2.skip).toBe(10);
|
|
175
|
+
ctx.assert(
|
|
176
|
+
d2.skip + d2.products.length <= d2.total,
|
|
177
|
+
"skip + length should be <= total",
|
|
178
|
+
{ actual: d2.skip + d2.products.length, expected: `<= ${d2.total}` },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
ctx.log(`Page 1: ${d1.products.length} items (skip=0)`);
|
|
182
|
+
ctx.log(`Page 2: ${d2.products.length} items (skip=10)`);
|
|
183
|
+
ctx.log(`Total: ${d2.total}, verified skip + length <= total`);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// 6. Multi-step builder — User-Todos cross-resource integrity
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export const userTodosIntegrity = test("user-todos-integrity")
|
|
192
|
+
.meta({ name: "User Todos Cross-Resource", tags: ["data-integrity"] })
|
|
193
|
+
.step("fetch user", async (ctx) => {
|
|
194
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
195
|
+
|
|
196
|
+
const user = await ctx.http
|
|
197
|
+
.get(`${baseUrl}/users/1`)
|
|
198
|
+
.json<{ id: number; firstName: string; lastName: string }>();
|
|
199
|
+
|
|
200
|
+
ctx.expect(user.id).toBe(1);
|
|
201
|
+
ctx.log(`User: ${user.firstName} ${user.lastName} (id=${user.id})`);
|
|
202
|
+
|
|
203
|
+
return { userId: user.id };
|
|
204
|
+
})
|
|
205
|
+
.step("verify todos", async (ctx, state) => {
|
|
206
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
207
|
+
|
|
208
|
+
const data = await ctx.http
|
|
209
|
+
.get(`${baseUrl}/todos?limit=5&skip=0`)
|
|
210
|
+
.json<{ todos: { id: number; todo: string; userId: number }[] }>();
|
|
211
|
+
|
|
212
|
+
ctx.expect(data.todos.length).toBeGreaterThan(0);
|
|
213
|
+
|
|
214
|
+
// Check that we can find todos for this user
|
|
215
|
+
const userTodos = data.todos.filter((t) => t.userId === state.userId);
|
|
216
|
+
|
|
217
|
+
ctx.log(
|
|
218
|
+
`Found ${data.todos.length} todos, ${userTodos.length} belong to user ${state.userId}`,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Each todo should have required fields
|
|
222
|
+
for (const todo of data.todos.slice(0, 3)) {
|
|
223
|
+
ctx.expect(todo.id).toBeDefined();
|
|
224
|
+
ctx.expect(todo.todo).toBeDefined();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "/Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts",
|
|
3
|
+
"files": [
|
|
4
|
+
"packages/cli/templates/explore-api.test.ts"
|
|
5
|
+
],
|
|
6
|
+
"runAt": "2026-02-16 13:46:19",
|
|
7
|
+
"summary": {
|
|
8
|
+
"total": 1,
|
|
9
|
+
"passed": 0,
|
|
10
|
+
"failed": 1,
|
|
11
|
+
"skipped": 0,
|
|
12
|
+
"durationMs": 34,
|
|
13
|
+
"stats": {
|
|
14
|
+
"httpRequestTotal": 0,
|
|
15
|
+
"httpErrorTotal": 0,
|
|
16
|
+
"assertionTotal": 0,
|
|
17
|
+
"assertionFailed": 0,
|
|
18
|
+
"warningTotal": 0,
|
|
19
|
+
"warningTriggered": 0,
|
|
20
|
+
"stepTotal": 0,
|
|
21
|
+
"stepPassed": 0,
|
|
22
|
+
"stepFailed": 0
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"tests": [
|
|
26
|
+
{
|
|
27
|
+
"testId": "quick-check",
|
|
28
|
+
"testName": "Quick Endpoint Check",
|
|
29
|
+
"tags": [
|
|
30
|
+
"explore"
|
|
31
|
+
],
|
|
32
|
+
"success": false,
|
|
33
|
+
"durationMs": 34,
|
|
34
|
+
"events": [
|
|
35
|
+
{
|
|
36
|
+
"type": "log",
|
|
37
|
+
"message": "Loading test module: file:///Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "start",
|
|
41
|
+
"id": "quick-check",
|
|
42
|
+
"name": "Quick Endpoint Check",
|
|
43
|
+
"tags": [
|
|
44
|
+
"explore"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "summary",
|
|
49
|
+
"data": {
|
|
50
|
+
"httpRequestTotal": 0,
|
|
51
|
+
"httpErrorTotal": 0,
|
|
52
|
+
"httpErrorRate": 0,
|
|
53
|
+
"assertionTotal": 0,
|
|
54
|
+
"assertionFailed": 0,
|
|
55
|
+
"warningTotal": 0,
|
|
56
|
+
"warningTriggered": 0,
|
|
57
|
+
"schemaValidationTotal": 0,
|
|
58
|
+
"schemaValidationFailed": 0,
|
|
59
|
+
"schemaValidationWarnings": 0,
|
|
60
|
+
"stepTotal": 0,
|
|
61
|
+
"stepPassed": 0,
|
|
62
|
+
"stepFailed": 0,
|
|
63
|
+
"stepSkipped": 0
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "status",
|
|
68
|
+
"status": "failed",
|
|
69
|
+
"error": "Missing required var: BASE_URL",
|
|
70
|
+
"stack": "Error: Missing required var: BASE_URL\n at Object.require (file:///Users/peisong/glubean/oss/packages/runner/harness.ts:360:15)\n at Object.fn (file:///Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts:18:30)\n at executeNewTest (file:///Users/peisong/glubean/oss/packages/runner/harness.ts:1192:18)\n at file:///Users/peisong/glubean/oss/packages/runner/harness.ts:971:11"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "error",
|
|
74
|
+
"message": "Process exited with code 1"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Glubean — Playground
|
|
2
|
+
|
|
3
|
+
This is a quick playground for exploring APIs with [Glubean](https://glubean.com). Write TypeScript, run it, see every
|
|
4
|
+
request traced.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
deno task explore
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What's here
|
|
11
|
+
|
|
12
|
+
| Path | Purpose |
|
|
13
|
+
| --------------------------- | ----------------------------------------------- |
|
|
14
|
+
| `explore/api.test.ts` | GET and POST examples — edit and run |
|
|
15
|
+
| `explore/search.test.ts` | `test.pick` — one test, multiple variations |
|
|
16
|
+
| `explore/auth.test.ts` | Multi-step auth — login, use token, get profile |
|
|
17
|
+
| `data/search-examples.json` | Search parameters for pick examples |
|
|
18
|
+
|
|
19
|
+
Edit the files, change the URLs, hit play. That's it.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Ready for real work?
|
|
24
|
+
|
|
25
|
+
This playground is for trying things out. When you're ready to build a real test suite — with AI writing and running
|
|
26
|
+
tests for you — create a full project:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
mkdir my-api-tests && cd my-api-tests
|
|
30
|
+
glubean init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Choose **Best Practice** to unlock:
|
|
34
|
+
|
|
35
|
+
- **AI closed-loop** — your AI reads your API spec, writes tests, runs them via MCP, reads failures, fixes, and reruns
|
|
36
|
+
until green. You review the result, not the process.
|
|
37
|
+
- **OpenAPI-driven** — drop your spec in `context/`, and the AI knows every endpoint, method, and schema. No guessing.
|
|
38
|
+
- **Multi-environment** — same tests against dev, staging, and production. Switch with one flag.
|
|
39
|
+
- **Data-driven tests** — generate dozens of cases from JSON, CSV, or YAML with `test.each`.
|
|
40
|
+
- **CI + Cloud** — Git hooks, GitHub Actions, scheduled runs, Slack alerts when something breaks.
|
|
41
|
+
|
|
42
|
+
The difference: here you write tests manually. There, the AI writes them and proves they work — before you even look.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick API exploration — GET and POST examples ready to run.
|
|
3
|
+
*
|
|
4
|
+
* Edit the URLs, change the payload, hit play. Every request is
|
|
5
|
+
* auto-traced so you can inspect headers, timing, and response
|
|
6
|
+
* bodies in the trace viewer.
|
|
7
|
+
*
|
|
8
|
+
* Run: deno task explore
|
|
9
|
+
*/
|
|
10
|
+
import { test } from "@glubean/sdk";
|
|
11
|
+
|
|
12
|
+
export const getProduct = test(
|
|
13
|
+
{ id: "get-product", name: "GET Product", tags: ["explore"] },
|
|
14
|
+
async (ctx) => {
|
|
15
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
16
|
+
|
|
17
|
+
const res = await ctx.http.get(`${baseUrl}/products/1`);
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
|
|
20
|
+
ctx.expect(res.status).toBe(200);
|
|
21
|
+
ctx.expect(data.title).toBeDefined();
|
|
22
|
+
|
|
23
|
+
ctx.log("Product", data);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const createProduct = test(
|
|
28
|
+
{ id: "create-product", name: "POST Create Product", tags: ["explore"] },
|
|
29
|
+
async (ctx) => {
|
|
30
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
31
|
+
|
|
32
|
+
const res = await ctx.http.post(`${baseUrl}/products/add`, {
|
|
33
|
+
json: { title: "Test Product", price: 9.99, category: "test" },
|
|
34
|
+
});
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
|
|
37
|
+
ctx.expect(res.status).toBe(200);
|
|
38
|
+
ctx.expect(data.id).toBeDefined();
|
|
39
|
+
|
|
40
|
+
ctx.log("Created", data);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-step auth flow — login, use token, get profile.
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates the builder API: each step passes state to the next.
|
|
5
|
+
* The trace viewer shows all three requests as a connected flow, not
|
|
6
|
+
* isolated calls. That's the difference between Glubean and a REST client.
|
|
7
|
+
*
|
|
8
|
+
* Run: deno task explore
|
|
9
|
+
*/
|
|
10
|
+
import { test } from "@glubean/sdk";
|
|
11
|
+
|
|
12
|
+
export const authFlow = test("auth-flow")
|
|
13
|
+
.meta({ name: "Auth Flow", tags: ["explore", "auth"] })
|
|
14
|
+
.step("login", async (ctx) => {
|
|
15
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
16
|
+
const username = ctx.secrets.require("USERNAME");
|
|
17
|
+
const password = ctx.secrets.require("PASSWORD");
|
|
18
|
+
|
|
19
|
+
const data = await ctx.http
|
|
20
|
+
.post(`${baseUrl}/auth/login`, {
|
|
21
|
+
json: { username, password, expiresInMins: 1 },
|
|
22
|
+
})
|
|
23
|
+
.json<{ accessToken: string; refreshToken: string; username: string }>();
|
|
24
|
+
|
|
25
|
+
ctx.expect(data.accessToken).toBeDefined().orFail();
|
|
26
|
+
ctx.expect(data.username).toBe(username);
|
|
27
|
+
|
|
28
|
+
ctx.log(`Logged in as ${data.username}`);
|
|
29
|
+
|
|
30
|
+
return { token: data.accessToken };
|
|
31
|
+
})
|
|
32
|
+
.step("get profile", async (ctx, state) => {
|
|
33
|
+
const baseUrl = ctx.vars.require("BASE_URL");
|
|
34
|
+
|
|
35
|
+
const data = await ctx.http
|
|
36
|
+
.get(`${baseUrl}/auth/me`, {
|
|
37
|
+
headers: { Authorization: `Bearer ${state.token}` },
|
|
38
|
+
})
|
|
39
|
+
.json<{ email: string; firstName: string; lastName: string }>();
|
|
40
|
+
|
|
41
|
+
ctx.expect(data.email).toBeDefined();
|
|
42
|
+
ctx.expect(data.firstName).toBeDefined();
|
|
43
|
+
|
|
44
|
+
ctx.log(`Profile: ${data.firstName} ${data.lastName} (${data.email})`);
|
|
45
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameterized search — run different query variations with test.pick.
|
|
3
|
+
*
|
|
4
|
+
* Each example in search-examples.json defines a search term and expected
|
|
5
|
+
* outcome. In VS Code, CodeLens buttons appear above the test so you can
|
|
6
|
+
* click a specific example to run.
|
|
7
|
+
*
|
|
8
|
+
* Run all: deno task explore
|
|
9
|
+
* Pick one: glubean run explore/search.test.ts --pick by-name
|
|
10
|
+
* Pick another: glubean run explore/search.test.ts --pick no-results
|
|
11
|
+
*/
|
|
12
|
+
import { test } from "@glubean/sdk";
|
|
13
|
+
import examples from "../data/search-examples.json" with { type: "json" };
|
|
14
|
+
|
|
15
|
+
export const searchProducts = test.pick(examples)(
|
|
16
|
+
"search-$_pick",
|
|
17
|
+
async ({ http, vars, expect, log }, { q, expected }) => {
|
|
18
|
+
const baseUrl = vars.require("BASE_URL");
|
|
19
|
+
|
|
20
|
+
const res = await http
|
|
21
|
+
.get(`${baseUrl}/products/search`, { searchParams: { q } })
|
|
22
|
+
.json<{ products: { title: string }[]; total: number }>();
|
|
23
|
+
|
|
24
|
+
expect(res.total).toBeGreaterThan(expected.minResults - 1);
|
|
25
|
+
|
|
26
|
+
if (expected.titleContains && res.products.length > 0) {
|
|
27
|
+
expect(res.products[0].title.toLowerCase()).toContain(
|
|
28
|
+
expected.titleContains,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log(`"${q}" → ${res.total} results`);
|
|
33
|
+
},
|
|
34
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Glubean Sample API (mock)",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"description": "A mock OpenAPI spec shipped with `glubean init` for AI-assisted test generation. The real sample API service is not ready yet."
|
|
7
|
+
},
|
|
8
|
+
"servers": [
|
|
9
|
+
{
|
|
10
|
+
"url": "https://sample-api.glubean.com",
|
|
11
|
+
"description": "Mock URL"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"components": {
|
|
15
|
+
"securitySchemes": {
|
|
16
|
+
"ApiKeyAuth": {
|
|
17
|
+
"type": "http",
|
|
18
|
+
"scheme": "bearer",
|
|
19
|
+
"bearerFormat": "API key"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"schemas": {
|
|
23
|
+
"ErrorResponse": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"required": [
|
|
26
|
+
"error"
|
|
27
|
+
],
|
|
28
|
+
"properties": {
|
|
29
|
+
"error": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": [
|
|
32
|
+
"code",
|
|
33
|
+
"message"
|
|
34
|
+
],
|
|
35
|
+
"properties": {
|
|
36
|
+
"code": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"example": "VALIDATION_ERROR"
|
|
39
|
+
},
|
|
40
|
+
"message": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"example": "Invalid payload"
|
|
43
|
+
},
|
|
44
|
+
"fields": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": [
|
|
49
|
+
"path",
|
|
50
|
+
"reason"
|
|
51
|
+
],
|
|
52
|
+
"properties": {
|
|
53
|
+
"path": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"example": "email"
|
|
56
|
+
},
|
|
57
|
+
"reason": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"example": "must be a valid email"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"security": [
|
|
71
|
+
{
|
|
72
|
+
"ApiKeyAuth": []
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"paths": {
|
|
76
|
+
"/health": {
|
|
77
|
+
"get": {
|
|
78
|
+
"operationId": "healthCheck",
|
|
79
|
+
"tags": [
|
|
80
|
+
"System"
|
|
81
|
+
],
|
|
82
|
+
"responses": {
|
|
83
|
+
"200": {
|
|
84
|
+
"description": "Service health",
|
|
85
|
+
"content": {
|
|
86
|
+
"application/json": {
|
|
87
|
+
"schema": {
|
|
88
|
+
"type": "object"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|