Skip to content

Commit 6469007

Browse files
committed
initial wip
0 parents  commit 6469007

49 files changed

Lines changed: 12376 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
23+
- uses: goreleaser/goreleaser-action@v6
24+
with:
25+
version: latest
26+
args: release --clean
27+
env:
28+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.goreleaser.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
version: 2
2+
3+
before:
4+
hooks:
5+
- go mod tidy
6+
7+
builds:
8+
- id: httpsuite
9+
main: .
10+
binary: httpsuite
11+
ldflags:
12+
- -s -w
13+
- -X main.version={{.Version}}
14+
- -X main.commit={{.Commit}}
15+
- -X main.date={{.Date}}
16+
env:
17+
- CGO_ENABLED=0
18+
goos:
19+
- linux
20+
- darwin
21+
goarch:
22+
- amd64
23+
- arm64
24+
25+
archives:
26+
- formats: [tar.gz]
27+
name_template: "httpsuite_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
28+
files:
29+
- README.md
30+
- LICENSE
31+
32+
checksum:
33+
name_template: "httpsuite_{{ .Version }}_checksums.txt"
34+
algorithm: sha256
35+
36+
brews:
37+
- name: httpsuite
38+
repository:
39+
owner: uradical
40+
name: homebrew-tap
41+
homepage: https://httpsuite.dev
42+
description: "JetBrains HTTP Client compatible test runner for CI pipelines"
43+
license: MIT
44+
test: |
45+
system "#{bin}/httpsuite --version"
46+
47+
release:
48+
github:
49+
owner: uradical
50+
name: httpsuite
51+
draft: false
52+
prerelease: auto
53+
name_template: "httpsuite v{{.Version}}"

