diff --git a/assertions.go b/assertions.go index d53f801..7108da3 100644 --- a/assertions.go +++ b/assertions.go @@ -142,11 +142,15 @@ func AssertBodyEqual(expContent string) Assertion { return err } - if len(body) == 0 { - return fmt.Errorf("body: expected %q, missing", expContent) - } - if c := string(body); expContent != c { + // "missing" reads better than `got ""` for the common case of an + // empty body where content was expected, but it is a wording + // choice inside the failure -- deciding the verdict on it is what + // made --assert-body-eq '' impossible to satisfy (#22). + if len(body) == 0 { + return fmt.Errorf("body: expected %q, missing", expContent) + } + return fmt.Errorf("body: expected %q, got %q", expContent, c) } @@ -166,11 +170,14 @@ func AssertBodyMatch(expPattern string) (Assertion, error) { return err } - if len(body) == 0 { - return fmt.Errorf("body: expected to match %q, missing", expPattern) - } - if c := string(body); !re.MatchString(c) { + // As above: an empty body is a legitimate subject for a pattern. + // `^$`, `.*` and `\A\z` all match it, and none of them could pass + // while emptiness was checked before the pattern was. + if len(body) == 0 { + return fmt.Errorf("body: expected to match %q, missing", expPattern) + } + return fmt.Errorf("body: expected to match %q, got %q", expPattern, c) } diff --git a/assertions_test.go b/assertions_test.go index 4deeae2..b46aec5 100644 --- a/assertions_test.go +++ b/assertions_test.go @@ -284,6 +284,57 @@ func Test_AssertBody(t *testing.T) { } } +// Test_AssertBody_emptyIsAssertable covers the expectations that an empty body +// satisfies. They were unreachable while emptiness was checked before the +// comparison: the guard existed to word the failure nicely and ended up +// deciding it (#22). +func Test_AssertBody_emptyIsAssertable(t *testing.T) { + t.Parallel() + + // Patterns an empty body legitimately matches. `.*` is the one a user is + // most likely to reach for, `^$` the one they mean. + patterns := []string{"^$", ".*", `\A\z`, ""} + + t.Run("equal to the empty string", func(t *testing.T) { + res := &httpResponse{BodyBytes: []byte{}} + checkErr(t, "equal", AssertBodyEqual("")(res), "") + + // And a nil body, which is what a 204 produces. + checkErr(t, "equal, nil body", AssertBodyEqual("")(&httpResponse{}), "") + }) + + for _, p := range patterns { + t.Run("matching "+strconv.Quote(p), func(t *testing.T) { + a, err := AssertBodyMatch(p) + if err != nil { + t.Fatalf("cannot build the assertion: %s", err) + } + + checkErr(t, "match", a(&httpResponse{BodyBytes: []byte{}}), "") + checkErr(t, "match, nil body", a(&httpResponse{}), "") + }) + } + + // The verdict moved; the wording did not. A body that is empty when + // something was expected still reads as "missing" rather than `got ""`. + t.Run("an empty body still reports as missing", func(t *testing.T) { + res := &httpResponse{BodyBytes: []byte{}} + checkErr(t, "equal", AssertBodyEqual("value")(res), `body: expected "value", missing`) + + a, err := AssertBodyMatch("^value$") + if err != nil { + t.Fatalf("cannot build the assertion: %s", err) + } + checkErr(t, "match", a(res), `body: expected to match "^value$", missing`) + }) + + // The inverse must keep failing: a non-empty body is not the empty string. + t.Run("a non-empty body does not equal the empty string", func(t *testing.T) { + res := &httpResponse{BodyBytes: []byte("x")} + checkErr(t, "equal", AssertBodyEqual("")(res), `body: expected "", got "x"`) + }) +} + func Test_AssertRedirect(t *testing.T) { t.Parallel() diff --git a/e2e_assert_test.go b/e2e_assert_test.go index 2d67df0..ff629fe 100644 --- a/e2e_assert_test.go +++ b/e2e_assert_test.go @@ -221,3 +221,32 @@ func TestE2ELargePayloadCropped(t *testing.T) { assertContains(t, r, "Payload is cropped") assertContains(t, r, "4744 bytes are hidden") } + +// TestE2EAssertEmptyBody covers the expectations an empty body satisfies. +// +// They were unreachable until #22: both --assert-body-eq and --assert-body +// checked whether the body was empty before checking what was asked of it, so +// a 204 could not be asserted to have the body a 204 is defined to have. +func TestE2EAssertEmptyBody(t *testing.T) { + t.Run("--assert-body-eq '' passes against a 204", func(t *testing.T) { + assertExit(t, run(t, nil, "--assert-body-eq", "", url("/empty")), exitOK) + }) + + t.Run("--assert-body '^$' passes against a 204", func(t *testing.T) { + assertExit(t, run(t, nil, "--assert-body", "^$", url("/empty")), exitOK) + }) + + // The inverse still fails, so the fix did not simply stop checking. + t.Run("--assert-body-eq '' fails against a body", func(t *testing.T) { + r := run(t, nil, "--assert-body-eq", "", url("/ok")) + assertExit(t, r, exitRequestFail) + assertContains(t, r, `body: expected "", got`) + }) + + // And the wording that the old guard existed to produce is still there. + t.Run("an empty body still reports as missing", func(t *testing.T) { + r := run(t, nil, "--assert-body-eq", "value", url("/empty")) + assertExit(t, r, exitRequestFail) + assertContains(t, r, `body: expected "value", missing`) + }) +} diff --git a/e2e_known_issues_test.go b/e2e_known_issues_test.go index d821d15..34d7763 100644 --- a/e2e_known_issues_test.go +++ b/e2e_known_issues_test.go @@ -76,16 +76,6 @@ func TestKnownIssue20AssertOkAcceptsRedirects(t *testing.T) { } } -// TestKnownIssue22EmptyBodyEqualsNeverPasses: --assert-body-eq "" short-circuits -// on an empty body and reports that the empty string is "missing". -func TestKnownIssue22EmptyBodyEqualsNeverPasses(t *testing.T) { - characterizes(t, 22, `--assert-body-eq "" cannot pass, even against a 204`) - - r := run(t, nil, "--assert-body-eq", "", url("/empty")) - assertExit(t, r, exitRequestFail) - assertContains(t, r, `body: expected "", missing`) -} - // TestKnownIssue23WildcardMaphostUnreachable: hostMapping.Matches handles "*" // and "*:*", but the parser rejects both, so the branches are dead code. func TestKnownIssue23WildcardMaphostUnreachable(t *testing.T) {