@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
|
+
# Glubean API Tests
|
|
2
|
+
|
|
3
|
+
This project was generated by `glubean init`.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
├── tests/ # Permanent tests — run in CI, Cloud, and locally
|
|
9
|
+
├── explore/ # Exploratory tests — quick iteration in your editor
|
|
10
|
+
├── data/ # Shared test data (JSON, CSV, YAML) used by both
|
|
11
|
+
├── context/ # API specs and reference docs for AI and tooling
|
|
12
|
+
├── deno.json # Config: tasks, SDK import, testDir/exploreDir
|
|
13
|
+
├── .env # Default environment variables (BASE_URL, etc.)
|
|
14
|
+
├── .env.secrets # Default secrets (API keys — gitignored)
|
|
15
|
+
├── .env.staging # Staging environment variables
|
|
16
|
+
├── .env.staging.secrets # Staging secrets (gitignored)
|
|
17
|
+
├── CLAUDE.md # AI instructions (Claude Code, Cursor)
|
|
18
|
+
└── AGENTS.md # AI instructions (Codex, other agents)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Required:** `tests/` and `deno.json`. Everything else is recommended.
|
|
22
|
+
|
|
23
|
+
> **AI instructions:** Both `CLAUDE.md` and `AGENTS.md` are generated with the same content. Keep whichever your AI tool
|
|
24
|
+
> reads — **Claude Code** reads `CLAUDE.md`, **Codex** reads `AGENTS.md`, **Cursor** reads both. Delete the one you
|
|
25
|
+
> don't need.
|
|
26
|
+
|
|
27
|
+
- **`tests/`** — Your permanent test files (`*.test.ts`). These are what `deno task test` and CI run. All test files
|
|
28
|
+
must end in `.test.ts`.
|
|
29
|
+
- **`explore/`** — Same `*.test.ts` files, but for interactive exploration. Think of it as a scratchpad: quick API
|
|
30
|
+
calls, debugging, prototyping. Move files to `tests/` when they're ready to be permanent. Run with
|
|
31
|
+
`deno task explore`.
|
|
32
|
+
- **`data/`** — Shared test data files (JSON, CSV, YAML). Both `tests/` and `explore/` can import from here. Keeps test
|
|
33
|
+
logic clean and data reviewable.
|
|
34
|
+
- **`context/`** — API specs (OpenAPI), reference docs, and anything that helps you (or your AI) understand the API
|
|
35
|
+
under test. Drop your OpenAPI spec here and point your AI at it.
|
|
36
|
+
|
|
37
|
+
The `data/` and `context/` directories are optional conventions — you can organize data and specs however you prefer.
|
|
38
|
+
These defaults exist because they work well with the CLI tooling and AI workflows.
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
1. Install [Deno](https://deno.land) (v2+):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# macOS
|
|
46
|
+
brew install deno
|
|
47
|
+
|
|
48
|
+
# or see https://docs.deno.com/runtime/getting_started/installation/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
2. Install the Glubean CLI:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
deno install -Ag -n glubean jsr:@glubean/cli
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
1. Edit `.env` with your API base URL
|
|
60
|
+
2. Edit `.env.secrets` with any API keys
|
|
61
|
+
3. Run tests:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
deno task test # run all tests in tests/
|
|
65
|
+
deno task test:verbose # with detailed output
|
|
66
|
+
deno task test:staging # run against staging environment
|
|
67
|
+
deno task explore # run explore/ tests
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or run a specific file:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
glubean run tests/demo.test.ts --verbose
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Environment switching
|
|
77
|
+
|
|
78
|
+
The same tests can run against different environments by switching env files. Each env file pairs with a secrets file
|
|
79
|
+
automatically:
|
|
80
|
+
|
|
81
|
+
| Env file | Secrets file | Task shortcut |
|
|
82
|
+
| -------------- | ---------------------- | ------------------------ |
|
|
83
|
+
| `.env` | `.env.secrets` | `deno task test` |
|
|
84
|
+
| `.env.staging` | `.env.staging.secrets` | `deno task test:staging` |
|
|
85
|
+
|
|
86
|
+
To add more environments (e.g. production), create `.env.production` and `.env.production.secrets`, then run:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
glubean run --env-file .env.production
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or add a task in `deno.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
"test:production": "glubean run --env-file .env.production"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
All `*.secrets` files are gitignored by default. The non-secret `.env.*` files can be committed safely — they typically
|
|
99
|
+
only contain `BASE_URL` and other non-sensitive config.
|
|
100
|
+
|
|
101
|
+
## Use your AI to generate tests (from OpenAPI)
|
|
102
|
+
|
|
103
|
+
This project includes a sample OpenAPI spec at `context/openapi.sample.json`. Most AI coding tools can use this spec to
|
|
104
|
+
generate accurate tests quickly.
|
|
105
|
+
|
|
106
|
+
Copy/paste prompts you can try (edit as needed):
|
|
107
|
+
|
|
108
|
+
1. Generate a smoke suite:
|
|
109
|
+
|
|
110
|
+
> Read `context/openapi.sample.json` and `CLAUDE.md` (or `AGENTS.md`). Generate Glubean tests in `tests/smoke.test.ts`
|
|
111
|
+
> that cover all GET endpoints with 200 responses. Use the builder API with `.step()` for multi-step flows. Use
|
|
112
|
+
> `ctx.http` for requests (auto-traces every call). Add assertions for status code and basic response schema. Group
|
|
113
|
+
> tests by tag.
|
|
114
|
+
|
|
115
|
+
2. Generate auth + negative tests:
|
|
116
|
+
|
|
117
|
+
> Using `context/openapi.sample.json`, generate tests in `tests/auth.test.ts` that verify authentication behavior:
|
|
118
|
+
> missing API key returns 401, invalid API key returns 401, insufficient scope returns 403. Use the builder API with
|
|
119
|
+
> `.setup()` for shared auth headers. Use `ctx.secrets.require("API_KEY")` and also test missing/empty API key scenarios
|
|
120
|
+
> safely.
|
|
121
|
+
|
|
122
|
+
3. Generate CRUD flow tests:
|
|
123
|
+
|
|
124
|
+
> From `context/openapi.sample.json`, identify resources with create/read/update/delete endpoints. Generate builder API
|
|
125
|
+
> tests in `tests/crud.test.ts` with steps: create → verify → update → verify → delete → verify. Pass state (IDs)
|
|
126
|
+
> between steps.
|
|
127
|
+
|
|
128
|
+
4. Generate validation tests:
|
|
129
|
+
|
|
130
|
+
> From `context/openapi.sample.json`, identify POST/PUT endpoints with request bodies. Generate tests in
|
|
131
|
+
> `tests/validation.test.ts` for validation edge cases (missing required fields, invalid enums, min/max constraints).
|
|
132
|
+
> Assert 422 responses and validate the error schema shape.
|
|
133
|
+
|
|
134
|
+
## Try MCP in Cursor (AI closed loop)
|
|
135
|
+
|
|
136
|
+
Glubean also ships an MCP (Model Context Protocol) server so your AI tool can **discover tests**, **run them**, read
|
|
137
|
+
**structured failures**, and iterate quickly:
|
|
138
|
+
|
|
139
|
+
> AI writes tests → runs locally → reads assertions/logs/traces → fixes → reruns → ✓ pass
|
|
140
|
+
|
|
141
|
+
### Setup
|
|
142
|
+
|
|
143
|
+
1. Configure the MCP server in Cursor (via UI or `~/.cursor/mcp.json`):
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mcpServers": {
|
|
148
|
+
"glubean": {
|
|
149
|
+
"command": "deno",
|
|
150
|
+
"args": ["run", "-A", "jsr:@glubean/mcp"],
|
|
151
|
+
"description": "Glubean test runner and cloud integration"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
2. Restart Cursor.
|
|
158
|
+
|
|
159
|
+
### Prompts to try
|
|
160
|
+
|
|
161
|
+
- Generate + run + fix (local):
|
|
162
|
+
|
|
163
|
+
> Use the Glubean MCP tools to discover tests in `tests/`, run them locally, and if any fail, fix the test code and
|
|
164
|
+
> rerun until everything passes. Keep changes minimal and explain each fix.
|
|
165
|
+
|
|
166
|
+
- Generate tests from spec + run (local):
|
|
167
|
+
|
|
168
|
+
> Read `context/openapi.sample.json` and generate Glubean tests in `tests/smoke.test.ts`. Then use MCP to run them
|
|
169
|
+
> locally. If validation or auth failures happen, adjust payloads/headers and rerun until green.
|
|
170
|
+
|
|
171
|
+
### Optional: enable cloud tools
|
|
172
|
+
|
|
173
|
+
Local MCP tools do not require cloud auth. If you also want the AI to trigger remote runs via the Open Platform, set:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
export GLUBEAN_TOKEN=gpt_your_project_token
|
|
177
|
+
export GLUBEAN_API_URL=https://api.glubean.com
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## What you get from the cloud (and why it matters)
|
|
181
|
+
|
|
182
|
+
Running locally is great for authoring and debugging. The cloud is where the same verification code becomes a
|
|
183
|
+
**continuous operational workflow** — no rewrites, no extra config files, no separate CI pipeline.
|
|
184
|
+
|
|
185
|
+
### The problem Glubean Cloud solves
|
|
186
|
+
|
|
187
|
+
| Without Glubean Cloud | With Glubean Cloud |
|
|
188
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
189
|
+
| Tests run in CI, pass, and are forgotten | Tests run on a schedule — every 5 min, hourly, daily |
|
|
190
|
+
| Staging tested, production assumed OK | Same tests, different environments, zero code changes |
|
|
191
|
+
| API breaks at 3am, discovered next morning | Slack/email alert within minutes with exact failure details |
|
|
192
|
+
| Test results are ephemeral terminal output | Structured history: assertions, traces, logs — searchable and shareable |
|
|
193
|
+
| Secrets hardcoded or scattered across CI configs | Encrypted environment groups, injected at runtime, never in git |
|
|
194
|
+
|
|
195
|
+
### What the cloud provides
|
|
196
|
+
|
|
197
|
+
- **Automatic bundle builds**: `git push` triggers a build — your test files are packaged into an immutable bundle (like
|
|
198
|
+
a Docker image for verification).
|
|
199
|
+
- **Environment groups**: configure `staging` and `production` with different `BASE_URL`, API keys, and secrets. Run the
|
|
200
|
+
same tests against both.
|
|
201
|
+
- **Scheduling**: every N minutes, daily at a specific time, weekly, or cron. Workers pick up runs from a queue —
|
|
202
|
+
nothing to maintain.
|
|
203
|
+
- **Alerts**: Slack, email, or webhooks. Failures include the exact assertion that broke (`expected 200, got 502`), not
|
|
204
|
+
a generic "tests failed" message.
|
|
205
|
+
- **Dashboard**: full run history with assertion timelines, API traces, HTTP metrics, and step-level breakdowns.
|
|
206
|
+
- **Open Platform**: project-scoped tokens and webhooks for integrating results into your internal tools and automation.
|
|
207
|
+
|
|
208
|
+
### Get started with the cloud
|
|
209
|
+
|
|
210
|
+
1. Create an account at `https://app.glubean.com`
|
|
211
|
+
2. Create a Project and connect your GitHub repository
|
|
212
|
+
3. Push your tests:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
git add .
|
|
216
|
+
git commit -m "add api tests"
|
|
217
|
+
git push
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
4. Glubean builds your bundle automatically. Click **Run Now** in the dashboard, or set up a schedule to run
|
|
221
|
+
continuously.
|
|
222
|
+
|
|
223
|
+
## Notes
|
|
224
|
+
|
|
225
|
+
- `.env.secrets` should stay local and must not be committed.
|
|
226
|
+
- Edit test files in `tests/` to match your real API behavior and contracts.
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gb
|
|
3
|
+
description: Generate Glubean API tests from OpenAPI specs or user instructions. Reads context/, writes tests, runs them via MCP, and fixes failures automatically.
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Glob
|
|
9
|
+
- Grep
|
|
10
|
+
- Bash
|
|
11
|
+
- mcp__glubean__glubean_run_local_file
|
|
12
|
+
- mcp__glubean__glubean_discover_tests
|
|
13
|
+
- mcp__glubean__glubean_list_test_files
|
|
14
|
+
- mcp__glubean__glubean_diagnose_config
|
|
15
|
+
- mcp__glubean__glubean_get_last_run_summary
|
|
16
|
+
- mcp__glubean__glubean_get_local_events
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Glubean Test Generator
|
|
20
|
+
|
|
21
|
+
You are a Glubean test expert. Generate, run, and fix API tests using `@glubean/sdk`.
|
|
22
|
+
|
|
23
|
+
## Your workflow
|
|
24
|
+
|
|
25
|
+
1. **Read the API spec** — first check for `context/*-endpoints/_index.md` (pre-split specs). If found, read the index
|
|
26
|
+
and only open the specific endpoint file you need. If no split endpoints exist, search `context/` for OpenAPI specs
|
|
27
|
+
(`.json`, `.yaml`). If a spec is larger than 50K, suggest the user run `glubean spec split context/<file>` first. If
|
|
28
|
+
no spec found, ask the user for endpoint details.
|
|
29
|
+
2. **Read existing tests** — check `tests/` and `explore/` for patterns, configure files, and naming conventions already
|
|
30
|
+
in use.
|
|
31
|
+
3. **Write tests** — generate test files following the SDK API and conventions below.
|
|
32
|
+
4. **Run tests** — use MCP tool `glubean_run_local_file` to execute. If MCP is unavailable, use `deno task test` via
|
|
33
|
+
Bash.
|
|
34
|
+
5. **Fix failures** — read the structured failure output, fix the test code, and rerun. Repeat until green.
|
|
35
|
+
|
|
36
|
+
If $ARGUMENTS is provided, treat it as the target: an endpoint path, a tag, a file to test, or a natural language
|
|
37
|
+
description.
|
|
38
|
+
|
|
39
|
+
## Project structure
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
tests/ # Permanent test files (*.test.ts)
|
|
43
|
+
explore/ # Exploratory tests (same format, for iteration)
|
|
44
|
+
data/ # Test data files (JSON, CSV, YAML)
|
|
45
|
+
context/ # OpenAPI specs and reference docs
|
|
46
|
+
.env # Public variables (BASE_URL)
|
|
47
|
+
.env.secrets # Credentials — gitignored
|
|
48
|
+
deno.json # Runtime config, imports, glubean settings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Import convention
|
|
52
|
+
|
|
53
|
+
Always use the import map alias from `deno.json`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { configure, fromCsv, fromDir, fromYaml, test } from "@glubean/sdk";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Never use `jsr:` URLs directly.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## SDK API Reference
|
|
64
|
+
|
|
65
|
+
### test()
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Quick mode — single function
|
|
69
|
+
export const myTest = test(
|
|
70
|
+
{ id: "kebab-case-id", name: "Human name", tags: ["api"] },
|
|
71
|
+
async (ctx) => { ... }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Builder mode — multi-step
|
|
75
|
+
export const myFlow = test("flow-id")
|
|
76
|
+
.meta({ name: "Flow name", tags: ["api"] })
|
|
77
|
+
.setup(async (ctx) => { return { token: "..." }; })
|
|
78
|
+
.step("step-name", async (ctx, state) => { return { ...state, new: "data" }; })
|
|
79
|
+
.teardown(async (ctx, state) => { /* cleanup, always runs */ });
|
|
80
|
+
|
|
81
|
+
// Reusable steps with .use() — extract common sequences into plain functions
|
|
82
|
+
const withAuth = (b: TestBuilder<unknown>) => b
|
|
83
|
+
.step("login", async (ctx) => {
|
|
84
|
+
const { token } = await ctx.http.post("/login", { json: { ... } }).json<{ token: string }>();
|
|
85
|
+
return { token };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const testA = test("test-a").use(withAuth).step("act", async (ctx, { token }) => { ... });
|
|
89
|
+
export const testB = test("test-b").use(withAuth).step("verify", async (ctx, { token }) => { ... });
|
|
90
|
+
|
|
91
|
+
// .group(id, fn) — same as .use() but tags steps for visual grouping in reports
|
|
92
|
+
export const checkout = test("checkout")
|
|
93
|
+
.group("auth", withAuth)
|
|
94
|
+
.step("pay", async (ctx, { token }) => { ... });
|
|
95
|
+
// Report: checkout → [auth] login → pay
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### TestContext (ctx)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
ctx.http // HTTP client (auto-traces)
|
|
102
|
+
ctx.expect(value) // Soft assertion
|
|
103
|
+
ctx.assert(condition, message?) // Hard assertion
|
|
104
|
+
ctx.log(message, data?) // Structured log
|
|
105
|
+
ctx.vars.require("KEY") // Read env var (throws if missing)
|
|
106
|
+
ctx.secrets.require("KEY") // Read secret (auto-redacted)
|
|
107
|
+
ctx.metric("name", value, { unit? }) // Record metric
|
|
108
|
+
ctx.validate(data, zodSchema) // Schema validation
|
|
109
|
+
ctx.pollUntil({ timeoutMs }, fn) // Poll until truthy
|
|
110
|
+
ctx.skip(reason?) // Skip test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### HTTP Client
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const res = await ctx.http.get(url, options?);
|
|
117
|
+
const data = await ctx.http.post(url, { json: { ... } }).json<T>();
|
|
118
|
+
// Also: .put(), .patch(), .delete()
|
|
119
|
+
// Response: .json<T>(), .text(), .blob()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
{
|
|
126
|
+
json: { ... }, // JSON body
|
|
127
|
+
searchParams: { key: "value" }, // Query params
|
|
128
|
+
headers: { "X-Custom": "val" }, // Headers
|
|
129
|
+
timeout: 5000, // ms
|
|
130
|
+
retry: 3, // Retry count
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Extend with defaults:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const authed = ctx.http.extend({
|
|
138
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Assertions — ctx.expect()
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
expect(x).toBe(y); // Strict equal
|
|
146
|
+
expect(x).toEqual(y); // Deep equal
|
|
147
|
+
expect(x).toBeTruthy();
|
|
148
|
+
expect(x).toBeDefined();
|
|
149
|
+
expect(n).toBeGreaterThan(5);
|
|
150
|
+
expect(s).toContain("sub");
|
|
151
|
+
expect(s).toMatch(/regex/);
|
|
152
|
+
expect(arr).toHaveLength(3);
|
|
153
|
+
expect(obj).toMatchObject({ key: "val" });
|
|
154
|
+
expect(obj).toHaveProperty("path.to.key");
|
|
155
|
+
expect(res).toHaveStatus(200); // HTTP response
|
|
156
|
+
expect(x).not.toBe(y); // Negate
|
|
157
|
+
expect(x).toBe(y).orFail(); // Hard fail (stop test)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### configure() — shared HTTP client
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// config/api.ts or tests/configure.ts
|
|
164
|
+
import { configure } from "@glubean/sdk";
|
|
165
|
+
|
|
166
|
+
export const { http, vars, secrets } = configure({
|
|
167
|
+
vars: { baseUrl: "BASE_URL" },
|
|
168
|
+
secrets: { apiKey: "API_KEY" },
|
|
169
|
+
http: {
|
|
170
|
+
prefixUrl: "BASE_URL", // Env var name
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: "Bearer {{API_KEY}}", // {{var}} interpolation
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Data loading
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const rows = await fromDir<T>("./data/users/"); // One JSON file = one row
|
|
182
|
+
const cases = await fromDir.merge<T>("./data/search/"); // Merged for test.pick
|
|
183
|
+
const csv = await fromCsv<T>("./data/file.csv");
|
|
184
|
+
const yaml = await fromYaml<T>("./data/file.yaml");
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Data file formats:**
|
|
188
|
+
|
|
189
|
+
`fromDir` — each file is one row. File content is a flat object:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
// data/users/alice.json — becomes one row with _name="alice"
|
|
193
|
+
{ "username": "alice", "role": "admin", "expected": 200 }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`fromDir.merge` — each file contains named examples as top-level keys. Keys = pick names, values = scenario objects. All
|
|
197
|
+
files are shallow-merged into one map:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
// data/search/queries.json
|
|
201
|
+
{
|
|
202
|
+
"by-name": { "q": "phone", "expected": "phone" },
|
|
203
|
+
"by-category": { "q": "laptop", "expected": "laptop" }
|
|
204
|
+
}
|
|
205
|
+
// data/search/edge-cases.json
|
|
206
|
+
{
|
|
207
|
+
"empty-query": { "q": "", "expected": "" }
|
|
208
|
+
}
|
|
209
|
+
// → merged result: { "by-name": {...}, "by-category": {...}, "empty-query": {...} }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Inline examples (no data file needed):
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// test.each — array of objects
|
|
216
|
+
test.each([
|
|
217
|
+
{ id: 1, expected: 200 },
|
|
218
|
+
{ id: 999, expected: 404 },
|
|
219
|
+
])("get-user-$id", async (ctx, { id, expected }) => { ... });
|
|
220
|
+
|
|
221
|
+
// test.pick — object with named keys
|
|
222
|
+
test.pick({
|
|
223
|
+
"normal": { name: "Alice", age: 25 },
|
|
224
|
+
"edge-case": { name: "", age: -1 },
|
|
225
|
+
})("create-user-$_pick", async (ctx, data) => { ... });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### test.each — data-driven
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// Quick mode — single function per row
|
|
232
|
+
const users = await fromDir<{ username: string }>("./data/users/");
|
|
233
|
+
export const tests = test.each(users)(
|
|
234
|
+
"user-lookup-$username", // $field interpolates
|
|
235
|
+
async (ctx, { username }) => { ... },
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Builder mode — multi-step per row (omit callback to get builder)
|
|
239
|
+
export const flows = test.each(users)("user-flow-$username")
|
|
240
|
+
.step("fetch", async (ctx, _state, row) => {
|
|
241
|
+
const res = await ctx.http.get(`/users/${row.username}`).json<{ id: string }>();
|
|
242
|
+
return { id: res.id };
|
|
243
|
+
})
|
|
244
|
+
.step("verify", async (ctx, state, row) => {
|
|
245
|
+
ctx.expect(state.id).toBeDefined();
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### test.pick — named examples
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Quick mode — single function
|
|
253
|
+
const cases = await fromDir.merge<{ q: string }>("./data/search/");
|
|
254
|
+
export const tests = test.pick(cases)(
|
|
255
|
+
"search-$_pick", // $_pick = case name
|
|
256
|
+
async (ctx, { q }) => { ... },
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Builder mode — multi-step per picked example (omit callback to get builder)
|
|
260
|
+
export const flows = test.pick(cases)("search-flow-$_pick")
|
|
261
|
+
.setup(async (ctx, row) => ({ query: row.q }))
|
|
262
|
+
.step("search", async (ctx, state, row) => {
|
|
263
|
+
const res = await ctx.http.get("/search", { searchParams: { q: state.query } }).json<{ total: number }>();
|
|
264
|
+
return { ...state, total: res.total };
|
|
265
|
+
})
|
|
266
|
+
.step("verify", async (ctx, state) => {
|
|
267
|
+
ctx.expect(state.total).toBeGreaterThan(0);
|
|
268
|
+
})
|
|
269
|
+
.teardown(async (ctx, state) => { /* cleanup */ });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Both `test.each` and `test.pick` support the same builder API as `test()`. Omit the callback to enter builder mode;
|
|
273
|
+
chain `.step()`, `.setup()`, `.teardown()`, `.meta()`, etc.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Patterns
|
|
278
|
+
|
|
279
|
+
### Simple API test
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { test } from "@glubean/sdk";
|
|
283
|
+
import { http } from "./configure.ts";
|
|
284
|
+
|
|
285
|
+
export const listUsers = test(
|
|
286
|
+
{ id: "list-users", tags: ["api", "smoke"] },
|
|
287
|
+
async ({ expect }) => {
|
|
288
|
+
const users = await http.get("users").json<{ users: unknown[] }>();
|
|
289
|
+
expect(users.users.length).toBeGreaterThan(0);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### CRUD with cleanup
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
export const crud = test("resource-crud")
|
|
298
|
+
.meta({ tags: ["api"] })
|
|
299
|
+
.setup(async () => {
|
|
300
|
+
const item = await http.post("items", { json: { name: "test" } }).json<{ id: string }>();
|
|
301
|
+
return { id: item.id };
|
|
302
|
+
})
|
|
303
|
+
.step("read", async ({ expect }, state) => {
|
|
304
|
+
const item = await http.get(`items/${state.id}`).json<{ name: string }>();
|
|
305
|
+
expect(item.name).toBe("test");
|
|
306
|
+
return state;
|
|
307
|
+
})
|
|
308
|
+
.step("update", async ({ expect }, state) => {
|
|
309
|
+
const item = await http.put(`items/${state.id}`, { json: { name: "updated" } }).json<{ name: string }>();
|
|
310
|
+
expect(item.name).toBe("updated");
|
|
311
|
+
return state;
|
|
312
|
+
})
|
|
313
|
+
.teardown(async (_ctx, state) => {
|
|
314
|
+
if (state?.id) await http.delete(`items/${state.id}`);
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Auth flow
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
export const auth = test("auth-flow")
|
|
322
|
+
.meta({ tags: ["api", "auth"] })
|
|
323
|
+
.step("login", async ({ http, expect, secrets }) => {
|
|
324
|
+
const res = await http.post("https://api.example.com/auth/login", {
|
|
325
|
+
json: { username: secrets.require("USERNAME"), password: secrets.require("PASSWORD") },
|
|
326
|
+
}).json<{ token: string }>();
|
|
327
|
+
expect(res.token).toBeDefined();
|
|
328
|
+
return { token: res.token };
|
|
329
|
+
})
|
|
330
|
+
.step("access protected", async ({ http, expect }, state) => {
|
|
331
|
+
const authed = http.extend({ headers: { Authorization: `Bearer ${state.token}` } });
|
|
332
|
+
const profile = await authed.get("https://api.example.com/me").json<{ id: string }>();
|
|
333
|
+
expect(profile.id).toBeDefined();
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Error / negative test
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
export const unauthorized = test(
|
|
341
|
+
{ id: "unauthorized-access", tags: ["api", "auth"] },
|
|
342
|
+
async ({ http, expect }) => {
|
|
343
|
+
const res = await http.get("https://api.example.com/protected");
|
|
344
|
+
expect(res).toHaveStatus(401);
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Rules
|
|
352
|
+
|
|
353
|
+
- **IDs**: kebab-case, unique across the project
|
|
354
|
+
- **Tags**: always set — `["smoke"]`, `["api"]`, `["e2e"]`, `["auth"]`
|
|
355
|
+
- **Secrets**: use `secrets.require("KEY")` or `{{KEY}}` in configure. Never hardcode.
|
|
356
|
+
- **Cleanup**: any test that creates data MUST have `.teardown()`
|
|
357
|
+
- **Data**: keep test data in `data/`, not inline in test files
|
|
358
|
+
- **Imports**: use `.ts` extensions for local files
|
|
359
|
+
- **One export per behavior**: each `export const` is one test case
|
|
360
|
+
|
|
361
|
+
## Coverage expectations
|
|
362
|
+
|
|
363
|
+
For each endpoint, consider:
|
|
364
|
+
|
|
365
|
+
- Success path (200/201)
|
|
366
|
+
- Auth boundary (401/403) — missing or invalid credentials
|
|
367
|
+
- Validation boundary (400/422) — invalid input
|
|
368
|
+
- Not-found boundary (404) — nonexistent resource
|
|
369
|
+
|
|
370
|
+
Cover all applicable boundaries unless the user asks for a narrower scope. If the spec lacks detail for a negative case,
|
|
371
|
+
say so explicitly.
|
|
372
|
+
|
|
373
|
+
## Anti-patterns to avoid
|
|
374
|
+
|
|
375
|
+
- Hardcoded secrets or base URLs
|
|
376
|
+
- Using raw `fetch()` instead of `ctx.http` or configured client
|
|
377
|
+
- No tags on tests
|
|
378
|
+
- Creating resources without teardown cleanup
|
|
379
|
+
- Guessing endpoint paths — always check the spec first
|
|
380
|
+
- Using `jsr:` URLs instead of `@glubean/sdk` alias
|
|
381
|
+
- Using `any` or `unknown` for HTTP response types — always provide a type parameter: `.json<{ id: string }>()`, not
|
|
382
|
+
`.json<any>()`. The SDK is fully typed; if you know the shape from the spec, type it.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"normal": {
|
|
3
|
+
"body": { "firstName": "Alice", "lastName": "Smith", "age": 28 },
|
|
4
|
+
"query": { "org": "acme" }
|
|
5
|
+
},
|
|
6
|
+
"young": {
|
|
7
|
+
"body": { "firstName": "Bob", "lastName": "Junior", "age": 18 },
|
|
8
|
+
"query": { "org": "acme" }
|
|
9
|
+
},
|
|
10
|
+
"edge-case": {
|
|
11
|
+
"body": { "firstName": "", "lastName": "", "age": -1 },
|
|
12
|
+
"query": { "org": "test" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Scenarios for data-driven API tests
|
|
2
|
+
# Each entry describes one test case
|
|
3
|
+
- id: get-first-product
|
|
4
|
+
method: GET
|
|
5
|
+
path: /products/1
|
|
6
|
+
expected: 200
|
|
7
|
+
description: Fetch first product
|
|
8
|
+
|
|
9
|
+
- id: get-missing-product
|
|
10
|
+
method: GET
|
|
11
|
+
path: /products/99999
|
|
12
|
+
expected: 404
|
|
13
|
+
description: Non-existent product returns 404
|
|
14
|
+
|
|
15
|
+
- id: get-product-categories
|
|
16
|
+
method: GET
|
|
17
|
+
path: /products/categories
|
|
18
|
+
expected: 200
|
|
19
|
+
description: Product categories should be accessible
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"by-name": {
|
|
3
|
+
"q": "phone",
|
|
4
|
+
"expected": { "minResults": 1, "titleContains": "phone" }
|
|
5
|
+
},
|
|
6
|
+
"by-category": {
|
|
7
|
+
"q": "laptop",
|
|
8
|
+
"expected": { "minResults": 1, "titleContains": "laptop" }
|
|
9
|
+
},
|
|
10
|
+
"no-results": {
|
|
11
|
+
"q": "zzzznotreal",
|
|
12
|
+
"expected": { "minResults": 0, "titleContains": "" }
|
|
13
|
+
}
|
|
14
|
+
}
|