Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.*.sw[a-z]
http-assert
/http-assert
dist/
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version: 2

builds:
- id: http-assert
main: ./cmd/http-assert
# A tool whose job is to run inside pipelines and scratch images has no
# business linking against a libc that might not be there.
env:
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ are marked **Breaking** and listed first in their section.

## [Unreleased]

### Added

- A reusable Go package at `github.com/korya/http-assert` exposes the HTTP
client, structured results and all response assertion constructors. The
library invokes its configured HTTP client once and leaves retries, logging
and presentation to its caller.
- `ha.Must(...)` keeps assertions built from static expressions inline while
preserving the constructors' explicit error returns for runtime input.

### Changed

- **Breaking for source installs:** the CLI now lives at
`github.com/korya/http-assert/cmd/http-assert`; use
`go install github.com/korya/http-assert/cmd/http-assert@latest`. Published
release archives and the `http-assert` binary name are unchanged.
- Assertion failures and evaluation errors are structured library data. The
CLI owns human-readable formatting and preserves its existing output.
- The library's zero-value client uses a 20-second total request timeout instead
of the unbounded `http.DefaultClient`. Callers can still inject an HTTP client
or apply a shorter request-context deadline.

## [0.3.0] - 2026-08-11

### Added
Expand Down
19 changes: 11 additions & 8 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ default:

[doc("Build the binary")]
build:
go build -o http-assert .
go build -o http-assert ./cmd/http-assert

[doc("Build for release (optimized)")]
build-release:
go build -ldflags="-s -w" -o http-assert .
go build -ldflags="-s -w" -o http-assert ./cmd/http-assert

[doc("Run every platform-independent check: build, tidy, config, vet, lint, security")]
static-checks: build tidy-check lint-config-check vet lint security
Expand All @@ -18,7 +18,7 @@ pre-commit: static-checks test test-race

[doc("Build and check compilation without creating binary")]
check:
go build -o /dev/null .
go build ./...

[doc("Fail if go.mod or go.sum is not tidy")]
tidy-check:
Expand Down Expand Up @@ -96,7 +96,7 @@ test:

[doc("Run the end-to-end suite (builds and executes the CLI)")]
test-e2e:
go test ./... -e2e -count=1
go test ./cmd/http-assert -e2e -count=1

[doc("Run tests with race detection")]
test-race:
Expand All @@ -112,7 +112,8 @@ test-cover:
set -euo pipefail
unit=$(mktemp -d); e2e=$(mktemp -d)
trap 'rm -rf "$unit" "$e2e"' EXIT
E2E_COVERDIR="$e2e" go test ./... -e2e -count=1 -cover -args -test.gocoverdir="$unit"
go test ./... -cover -args -test.gocoverdir="$unit"
E2E_COVERDIR="$e2e" go test ./cmd/http-assert -e2e -count=1 -cover -args -test.gocoverdir="$unit"
go tool covdata percent -i="$unit,$e2e"

[doc("Run tests with coverage and generate HTML report")]
Expand All @@ -121,7 +122,8 @@ test-coverage:
set -euo pipefail
unit=$(mktemp -d); e2e=$(mktemp -d)
trap 'rm -rf "$unit" "$e2e"' EXIT
E2E_COVERDIR="$e2e" go test ./... -e2e -count=1 -cover -args -test.gocoverdir="$unit"
go test ./... -cover -args -test.gocoverdir="$unit"
E2E_COVERDIR="$e2e" go test ./cmd/http-assert -e2e -count=1 -cover -args -test.gocoverdir="$unit"
go tool covdata textfmt -i="$unit,$e2e" -o=coverage.out
go tool cover -html=coverage.out -o coverage.html
echo "Coverage report: coverage.html"
Expand All @@ -132,7 +134,8 @@ test-cover-func:
set -euo pipefail
unit=$(mktemp -d); e2e=$(mktemp -d)
trap 'rm -rf "$unit" "$e2e"' EXIT
E2E_COVERDIR="$e2e" go test ./... -e2e -count=1 -cover -args -test.gocoverdir="$unit" >/dev/null
go test ./... -cover -args -test.gocoverdir="$unit" >/dev/null
E2E_COVERDIR="$e2e" go test ./cmd/http-assert -e2e -count=1 -cover -args -test.gocoverdir="$unit" >/dev/null
go tool covdata func -i="$unit,$e2e" | sort -k2 -n

