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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ http-assert [flags] <URL>

| Flag | Description |
|------|-------------|
| `--assert-ok` | Assert 2xx status code |
| `--assert-ok` | Assert the status is not an error (2xx or 3xx) |
| `--assert-status` | Assert specific status code |
| `--assert-header` | Assert header matches regex pattern |
| `--assert-header-eq` | Assert header equals exact value |
Expand Down Expand Up @@ -238,6 +238,10 @@ http-assert --assert-ok https://api.example.com

**A command-line flag always wins over the environment**, which in turn wins over the built-in default. An empty variable counts as unset.

#### Proxies

`HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` are honoured for the request itself, through Go's standard proxy resolution. There is no flag for them, and no way to disable the behaviour from the command line — if one of these is set in your environment for unrelated reasons, requests go through it.

**A value that does not parse is rejected** rather than ignored, so a typo cannot silently change behaviour:

```console
Expand Down
65 changes: 65 additions & 0 deletions e2e_help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main_test

import (
"fmt"
"strings"
"testing"
)

// --help is the only reference available at the terminal, and the two things
// this tool's callers most need from it -- what the exit code will be, and
// what can be set without touching the command line -- were the two things it
// never mentioned (#39).
//
// The exit codes live in four places now: here, the README, the package
// comment, and the constants in the harness. Prose cannot be generated from
// the constants, so this test does the next best thing and fails when they
// stop agreeing.

func TestE2EHelpDocumentsTheContract(t *testing.T) {
r := run(t, nil, "--help")
assertExit(t, r, exitOK)

t.Run("every exit code the CLI can return is listed", func(t *testing.T) {
for _, code := range []int{exitOK, exitBadFlagVal, exitBadRequest, exitRequestFail, exitUsage} {
if !strings.Contains(r.Output(), fmt.Sprintf("\n %d ", code)) {
t.Errorf("--help does not document exit code %d", code)
}
}
})

// Exit 2 was the Go panic path. #61 removed it and TestE2ENoFlagPanics
// keeps it removed, so it is deliberately absent rather than overlooked.
t.Run("the panic exit code is not advertised", func(t *testing.T) {
if strings.Contains(r.Output(), "\n 2 ") {
t.Error("--help documents exit code 2, which the CLI can no longer return")
}
})

t.Run("every environment-backed option is named", func(t *testing.T) {
for _, tc := range envSupportedCases(t) {
if !strings.Contains(r.Output(), tc.EnvKey) {
t.Errorf("--help does not mention %s", tc.EnvKey)
}
}
})

// Proxying works through the transport's environment defaults and has no
// flag, so --help is the only place a user could learn it exists (#46).
t.Run("proxy support is discoverable", func(t *testing.T) {
for _, name := range []string{"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"} {
if !strings.Contains(r.Output(), name) {
t.Errorf("--help does not mention %s", name)
}
}
})

// The flag list is found by cutting the help output at "Flags:", so that
// string appearing in the prose above would silently point the parser at
// the wrong section -- and a probe that reads no flags passes.
t.Run("prose does not shadow the flag section", func(t *testing.T) {
if n := strings.Count(r.Output(), "Flags:"); n != 1 {
t.Fatalf("help output contains %d occurrences of \"Flags:\", want exactly 1", n)
}
})
}
69 changes: 58 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,48 @@ import (

func main() {
cmd := &cobra.Command{
Use: "http-assert <URL>",
Short: "Perform HTTP request and assert received HTTP response",
Use: "http-assert <URL>",
Short: "Perform HTTP request and assert received HTTP response",
// The exit code is the whole product, so it is documented where a
// person actually looks for it. The environment is here for the same
// reason: nothing else at the terminal reveals that six of these
// options can be set without touching the command line.
//
// Careful with the wording below: the end-to-end suite locates the
// flag list by cutting this output at the first "Flags:", so that
// exact string must not appear here (see e2e_panic_test.go).
Long: `Perform an HTTP request and assert properties of the response.

Assertions are declared as flags. The request is made once and checked
against all of them, and every failure is reported, not just the first.

Exit codes:
0 every assertion passed
71 a flag or environment value failed to parse
91 the request could not be constructed from the method and URL
93 the request failed, or at least one assertion did
103 wrong argument count, or an unknown flag