README.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# httpsuite
2+
3+
Run `.http` files as API tests, for local development and CI. A single binary
4+
with no runtime dependencies beyond the Go standard library and two uRadical
5+
modules ([httpparser](../httpparser) for parsing, [webbrowser](../webbrowser)
6+
for the forthcoming `--ui`).
7+
8+
httpsuite executes the requests in your `.http` files, checks any `# @expect`
9+
assertions you attach, and reports pass/fail with the right output for where
10+
it's running — coloured and aligned on a terminal, plain and diffable in CI.
11+
12+
## Install
13+
14+
```sh
15+
go install github.com/uradical/httpsuite@latest
16+
```
17+
18+
Requires Go 1.21 or later.
19+
20+
## Usage
21+
22+
```
23+
httpsuite [--var key=value]... [--ui] [path]
24+
```
25+
26+
- `path` is optional and defaults to the current directory.
27+
- `--var key=value` overrides a `{{placeholder}}`. Repeatable.
28+
- `--ui` is reserved for a results UI and currently prints a notice and exits.
29+
30+
```sh
31+
httpsuite # discover and run in the current directory
32+
httpsuite ./api # run a directory
33+
httpsuite ./api/users.http # run a single file
34+
httpsuite --var token=abc ./api # override {{token}}
35+
```
36+
37+
Exit code is **0** when everything passes, **1** when any request or assertion
38+
fails, and **2** on a usage or setup error.
39+
40+
## Discovery
41+
42+
When `path` is a directory (or omitted), httpsuite decides what to run:
43+
44+
1. If a **`httpsuite.yaml`** exists in the directory, it is used as the suite
45+
definition.
46+
2. Otherwise every **`*.http`** file in the directory is run serially, in
47+
sorted order.
48+
49+
When `path` is a single file, only that file is run.
50+
51+
## Suite definition (`httpsuite.yaml`)
52+
53+
```yaml
54+
parallel: true # run groups concurrently (default false)
55+
timeout: 30s # global per-request timeout (default 10s)
56+
groups:
57+
- name: auth
58+
serial: true # opt this group out of parallel execution
59+
files:
60+
- auth/login.http
61+
- auth/refresh.http
62+
- name: users
63+
timeout: 5s # override the timeout for this group
64+
files:
65+
- users/list.http
66+
- users/create.http
67+
```
68+
69+
- Files within a group always run serially, in order.
70+
- `serial: true` keeps a group off the parallel path even when `parallel` is on.
71+
- `timeout` may be set globally and overridden per group.
72+
73+
## Variables
74+
75+
`{{placeholder}}` tokens in a request's URL, headers, and body are resolved at
76+
execution time. Resolution order, from lowest to highest precedence:
77+
78+
1. File-level `@key = value` declarations inside the `.http` file
79+
2. OS environment variables
80+
3. `--var key=value` flags
81+
82+
A placeholder that is still unresolved at execution time fails that request.
83+
84+
## Assertions (`# @expect`)
85+
86+
Attach checks to a request with `# @expect` comments. All assertions on a
87+
request are evaluated — evaluation never stops at the first failure — and a
88+
request fails if **any** assertion fails. A request with no assertions passes as
89+
long as it returns any HTTP response.
90+
91+
```http
92+
# @name createUser
93+
POST {{baseUrl}}/users
94+
Content-Type: application/json
95+
# @expect status == 201
96+
# @expect header Location exists
97+
# @expect body.id number
98+
# @expect body.name == "Ada Lovelace"
99+
# @expect body.roles length > 0
100+
# @expect duration < 500
101+
102+
{ "name": "Ada Lovelace" }
103+
```
104+
105+
### Status
106+
107+
```http
108+
# @expect status == 201 # == != < > <= >=
109+
# @expect status 2xx # 2xx 3xx 4xx 5xx range match
110+
```
111+
112+
### Headers
113+
114+
```http
115+
# @expect header X-Request-Id exists
116+
# @expect header Content-Type == application/json # case-insensitive contains
117+
```
118+
119+
### Body
120+
121+
The response body is parsed as JSON. Paths use dot notation with array indexing,
122+
e.g. `body.items[0].id`.
123+
124+
```http
125+
# @expect body.id exists
126+
# @expect body.id == 10 # numeric compare: == != < > <= >=
127+
# @expect body.name == "Alan Bradley" # quoted string
128+
# @expect body.active == true # boolean
129+
# @expect body.items[0].name == "Alan" # array indexing
130+
131+
# type checks
132+
# @expect body.count number # integer, no decimal point
133+
# @expect body.price double # number with a decimal point
134+
# @expect body.name string
135+
# @expect body.active boolean
136+
# @expect body.address object
137+
# @expect body.items array
138+
139+
# length of an array or string
140+
# @expect body.items length 3
141+
# @expect body.items length > 0
142+
143+
# dates
144+
# @expect body.created date # ISO 8601 by default
145+
# @expect body.created date "YYYY-MM-DD"
146+
# @expect body.timestamp date "YYYY-MM-DD HH:mm"
147+
```
148+
149+
Date format tokens: `YYYY MM DD` (date) and `HH mm ss` (time).
150+
151+
### Duration
152+
153+
```http
154+
# @expect duration < 500 # milliseconds elapsed for the request
155+
```
156+
157+
> JetBrains `> {% ... %}` response-handler scripts are not executed. When one is
158+
> found, httpsuite prints a warning and continues — use `# @expect` instead.
159+
160+
## Output
161+
162+
On a terminal, results are coloured (green pass, red fail, bold summary) with
163+
``/`✗` marks on assertion lines. With no TTY (CI), the same layout is printed
164+
without ANSI codes and with `PASS`/`FAIL` words instead of glyphs:
165+
166+
```
167+
FAIL POST https://api.example.com/users 422 89ms
168+
PASS status 2xx
169+
FAIL status == 201 expected 201, got 422
170+
FAIL body.id exists field not present in response
171+
PASS body.name == "Alan"
172+
173+
1 requests 0 passed 1 failed 89ms
174+
```
175+
176+
## Development
177+
178+
```sh
179+
go test ./... # unit tests
180+
go vet ./...
181+
```