[doc("Clean build artifacts")]
Expand All @@ -141,7 +144,7 @@ clean:

[doc("Install the binary to $GOPATH/bin")]
install:
go install .
go install ./cmd/http-assert

[doc("Format Go code")]
fmt:
Expand Down
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ different answer, the deviation is deliberate, and
## Contents

- [Installation](#installation)
- [Go Library](#go-library)
- [Usage](#usage): [request options](#request-options),
[assertions](#assertion-options), [JSON](#json-assertions),
[redirects](#redirects), [retries](#retries), [compression](#compression),
Expand Down Expand Up @@ -119,9 +120,58 @@ http-assert completion zsh --help # per-shell install instructions
Requires Go 1.26 or newer. A release binary needs no Go toolchain at all.

```bash
go install github.com/korya/http-assert@latest
go install github.com/korya/http-assert/cmd/http-assert@latest
```

## Go Library

The same assertions are available to Go programs. Import the package as `ha`
to keep calls compact and distinct from `net/http`:

```go
package healthcheck

import (
"net/http"

ha "github.com/korya/http-assert"
)

func Check(url string) (*ha.Result, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

return (ha.Client{}).Do(
req,
ha.AssertStatusOK(),
ha.AssertHeaderEqual("Content-Type", "application/json"),
ha.Must(ha.AssertJQ(`.status == "healthy"`)),
)
}
```

Constructors that parse a status expression, regular expression or jq query
return `(ha.Assertion, error)`. `ha.Must(...)` keeps static, programmer-owned
expressions inline; it panics on invalid input, so runtime values should handle
the constructor error normally.

`Client.Do` calls the configured HTTP client once and never retries. A returned
error means no complete response was available, such as a transport or
body-read failure. With a nil error, `Result.Outcomes` contains one result per
assertion, in call order; `Result.Passed()` is the convenient aggregate verdict.
Failures expose a code, kind, target, expected value and actual value instead of
preformatted text, so the calling application controls presentation. Evaluation
errors such as invalid JSON remain distinct from responses that were evaluated
and failed.

The zero-value client uses a shared HTTP client with a 20-second total timeout,
covering connection setup, redirects and response-body reads. Supply
`HTTPClient` to choose another timeout, transport, TLS or redirect policy; a
request-context deadline can impose a shorter per-call bound. Retry policy,
logging and CLI output intentionally remain outside the library API.

## Usage

### Basic Syntax
Expand Down Expand Up @@ -655,7 +705,7 @@ v3.0; see [LICENSE](LICENSE).
```bash
git clone https://github.com/korya/http-assert.git
cd http-assert
go build -o http-assert .
go build -o http-assert ./cmd/http-assert
```

### Working on the Code
Expand All @@ -671,7 +721,8 @@ just test-cover # merged unit + end-to-end coverage
```

The end-to-end tests are opt-in: `go test ./...` runs the unit tests only, and
`-e2e` (or the recipes above) switches the full suite on.
`go test ./cmd/http-assert -e2e` (or the recipes above) switches the full suite
on.

### Releasing

Expand Down
42 changes: 42 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package httpassert_test

import (
"fmt"
"net/http"

ha "github.com/korya/http-assert"
)

type exampleTransport func(*http.Request) (*http.Response, error)

func (f exampleTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}

func ExampleClient_Do() {
req, _ := http.NewRequest(http.MethodGet, "https://example.test/health", nil)
client := ha.Client{HTTPClient: &http.Client{Transport: exampleTransport(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNoContent,
Status: "204 No Content",
Header: make(http.Header),
Body: http.NoBody,
Request: req,
}, nil
})}}

result, err := client.Do(req, ha.AssertStatusOK(), ha.AssertBodyEmpty())
fmt.Println(err)
fmt.Println(result.Passed())

// Output:
// <nil>
// true
}

func ExampleMust() {
assertion := ha.Must(ha.AssertJQ(`.status == "healthy"`))
fmt.Println(assertion.Kind())

// Output: jq
}
Loading
Loading