Environment:
Six options can also be set as HTTP_ASSERT_<NAME>, with dashes replaced by
underscores: HTTP_ASSERT_VERBOSE, HTTP_ASSERT_SILENT, HTTP_ASSERT_LOG_LEVEL,
HTTP_ASSERT_INSECURE, HTTP_ASSERT_MAX_TIME and HTTP_ASSERT_MAPHOST. Every
other option is command-line only.

The command line wins over the environment, which wins over the default. An
empty variable counts as unset, and a value that does not parse is rejected
rather than quietly replaced by a zero.

HTTP_PROXY, HTTPS_PROXY and NO_PROXY are honoured for the request itself.
There is no flag for them.`,
Example: ` # A health check: any non-error status passes
http-assert --assert-ok https://example.com/health

# Exact status plus a body pattern
http-assert --assert-status 201 --assert-body '"id":\s*[0-9]+' https://example.com/things

# Send a request to a specific backend, as curl --resolve does
http-assert --maphost 'example.com:443=127.0.0.1:8443' --assert-ok https://example.com/`,
Version: versionString(),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -100,13 +140,15 @@ func main() {
cmd.PersistentFlags().StringArray("maphost", nil,
"Provide a custom address for a specific host and port pair; "+
"e.g. <srchostname:srcport=dsthostname[:dstport]>")
cmd.PersistentFlags().BoolP("verbose", "v", false, "Be verbose; log info messages")
cmd.PersistentFlags().BoolP("silent", "s", false, "Be silent; log errors only")
cmd.PersistentFlags().BoolP("verbose", "v", false,
"Be verbose; log debug messages (same as --log-level debug)")
cmd.PersistentFlags().BoolP("silent", "s", false,
"Be silent; log error messages only (same as --log-level error)")
cmd.PersistentFlags().String("log-level", "",
"Set log level; possible values: debug, info (default), warn, error")
cmd.PersistentFlags().BoolP("insecure", "k", false, "Disable checking SSL certificates")
cmd.PersistentFlags().IntP("max-time", "m", 20,
"Maximum time in seconds that you allow each request to take")
"Maximum time in seconds that you allow each request to take")
cmd.Flags().StringP("request", "X", "GET", "Set method for HTTP request")
cmd.Flags().StringArrayP("header", "H", nil, "Set header for HTTP request")
cmd.Flags().StringP("data", "d", "",
Expand Down Expand Up @@ -254,17 +296,22 @@ func parseHostMappings(vals []string) ([]hostMapping, error) {

func registerAssertionFlags(cmd *cobra.Command) {
cmd.Flags().Int("assert-status", 0, "Assert response status equals the provided value")
cmd.Flags().StringArray("assert-header", nil, "Assert header equals the provided regexp")
cmd.Flags().StringArray("assert-header-eq", nil, "Assert header equals the provided regexp")
cmd.Flags().StringArray("assert-header", nil,
"Assert header matches the provided regexp; NAME alone asserts it is present")
cmd.Flags().StringArray("assert-header-eq", nil,
"Assert header equals the provided value; NAME alone asserts it is present")
cmd.Flags().StringArray("assert-header-missing", nil, "Assert header is missing")
cmd.Flags().String("assert-body", "", "Assert body equals the provided value")
cmd.Flags().String("assert-body", "", "Assert body matches the provided regexp")
cmd.Flags().String("assert-body-eq", "", "Assert body equals the provided value")
cmd.Flags().Bool("assert-body-empty", false, "Assert body is empty")

// Common shorthands
cmd.Flags().Bool("assert-ok", false, "Assert response is successful (2xx)")
cmd.Flags().String("assert-redirect", "", "Assert response redirects to the provided URL")
cmd.Flags().String("assert-redirect-eq", "", "Assert response redirects to the provided URL")
cmd.Flags().Bool("assert-ok", false,
"Assert response status is not an error (2xx or 3xx)")
cmd.Flags().String("assert-redirect", "",
"Assert redirect location matches the provided regexp")
cmd.Flags().String("assert-redirect-eq", "",
"Assert redirect location equals the provided URL")
}

// mustCompileAssertion builds a pattern-based assertion, reporting an
Expand Down
Loading