example/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# httpsuite example
2+
3+
A self-contained demo: a tiny in-memory REST API (stdlib `net/http` only) and a
4+
`.http` suite that exercises it with httpsuite.
5+
6+
```
7+
example/
8+
server/main.go # users API — GET/POST/PUT/PATCH/DELETE over an in-memory map
9+
api/ # the .http suite that tests it
10+
httpsuite.yaml
11+
list.http get.http create.http put.http patch.http delete.http not-found.http
12+
```
13+
14+
## The API
15+
16+
`server` exposes a `users` resource backed by a mutex-guarded map, seeded with
17+
two users (id `1` and `2`) on startup:
18+
19+
| Method | Path | Behaviour |
20+
| ------ | ------------- | ------------------------------------------- |
21+
| GET | `/users` | `200``{ "count", "users": [...] }` |
22+
| POST | `/users` | `201` — created user + `Location` header |
23+
| GET | `/users/{id}` | `200` or `404` |
24+
| PUT | `/users/{id}` | `200` (full replace) or `404` |
25+
| PATCH | `/users/{id}` | `200` (partial update) or `404` |
26+
| DELETE | `/users/{id}` | `204` or `404` |
27+
28+
## Run the demo
29+
30+
Start the server:
31+
32+
```sh
33+
go run ./example/server # listens on :8080
34+
# or choose a port: go run ./example/server --addr 127.0.0.1:9000
35+
```
36+
37+
In another terminal, run the suite:
38+
39+
```sh
40+
go run . ./example/api # or: httpsuite ./example/api
41+
```
42+
43+
You should see every request pass:
44+
45+
```
46+
PASS GET http://127.0.0.1:8080/users 200 1ms
47+
PASS status == 200
48+
PASS body.users[0].name == "Alan Bradley"
49+
...
50+
7 requests 7 passed 0 failed 3ms
51+
```
52+
53+
The requests default to `http://127.0.0.1:8080` via a file-level
54+
`@baseUrl` variable. Point the suite elsewhere without editing the files:
55+
56+
```sh
57+
go run . --var baseUrl=http://127.0.0.1:9000 ./example/api
58+
```
59+
60+
## A note on ordering and state
61+
62+
The suite runs as a single `serial` group in a deliberate order — list, read,
63+
create, replace, update, delete, then confirm the delete returns `404`. Because
64+
the requests mutate shared server state, **run them against a freshly started
65+
server**. Running the suite a second time against the same process will fail
66+
(e.g. `PUT /users/2` returns `404` because the earlier run deleted it) — which
67+
is exactly httpsuite doing its job:
68+
69+
```
70+
FAIL PUT http://127.0.0.1:8080/users/2 404 0ms
71+
FAIL status == 200 expected 200, got 404
72+
```
73+
74+
Restart the server to reset the seed data.

example/api/create.http

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@baseUrl = http://127.0.0.1:8080
2+
3+
# @name createUser
4+
POST {{baseUrl}}/users
5+
Content-Type: application/json
6+
# @expect status == 201
7+
# @expect status 2xx
8+
# @expect header Location exists
9+
# @expect header Content-Type == application/json
10+
# @expect body.id exists
11+
# @expect body.id number
12+
# @expect body.name == "Grace Hopper"
13+
# @expect body.email == "grace@example.com"
14+
# @expect body.active == false
15+
16+
{
17+
"name": "Grace Hopper",
18+
"email": "grace@example.com",
19+
"active": false
20+
}

example/api/delete.http

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@baseUrl = http://127.0.0.1:8080
2+
3+
# @name deleteUser
4+
DELETE {{baseUrl}}/users/2
5+
# @expect status == 204
6+
# @expect status 2xx
7+
# @expect duration < 2000

example/api/get.http

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@baseUrl = http://127.0.0.1:8080
2+
3+
# @name getUser
4+
GET {{baseUrl}}/users/1
5+
# @expect status == 200
6+
# @expect header Content-Type == application/json
7+
# @expect body.id == 1
8+
# @expect body.name == "Alan Bradley"
9+
# @expect body.email == "alan@uradical.io"
10+
# @expect body.active == true
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "../../../httpparser"
5+
},
6+
{
7+
"path": "../.."
8+
},
9+
{
10+
"path": "../../../webbrowser"
11+
}
12+
],
13+
"settings": {}
14+
}

0 commit comments

Comments
 (0)