diff --git a/assert/assert_assertions.go b/assert/assert_assertions.go index 961ea56dc..fad856330 100644 --- a/assert/assert_assertions.go +++ b/assert/assert_assertions.go @@ -1624,6 +1624,8 @@ func IsType(t T, expectedType any, object any, msgAndArgs ...any) bool { // // Expected and actual must be valid JSON. // +// For dynamic redaction of the input text via a callback, use [JSONEqT]. +// // # Usage // // assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) @@ -1668,6 +1670,8 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool { // // Expected and actual must be valid JSON. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Usage // // assertions.JSONEqT(t, `{"hello": "world", "foo": "bar"}`, []byte(`{"foo": "bar", "hello": "world"}`)) @@ -1678,20 +1682,22 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool { // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { +func JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } return assertions.JSONEqT[EDoc, ADoc](t, expected, actual, msgAndArgs...) } -// JSONMarshalAsT wraps [JSONEq] after [json.Marshal]. +// JSONMarshalAsT wraps [JSONEqT] after [json.Marshal]. // // The input JSON may be a string or []byte. // // It fails if the marshaling returns an error or if the expected JSON bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -1708,7 +1714,7 @@ func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // failure: `[{"foo": "bar"}, {"hello": "world"}]`, 1 // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool { +func JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -1724,6 +1730,8 @@ func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed jazon value may be wrapped as a function to redact the input JSON dynamically. +// // # Usage // // expected := struct { @@ -1740,7 +1748,7 @@ func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // failure: 1, `[{"foo": "bar"}, {"hello": "world"}]` // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { +func JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -3258,6 +3266,8 @@ func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool { // "github.com/go-openapi/testify/enable/yaml/v2" // ) // +// For dynamic redaction of the input text via a callback, use [YAMLEqT]. +// // # Usage // // expected := `--- @@ -3291,13 +3301,15 @@ func YAMLEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool { // // See [YAMLEqBytes]. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Examples // // panic: "key: value", "key: value" // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { +func YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -3311,6 +3323,8 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // It fails if the marshaling returns an error or if the expected YAML bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -3327,7 +3341,7 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool { +func YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -3343,6 +3357,8 @@ func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed yamlDoc value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // expected := struct { @@ -3359,11 +3375,11 @@ func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { +func YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) bool { if h, ok := t.(H); ok { h.Helper() } - return assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, jazon, msgAndArgs...) + return assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, yamlDoc, msgAndArgs...) } // Zero asserts that i is the zero value for its type. diff --git a/assert/assert_format.go b/assert/assert_format.go index 0a14dfaa2..ecd1c28e3 100644 --- a/assert/assert_format.go +++ b/assert/assert_format.go @@ -658,7 +658,7 @@ func JSONEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) // JSONEqTf is the same as [JSONEqT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool { +func JSONEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -668,7 +668,7 @@ func JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args // JSONMarshalAsTf is the same as [JSONMarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool { +func JSONMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -678,7 +678,7 @@ func JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args // JSONUnmarshalAsTf is the same as [JSONUnmarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func JSONUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool { +func JSONUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, jazon ADoc, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -1348,7 +1348,7 @@ func YAMLEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) // YAMLEqTf is the same as [YAMLEqT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool { +func YAMLEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -1358,7 +1358,7 @@ func YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args // YAMLMarshalAsTf is the same as [YAMLMarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool { +func YAMLMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } @@ -1368,11 +1368,11 @@ func YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args // YAMLUnmarshalAsTf is the same as [YAMLUnmarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. -func YAMLUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool { +func YAMLUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msg string, args ...any) bool { if h, ok := t.(H); ok { h.Helper() } - return assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, jazon, forwardArgs(msg, args)) + return assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, yamlDoc, forwardArgs(msg, args)) } // Zerof is the same as [Zero], but it accepts a format string to format arguments like [fmt.Printf]. diff --git a/assert/assert_types.go b/assert/assert_types.go index acf7e0509..634aaca0c 100644 --- a/assert/assert_types.go +++ b/assert/assert_types.go @@ -86,6 +86,15 @@ type ( // for table driven tests. PanicAssertionFunc = assertions.PanicAssertionFunc + // RText extends [Text] by supporting dynamic construction of the + // expected or actual value, e.g. "redact" functions. + RText = assertions.RText + + // Redactor allows dynamic construction of expected or actual values, e.g. "redacting" values dynamically. + // + // This is used by json and yaml assertions. + Redactor = assertions.Redactor + // RegExp is either a text containing a regular expression to compile (string or []byte), or directly the compiled regexp. // // This is used by [RegexpT] and [NotRegexpT]. diff --git a/docs/doc-site/api/json.md b/docs/doc-site/api/json.md index 840464b7b..da3a84f1a 100644 --- a/docs/doc-site/api/json.md +++ b/docs/doc-site/api/json.md @@ -32,9 +32,9 @@ Generic assertions are marked with a {{% icon icon="star" color=orange %}}. ```tree - [JSONEq](#jsoneq) | angles-right - [JSONEqBytes](#jsoneqbytes) | angles-right -- [JSONEqT[EDoc, ADoc Text]](#jsoneqtedoc-adoc-text) | star | orange -- [JSONMarshalAsT[EDoc Text]](#jsonmarshalastedoc-text) | star | orange -- [JSONUnmarshalAsT[Object any, ADoc Text]](#jsonunmarshalastobject-any-adoc-text) | star | orange +- [JSONEqT[EDoc, ADoc RText]](#jsoneqtedoc-adoc-rtext) | star | orange +- [JSONMarshalAsT[EDoc RText]](#jsonmarshalastedoc-rtext) | star | orange +- [JSONUnmarshalAsT[Object any, ADoc RText]](#jsonunmarshalastobject-any-adoc-rtext) | star | orange ``` ### JSONEq{#jsoneq} @@ -42,6 +42,8 @@ JSONEq asserts that two JSON strings are semantically equivalent. Expected and actual must be valid JSON. +For dynamic redaction of the input text via a callback, use [JSONEqT](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqT). + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -146,7 +148,7 @@ func main() { |--|--| | [`assertions.JSONEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEq) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L63) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L66) {{% /tab %}} {{< /tabs >}} @@ -259,7 +261,7 @@ func main() { |--|--| | [`assertions.JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEqBytes) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEqBytes](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L25) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEqBytes](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L26) > **Maintainer Note** > @@ -271,13 +273,15 @@ func main() { {{% /tab %}} {{< /tabs >}} -### JSONEqT[EDoc, ADoc Text] {{% icon icon="star" color=orange %}}{#jsoneqtedoc-adoc-text} +### JSONEqT[EDoc, ADoc RText] {{% icon icon="star" color=orange %}}{#jsoneqtedoc-adoc-rtext} JSONEqT asserts that two JSON documents are semantically equivalent. The expected and actual arguments may be string or []byte. They do not need to be of the same type. Expected and actual must be valid JSON. +NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -363,33 +367,35 @@ func main() { {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqT) | package-level function | -| [`assert.JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqTf) | formatted variant | +| [`assert.JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqT) | package-level function | +| [`assert.JSONEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqT) | package-level function | -| [`require.JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqTf) | formatted variant | +| [`require.JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqT) | package-level function | +| [`require.JSONEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEqT) | internal implementation | +| [`assertions.JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEqT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEqT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L86) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEqT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L91) {{% /tab %}} {{< /tabs >}} -### JSONMarshalAsT[EDoc Text] {{% icon icon="star" color=orange %}}{#jsonmarshalastedoc-text} -JSONMarshalAsT wraps [JSONEq](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEq) after [json.Marshal](https://pkg.go.dev/json#Marshal). +### JSONMarshalAsT[EDoc RText] {{% icon icon="star" color=orange %}}{#jsonmarshalastedoc-rtext} +JSONMarshalAsT wraps [JSONEqT](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqT) after [json.Marshal](https://pkg.go.dev/json#Marshal). The input JSON may be a string or []byte. It fails if the marshaling returns an error or if the expected JSON bytes differ semantically from the expected ones. +NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -490,26 +496,26 @@ type dummyStruct struct { {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONMarshalAsT) | package-level function | -| [`assert.JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONMarshalAsTf) | formatted variant | +| [`assert.JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONMarshalAsT) | package-level function | +| [`assert.JSONMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONMarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONMarshalAsT) | package-level function | -| [`require.JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONMarshalAsTf) | formatted variant | +| [`require.JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONMarshalAsT) | package-level function | +| [`require.JSONMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONMarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONMarshalAsT) | internal implementation | +| [`assertions.JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONMarshalAsT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONMarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L153) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONMarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L162) {{% /tab %}} {{< /tabs >}} -### JSONUnmarshalAsT[Object any, ADoc Text] {{% icon icon="star" color=orange %}}{#jsonunmarshalastobject-any-adoc-text} +### JSONUnmarshalAsT[Object any, ADoc RText] {{% icon icon="star" color=orange %}}{#jsonunmarshalastobject-any-adoc-rtext} JSONUnmarshalAsT wraps [Equal](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Equal) after [json.Unmarshal](https://pkg.go.dev/json#Unmarshal). The input JSON may be a string or []byte. @@ -519,6 +525,8 @@ It fails if the unmarshaling returns an error or if the resulting object is not Be careful not to wrap the expected object into an "any" interface if this is not what you expected: the unmarshaling would take this type to unmarshal as a map[string](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#string)any. +NOTE: passed jazon value may be wrapped as a function to redact the input JSON dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -619,22 +627,22 @@ type dummyStruct struct { {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONUnmarshalAsT) | package-level function | -| [`assert.JSONUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONUnmarshalAsTf) | formatted variant | +| [`assert.JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONUnmarshalAsT) | package-level function | +| [`assert.JSONUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONUnmarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONUnmarshalAsT) | package-level function | -| [`require.JSONUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONUnmarshalAsTf) | formatted variant | +| [`require.JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONUnmarshalAsT) | package-level function | +| [`require.JSONUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONUnmarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONUnmarshalAsT) | internal implementation | +| [`assertions.JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONUnmarshalAsT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONUnmarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L118) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONUnmarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L125) {{% /tab %}} {{< /tabs >}} diff --git a/docs/doc-site/api/metrics.md b/docs/doc-site/api/metrics.md index cb39474d1..7e01cdb18 100644 --- a/docs/doc-site/api/metrics.md +++ b/docs/doc-site/api/metrics.md @@ -84,9 +84,9 @@ Table of core assertions, excluding variants. Each function is side by side with | [IsType](type/#istype) | [IsNotType](type/#isnottype) | type | | | [JSONEq](json/#jsoneq) | | json | | | [JSONEqBytes](json/#jsoneqbytes) | | json | | -| [JSONEqT[EDoc, ADoc Text]](json/#jsoneqtedoc-adoc-text) {{% icon icon="star" color=orange %}} | | json | | -| [JSONMarshalAsT[EDoc Text]](json/#jsonmarshalastedoc-text) {{% icon icon="star" color=orange %}} | | json | | -| [JSONUnmarshalAsT[Object any, ADoc Text]](json/#jsonunmarshalastobject-any-adoc-text) {{% icon icon="star" color=orange %}} | | json | | +| [JSONEqT[EDoc, ADoc RText]](json/#jsoneqtedoc-adoc-rtext) {{% icon icon="star" color=orange %}} | | json | | +| [JSONMarshalAsT[EDoc RText]](json/#jsonmarshalastedoc-rtext) {{% icon icon="star" color=orange %}} | | json | | +| [JSONUnmarshalAsT[Object any, ADoc RText]](json/#jsonunmarshalastobject-any-adoc-rtext) {{% icon icon="star" color=orange %}} | | json | | | [Kind](type/#kind) | [NotKind](type/#notkind) | type | | | [Len](collection/#len) | | collection | | | [MapContainsT[Map ~map[K]V, K comparable, V any]](collection/#mapcontainstmap-mapkv-k-comparable-v-any) {{% icon icon="star" color=orange %}} | [MapNotContainsT](collection/#mapnotcontainstmap-mapkv-k-comparable-v-any) | collection | | @@ -118,8 +118,8 @@ Table of core assertions, excluding variants. Each function is side by side with | [WithinRange](time/#withinrange) | | time | | | [YAMLEq](yaml/#yamleq) | | yaml | | | [YAMLEqBytes](yaml/#yamleqbytes) | | yaml | | -| [YAMLEqT[EDoc, ADoc Text]](yaml/#yamleqtedoc-adoc-text) {{% icon icon="star" color=orange %}} | | yaml | | -| [YAMLMarshalAsT[EDoc Text]](yaml/#yamlmarshalastedoc-text) {{% icon icon="star" color=orange %}} | | yaml | | -| [YAMLUnmarshalAsT[Object any, ADoc Text]](yaml/#yamlunmarshalastobject-any-adoc-text) {{% icon icon="star" color=orange %}} | | yaml | | +| [YAMLEqT[EDoc, ADoc RText]](yaml/#yamleqtedoc-adoc-rtext) {{% icon icon="star" color=orange %}} | | yaml | | +| [YAMLMarshalAsT[EDoc RText]](yaml/#yamlmarshalastedoc-rtext) {{% icon icon="star" color=orange %}} | | yaml | | +| [YAMLUnmarshalAsT[Object any, ADoc RText]](yaml/#yamlunmarshalastobject-any-adoc-rtext) {{% icon icon="star" color=orange %}} | | yaml | | | [Zero](type/#zero) | [NotZero](type/#notzero) | type | | diff --git a/docs/doc-site/api/yaml.md b/docs/doc-site/api/yaml.md index 9f81c7b84..43e6d93b1 100644 --- a/docs/doc-site/api/yaml.md +++ b/docs/doc-site/api/yaml.md @@ -32,9 +32,9 @@ Generic assertions are marked with a {{% icon icon="star" color=orange %}}. ```tree - [YAMLEq](#yamleq) | angles-right - [YAMLEqBytes](#yamleqbytes) | angles-right -- [YAMLEqT[EDoc, ADoc Text]](#yamleqtedoc-adoc-text) | star | orange -- [YAMLMarshalAsT[EDoc Text]](#yamlmarshalastedoc-text) | star | orange -- [YAMLUnmarshalAsT[Object any, ADoc Text]](#yamlunmarshalastobject-any-adoc-text) | star | orange +- [YAMLEqT[EDoc, ADoc RText]](#yamleqtedoc-adoc-rtext) | star | orange +- [YAMLMarshalAsT[EDoc RText]](#yamlmarshalastedoc-rtext) | star | orange +- [YAMLUnmarshalAsT[Object any, ADoc RText]](#yamlunmarshalastobject-any-adoc-rtext) | star | orange ``` ### YAMLEq{#yamleq} @@ -77,7 +77,7 @@ See [YAMLEqBytes](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAM |--|--| | [`assertions.YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLEq) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L77) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L79) {{% /tab %}} {{< /tabs >}} @@ -96,6 +96,8 @@ To enable it, you should add a blank import like so: "github.com/go-openapi/testify/enable/yaml/v2" ) +For dynamic redaction of the input text via a callback, use [YAMLEqT](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqT). + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -146,17 +148,19 @@ To enable it, you should add a blank import like so: |--|--| | [`assertions.YAMLEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLEqBytes) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEqBytes](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L46) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEqBytes](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L48) {{% /tab %}} {{< /tabs >}} -### YAMLEqT[EDoc, ADoc Text] {{% icon icon="star" color=orange %}}{#yamleqtedoc-adoc-text} +### YAMLEqT[EDoc, ADoc RText] {{% icon icon="star" color=orange %}}{#yamleqtedoc-adoc-rtext} YAMLEqT asserts that two YAML documents are equivalent. The expected and actual arguments may be string or []byte. They do not need to be of the same type. See [YAMLEqBytes](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqBytes). +NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Examples" %}} @@ -173,26 +177,26 @@ See [YAMLEqBytes](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAM {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqT) | package-level function | -| [`assert.YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqTf) | formatted variant | +| [`assert.YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqT) | package-level function | +| [`assert.YAMLEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEqT) | package-level function | -| [`require.YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEqTf) | formatted variant | +| [`require.YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEqT) | package-level function | +| [`require.YAMLEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEqTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLEqT) | internal implementation | +| [`assertions.YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLEqT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEqT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L96) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEqT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L100) {{% /tab %}} {{< /tabs >}} -### YAMLMarshalAsT[EDoc Text] {{% icon icon="star" color=orange %}}{#yamlmarshalastedoc-text} +### YAMLMarshalAsT[EDoc RText] {{% icon icon="star" color=orange %}}{#yamlmarshalastedoc-rtext} YAMLMarshalAsT wraps [YAMLEq](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEq) after [yaml.Marshal](https://pkg.go.dev/yaml#Marshal). The input YAML may be a string or []byte. @@ -200,6 +204,8 @@ The input YAML may be a string or []byte. It fails if the marshaling returns an error or if the expected YAML bytes differ semantically from the expected ones. +NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -226,26 +232,26 @@ from the expected ones. {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLMarshalAsT) | package-level function | -| [`assert.YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLMarshalAsTf) | formatted variant | +| [`assert.YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLMarshalAsT) | package-level function | +| [`assert.YAMLMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLMarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLMarshalAsT) | package-level function | -| [`require.YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLMarshalAsTf) | formatted variant | +| [`require.YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLMarshalAsT) | package-level function | +| [`require.YAMLMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLMarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLMarshalAsT) | internal implementation | +| [`assertions.YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLMarshalAsT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLMarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L163) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLMarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L171) {{% /tab %}} {{< /tabs >}} -### YAMLUnmarshalAsT[Object any, ADoc Text] {{% icon icon="star" color=orange %}}{#yamlunmarshalastobject-any-adoc-text} +### YAMLUnmarshalAsT[Object any, ADoc RText] {{% icon icon="star" color=orange %}}{#yamlunmarshalastobject-any-adoc-rtext} YAMLUnmarshalAsT wraps [Equal](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Equal) after [yaml.Unmarshal](https://pkg.go.dev/yaml#Unmarshal). The input YAML may be a string or []byte. @@ -255,6 +261,8 @@ It fails if the unmarshaling returns an error or if the resulting object is not Be careful not to wrap the expected object into an "any" interface if this is not what you expected: the unmarshaling would take this type to unmarshal as a map[string](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#string)any. +NOTE: passed yamlDoc value may be wrapped as a function to redact the input text dynamically. + {{% expand title="Examples" %}} {{< tabs >}} {{% tab title="Usage" %}} @@ -281,22 +289,22 @@ the unmarshaling would take this type to unmarshal as a map[string](https://pkg. {{% tab title="assert" style="secondary" %}} | Signature | Usage | |--|--| -| [`assert.YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLUnmarshalAsT) | package-level function | -| [`assert.YAMLUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLUnmarshalAsTf) | formatted variant | +| [`assert.YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLUnmarshalAsT) | package-level function | +| [`assert.YAMLUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLUnmarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="require" style="secondary" %}} | Signature | Usage | |--|--| -| [`require.YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLUnmarshalAsT) | package-level function | -| [`require.YAMLUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLUnmarshalAsTf) | formatted variant | +| [`require.YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLUnmarshalAsT) | package-level function | +| [`require.YAMLUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLUnmarshalAsTf) | formatted variant | {{% /tab %}} {{% tab title="internal" style="accent" icon="wrench" %}} | Signature | Usage | |--|--| -| [`assertions.YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLUnmarshalAsT) | internal implementation | +| [`assertions.YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLUnmarshalAsT) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLUnmarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L128) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLUnmarshalAsT](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L134) {{% /tab %}} {{< /tabs >}} diff --git a/docs/doc-site/usage/TRACKING.md b/docs/doc-site/usage/TRACKING.md index 29711fb04..009f895ad 100644 --- a/docs/doc-site/usage/TRACKING.md +++ b/docs/doc-site/usage/TRACKING.md @@ -20,7 +20,6 @@ We continue to monitor and selectively adopt changes from the upstream repositor ### Monitoring - 🔍 [#1601] - `NoFieldIsZero` -- 🔍 [#1840] - JSON presence check without exact values ### Superseded by Our Implementation - ⛔ [#1845] - Fix Eventually/Never regression (superseded by context-based pollCondition) @@ -30,7 +29,6 @@ We continue to monitor and selectively adopt changes from the upstream repositor [#1087]: https://github.com/stretchr/testify/pull/1087 [#1601]: https://github.com/stretchr/testify/issues/1601 -[#1840]: https://github.com/stretchr/testify/issues/1840 [#1830]: https://github.com/stretchr/testify/pull/1830 [#1824]: https://github.com/stretchr/testify/pull/1824 [#1819]: https://github.com/stretchr/testify/pull/1819 @@ -86,6 +84,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif | [#1606] | PR | Consistently assertion | ✅ Adapted | | [#1848] | PR | Subset (garbled error message) | ✅ Adapted | | [#1839] | PR | Number equality with symmetric role | ✅ Adapted | +| [#1840] | Issue | JSON presence check without exact values | ✅ Adapted | | [#1859] | Issue | Channel assertions | ✅ Adapted | [#994]: https://github.com/stretchr/testify/pull/994 @@ -102,6 +101,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif [#1087]: https://github.com/stretchr/testify/issues/1087 [#1606]: https://github.com/stretchr/testify/pull/1606 [#1839]: https://github.com/stretchr/testify/pull/1839 +[#1840]: https://github.com/stretchr/testify/issues/1840 [#1848]: https://github.com/stretchr/testify/pull/1848 [#1859]: https://github.com/stretchr/testify/pull/1859 @@ -121,7 +121,6 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif |-----------|------|---------|--------| | [#1576] | Issue/PR | `EqualValues` assertion | 🔍 Monitoring [#1863]- Wrong equality when comparing float32 and float64| | [#1601] | Issue | `NoFieldIsZero` assertion | 🔍 Monitoring - Considering implementation | -| [#1840] | Issue | JSON presence check without exact values | 🔍 Monitoring - Interesting for testing APIs with generated IDs | | [#1860] | Issue+PR | `ErrorAsType[E]` for Go 1.26+ - PR: [#1861] | 🔍 Monitoring - Interesting UX syntax | ### Informational (Not Implemented) @@ -147,9 +146,9 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif | Category | Count | |----------|-------| -| **Implemented/Merged** | 26 | +| **Implemented/Merged** | 27 | | **Superseded** | 4 | -| **Monitoring** | 4 | +| **Monitoring** | 3 | | **Informational** | 3 | | **Total Processed** | 37 | diff --git a/enable/yaml/assertions_test.go b/enable/yaml/assertions_test.go index f36f5cff1..a5173de80 100644 --- a/enable/yaml/assertions_test.go +++ b/enable/yaml/assertions_test.go @@ -4,6 +4,7 @@ package yaml import ( + "strings" "testing" target "github.com/go-openapi/testify/v2/assert" @@ -120,3 +121,75 @@ func TestYAMLUnmarshalAs(t *testing.T) { target.True(t, target.YAMLUnmarshalAsT(mock, value, `{"hello": "world", "foo": "bar"}`)) target.True(t, target.YAMLMarshalAsT(mock, `{"hello": "world", "foo": "bar"}`, value)) } + +// TestYAMLRedact covers the happy-path Redactor (`func() string` / +// `func() []byte`) arms of the [RText] type set for the YAML T-variants, +// which can only be exercised once the YAML feature is enabled. The +// nil-Redactor panic path is also re-checked here for YAMLMarshalAsT, +// since in the internal/assertions package yaml.Marshal panics first +// when the feature is disabled and the asBytes guard is unreachable. +func TestYAMLRedact(t *testing.T) { + t.Parallel() + + mock := new(testing.T) + type dummy struct { + Hello string `yaml:"hello"` + Foo string `yaml:"foo"` + } + value := dummy{Hello: "world", Foo: "bar"} + const doc = `{"hello": "world", "foo": "bar"}` + + t.Run("YAMLEqT with func() string Redactor", func(t *testing.T) { + t.Parallel() + red := func() string { return doc } + target.True(t, target.YAMLEqT(mock, red, doc)) + }) + + t.Run("YAMLEqT with func() []byte Redactor on actual", func(t *testing.T) { + t.Parallel() + red := func() []byte { return []byte(doc) } + target.True(t, target.YAMLEqT(mock, doc, red)) + }) + + // Real-world scenario: a redactor normalises a non-deterministic field + // before comparison. + t.Run("YAMLEqT redactor normalises field", func(t *testing.T) { + t.Parallel() + raw := `{"id": 42, "ts": "2026-04-26T15:30:45Z"}` + red := func() string { + return strings.Replace(raw, `"2026-04-26T15:30:45Z"`, `"REDACTED"`, 1) + } + expected := `{"id": 42, "ts": "REDACTED"}` + target.True(t, target.YAMLEqT(mock, expected, red)) + }) + + t.Run("YAMLUnmarshalAsT with Redactor", func(t *testing.T) { + t.Parallel() + red := func() []byte { return []byte(doc) } + target.True(t, target.YAMLUnmarshalAsT(mock, value, red)) + }) + + t.Run("YAMLMarshalAsT with Redactor", func(t *testing.T) { + t.Parallel() + red := func() string { return doc } + target.True(t, target.YAMLMarshalAsT(mock, red, value)) + }) + + t.Run("YAMLMarshalAsT with nil Redactor panics with clear message", func(t *testing.T) { + t.Parallel() + const wantMsg = "passed Redactor cannot be nil" + defer func() { + rec := recover() + if rec == nil { + t.Errorf("expected YAMLMarshalAsT to panic with nil Redactor, got no panic") + return + } + s, _ := rec.(string) + if !strings.Contains(s, wantMsg) { + t.Errorf("expected panic message to contain %q, got: %v", wantMsg, rec) + } + }() + var nilFn func() string + _ = target.YAMLMarshalAsT(mock, nilFn, value) + }) +} diff --git a/internal/assertions/generics.go b/internal/assertions/generics.go index 439737ed1..17953fb90 100644 --- a/internal/assertions/generics.go +++ b/internal/assertions/generics.go @@ -26,6 +26,19 @@ type ( ~string | ~[]byte } + // RText extends [Text] by supporting dynamic construction of the + // expected or actual value, e.g. "redact" functions. + RText interface { + Text | Redactor + } + + // Redactor allows dynamic construction of expected or actual values, e.g. "redacting" values dynamically. + // + // This is used by json and yaml assertions. + Redactor interface { + func() string | func() []byte + } + // Ordered is a standard ordered type (i.e. types that support "<": [cmp.Ordered]) plus []byte and [time.Time]. // // This is used by [GreaterT], [GreaterOrEqualT], [LessT], [LessOrEqualT], [IsIncreasingT], [IsDecreasingT]. diff --git a/internal/assertions/json.go b/internal/assertions/json.go index 4d6377d29..07b0db7b4 100644 --- a/internal/assertions/json.go +++ b/internal/assertions/json.go @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -//nolint:dupl // YAML is actually very similar to JSON but we can't easily factorize this. package assertions import ( "bytes" "encoding/json" "fmt" + "reflect" ) // JSONEqBytes asserts that two JSON slices of bytes are semantically equivalent. @@ -52,6 +52,8 @@ func JSONEqBytes(t T, expected, actual []byte, msgAndArgs ...any) bool { // // Expected and actual must be valid JSON. // +// For dynamic redaction of the input text via a callback, use [JSONEqT]. +// // # Usage // // assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) @@ -66,7 +68,7 @@ func JSONEq(t T, expected, actual string, msgAndArgs ...any) bool { h.Helper() } - return JSONEqBytes(t, []byte(expected), []byte(actual), msgAndArgs) + return JSONEqBytes(t, []byte(expected), []byte(actual), msgAndArgs...) } // JSONEqT asserts that two JSON documents are semantically equivalent. @@ -75,6 +77,8 @@ func JSONEq(t T, expected, actual string, msgAndArgs ...any) bool { // // Expected and actual must be valid JSON. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Usage // // assertions.JSONEqT(t, `{"hello": "world", "foo": "bar"}`, []byte(`{"foo": "bar", "hello": "world"}`)) @@ -83,13 +87,13 @@ func JSONEq(t T, expected, actual string, msgAndArgs ...any) bool { // // success: `{"hello": "world", "foo": "bar"}`, []byte(`{"foo": "bar", "hello": "world"}`) // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` -func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { +func JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { // Domain: json if h, ok := t.(H); ok { h.Helper() } - return JSONEqBytes(t, []byte(expected), []byte(actual), msgAndArgs) + return JSONEqBytes(t, asBytes(expected), asBytes(actual), msgAndArgs...) } // JSONUnmarshalAsT wraps [Equal] after [json.Unmarshal]. @@ -101,6 +105,8 @@ func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed jazon value may be wrapped as a function to redact the input JSON dynamically. +// // # Usage // // expected := struct { @@ -115,27 +121,29 @@ func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // // success: dummyStruct{A: "a"} , []byte(`{"A": "a"}`) // failure: 1, `[{"foo": "bar"}, {"hello": "world"}]` -func JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { +func JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { // Domain: json if h, ok := t.(H); ok { h.Helper() } var actual Object - if err := json.Unmarshal([]byte(jazon), &actual); err != nil { + if err := json.Unmarshal(asBytes(jazon), &actual); err != nil { return Fail(t, fmt.Sprintf("JSON unmarshal failed: %v", err), msgAndArgs...) } return Equal(t, expected, actual, msgAndArgs...) } -// JSONMarshalAsT wraps [JSONEq] after [json.Marshal]. +// JSONMarshalAsT wraps [JSONEqT] after [json.Marshal]. // // The input JSON may be a string or []byte. // // It fails if the marshaling returns an error or if the expected JSON bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -150,7 +158,7 @@ func JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, m // // success: []byte(`{"A": "a"}`), dummyStruct{A: "a"} // failure: `[{"foo": "bar"}, {"hello": "world"}]`, 1 -func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool { +func JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool { // Domain: json if h, ok := t.(H); ok { h.Helper() @@ -161,5 +169,29 @@ func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any return Fail(t, fmt.Sprintf("JSON marshal failed: %v", err), msgAndArgs...) } - return JSONEqBytes(t, []byte(expected), actual, msgAndArgs...) + return JSONEqBytes(t, asBytes(expected), actual, msgAndArgs...) +} + +func asBytes[EDoc RText](e EDoc) []byte { + ie := any(e) + + switch typed := ie.(type) { + case func() string: + if typed == nil { + panic("passed Redactor cannot be nil") + } + return []byte(typed()) + case func() []byte: + if typed == nil { + panic("passed Redactor cannot be nil") + } + return typed() + case string: + return []byte(typed) + case []byte: + return typed + default: + // this edge case (redefined type) requires the input to be converted: the type constraint warrants it to work + return convertReflectValue[[]byte](e, reflect.ValueOf(e)) + } } diff --git a/internal/assertions/json_test.go b/internal/assertions/json_test.go index 824338e05..ea0e21371 100644 --- a/internal/assertions/json_test.go +++ b/internal/assertions/json_test.go @@ -6,6 +6,8 @@ package assertions import ( "iter" "slices" + "strings" + "sync/atomic" "testing" ) @@ -258,21 +260,228 @@ func testJSONUnmarshalAsT[Doc Text, Object any](value Object, jazon Doc, success // ======================================= func jsonFailCases() iter.Seq[failCase] { + const ( + part1 = `{"a":1}` + part2 = `{"a":2}` + ) return slices.Values([]failCase{ { name: "JSONEq/not-equal", - assertion: func(t T) bool { return JSONEq(t, `{"a":1}`, `{"a":2}`) }, + assertion: func(t T) bool { return JSONEq(t, part1, part2) }, wantContains: []string{"Not equal"}, }, { name: "JSONEq/invalid-expected", - assertion: func(t T) bool { return JSONEq(t, "not json", `{"a":1}`) }, + assertion: func(t T) bool { return JSONEq(t, "not json", part1) }, wantContains: []string{"is not valid json"}, }, { name: "JSONEq/invalid-actual", - assertion: func(t T) bool { return JSONEq(t, `{"a":1}`, "not json") }, + assertion: func(t T) bool { return JSONEq(t, part1, "not json") }, wantContains: []string{"needs to be valid json"}, }, + { + name: "JSONEqT/redactor-mismatch", + assertion: func(t T) bool { + return JSONEqT(t, func() string { return part1 }, part2) + }, + wantContains: []string{"Not equal"}, + }, + }) +} + +// ======================================= +// Test JSONEqT / JSONUnmarshalAsT / JSONMarshalAsT with Redactor inputs +// +// These tests exercise the Redactor (`func() string` / `func() []byte`) +// arms of the [RText] type set, the named-string/named-bytes default +// branch in asBytes, and the nil-redactor panic guard. +// ======================================= + +func TestJSONRedact(t *testing.T) { + t.Parallel() + + t.Run("JSONEqT/redactor-string-on-expected", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + red := func() string { return `{"a":1}` } + if !JSONEqT(mock, red, `{"a":1}`) { + t.Errorf("expected redactor output to match literal; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONEqT/redactor-bytes-on-actual", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + red := func() []byte { return []byte(`{"a":1}`) } + if !JSONEqT(mock, []byte(`{"a":1}`), red) { + t.Errorf("expected literal to match redactor output; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONEqT/redactor-on-both-sides", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + left := func() string { return `{"a":1,"b":2}` } + right := func() []byte { return []byte(`{"b":2,"a":1}`) } + if !JSONEqT(mock, left, right) { + t.Errorf("expected both redactor outputs to be JSON-equivalent; mock: %s", mock.errorString()) + } + }) + + // Real-world scenario: a redactor normalises a non-deterministic field + // (timestamp) before comparison. + t.Run("JSONEqT/redactor-normalises-timestamp", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + raw := `{"id":42,"timestamp":"2026-04-26T15:30:45Z"}` + red := func() string { + return strings.Replace(raw, `"2026-04-26T15:30:45Z"`, `"REDACTED"`, 1) + } + expected := `{"id":42,"timestamp":"REDACTED"}` + if !JSONEqT(mock, expected, red) { + t.Errorf("expected redactor to normalise timestamp; mock: %s", mock.errorString()) + } + }) + + // Side-effect proof: the redactor IS invoked (asBytes runs before the + // comparison) and is invoked exactly once per call, regardless of which + // position it's in. + t.Run("JSONEqT/redactor-invoked-once", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + var calls atomic.Int32 + red := func() string { + calls.Add(1) + return `{"a":1}` + } + if !JSONEqT(mock, red, `{"a":1}`) { + t.Errorf("expected match; mock: %s", mock.errorString()) + } + if got := calls.Load(); got != 1 { + t.Errorf("expected redactor to be called exactly once, got %d", got) + } + }) + + t.Run("JSONUnmarshalAsT/with-redactor-string", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + type doc struct { + A int `json:"a"` + } + red := func() string { return `{"a":42}` } + if !JSONUnmarshalAsT(mock, doc{A: 42}, red) { + t.Errorf("expected unmarshal of redactor output to match expected struct; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONUnmarshalAsT/with-redactor-bytes", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + type doc struct { + A int `json:"a"` + } + red := func() []byte { return []byte(`{"a":42}`) } + if !JSONUnmarshalAsT(mock, doc{A: 42}, red) { + t.Errorf("expected unmarshal of redactor output to match expected struct; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONMarshalAsT/with-redactor-string", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + type doc struct { + A int `json:"a"` + } + red := func() string { return `{"a":42}` } + if !JSONMarshalAsT(mock, red, doc{A: 42}) { + t.Errorf("expected marshal of struct to match redactor output; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONMarshalAsT/with-redactor-bytes", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + type doc struct { + A int `json:"a"` + } + red := func() []byte { return []byte(`{"a":42}`) } + if !JSONMarshalAsT(mock, red, doc{A: 42}) { + t.Errorf("expected marshal of struct to match redactor output; mock: %s", mock.errorString()) + } + }) + + // Named string/[]byte types reach the default reflect branch in asBytes. + t.Run("JSONEqT/named-string-type", func(t *testing.T) { + t.Parallel() + type myJSON string + mock := new(mockT) + var v myJSON = `{"a":1}` + if !JSONEqT(mock, v, `{"a":1}`) { + t.Errorf("expected named string type to convert to []byte via reflect; mock: %s", mock.errorString()) + } + }) + + t.Run("JSONEqT/named-bytes-type", func(t *testing.T) { + t.Parallel() + type myRaw []byte + mock := new(mockT) + v := myRaw(`{"a":1}`) + if !JSONEqT(mock, v, []byte(`{"a":1}`)) { + t.Errorf("expected named []byte type to convert via reflect; mock: %s", mock.errorString()) + } + }) + + // Nil redactors are a programming error and panic with a clear message + // rather than the cryptic nil-pointer dereference users would otherwise see. + t.Run("JSONEqT/nil-func-string-panics", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "JSONEqT", func() { + var nilFn func() string + _ = JSONEqT(new(mockT), nilFn, `{"a":1}`) + }) }) + + t.Run("JSONEqT/nil-func-bytes-panics", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "JSONEqT", func() { + var nilFn func() []byte + _ = JSONEqT(new(mockT), nilFn, []byte(`{"a":1}`)) + }) + }) + + t.Run("JSONUnmarshalAsT/nil-func-string-panics", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "JSONUnmarshalAsT", func() { + var nilFn func() string + _ = JSONUnmarshalAsT(new(mockT), struct{}{}, nilFn) + }) + }) + + t.Run("JSONMarshalAsT/nil-func-bytes-panics", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "JSONMarshalAsT", func() { + var nilFn func() []byte + _ = JSONMarshalAsT(new(mockT), nilFn, struct{}{}) + }) + }) +} + +// mustPanicWithRedactor asserts that fn panics with the standard +// nil-Redactor message. Used by both JSON and YAML redactor tests. +func mustPanicWithRedactor(t *testing.T, fn string, body func()) { + t.Helper() + const wantMsg = "passed Redactor cannot be nil" + defer func() { + rec := recover() + if rec == nil { + t.Errorf("expected %s to panic with nil Redactor, got no panic", fn) + return + } + s, _ := rec.(string) + if !strings.Contains(s, wantMsg) { + t.Errorf("expected %s panic message to contain %q, got: %v", fn, wantMsg, rec) + } + }() + body() } diff --git a/internal/assertions/yaml.go b/internal/assertions/yaml.go index 2b5c90cbd..5fce6d53d 100644 --- a/internal/assertions/yaml.go +++ b/internal/assertions/yaml.go @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -//nolint:dupl // YAML is actually very similar to JSON but we can't easily factorize this. package assertions import ( @@ -25,6 +24,8 @@ import ( // "github.com/go-openapi/testify/enable/yaml/v2" // ) // +// For dynamic redaction of the input text via a callback, use [YAMLEqT]. +// // # Usage // // expected := `--- @@ -80,7 +81,7 @@ func YAMLEq(t T, expected, actual string, msgAndArgs ...any) bool { h.Helper() } - return YAMLEqBytes(t, []byte(expected), []byte(actual), msgAndArgs) + return YAMLEqBytes(t, []byte(expected), []byte(actual), msgAndArgs...) } // YAMLEqT asserts that two YAML documents are equivalent. @@ -89,17 +90,19 @@ func YAMLEq(t T, expected, actual string, msgAndArgs ...any) bool { // // See [YAMLEqBytes]. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Examples // // panic: "key: value", "key: value" // should panic without the yaml feature enabled. -func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { +func YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) bool { // Domain: yaml if h, ok := t.(H); ok { h.Helper() } - return YAMLEqBytes(t, []byte(expected), []byte(actual), msgAndArgs) + return YAMLEqBytes(t, asBytes(expected), asBytes(actual), msgAndArgs...) } // YAMLUnmarshalAsT wraps [Equal] after [yaml.Unmarshal]. @@ -111,6 +114,8 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed yamlDoc value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // expected := struct { @@ -125,14 +130,14 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // // panic: "key: value", "key: value" // should panic without the yaml feature enabled. -func YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) bool { +func YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) bool { // Domain: yaml if h, ok := t.(H); ok { h.Helper() } var actual Object - if err := yaml.Unmarshal([]byte(jazon), &actual); err != nil { + if err := yaml.Unmarshal(asBytes(yamlDoc), &actual); err != nil { return Fail(t, fmt.Sprintf("YAML unmarshal failed: %v", err), msgAndArgs...) } @@ -146,6 +151,8 @@ func YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, m // It fails if the marshaling returns an error or if the expected YAML bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -160,7 +167,7 @@ func YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, m // // panic: "key: value", "key: value" // should panic without the yaml feature enabled. -func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) bool { +func YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) bool { // Domain: yaml if h, ok := t.(H); ok { h.Helper() @@ -171,5 +178,5 @@ func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any return Fail(t, fmt.Sprintf("YAML marshal failed: %v", err), msgAndArgs...) } - return YAMLEqBytes(t, []byte(expected), actual, msgAndArgs...) + return YAMLEqBytes(t, asBytes(expected), actual, msgAndArgs...) } diff --git a/internal/assertions/yaml_test.go b/internal/assertions/yaml_test.go index bd8051299..35f8d5589 100644 --- a/internal/assertions/yaml_test.go +++ b/internal/assertions/yaml_test.go @@ -3,7 +3,10 @@ package assertions -import "testing" +import ( + "sync/atomic" + "testing" +) func TestYAML(t *testing.T) { t.Parallel() @@ -151,3 +154,114 @@ func croakWantPanic(t *testing.T, fn string) { t.Helper() t.Errorf("expected %q to panic with default settings", fn) } + +// ======================================= +// Test YAML T-variants accept Redactor inputs +// +// In this package the YAML feature is disabled, so a successful redactor +// call still ends in a panic from YAMLEqBytes. These tests verify that: +// +// 1. The Redactor (`func() string` / `func() []byte`) arms of the [RText] +// constraint compile and route through asBytes correctly. +// 2. The redactor body IS invoked (proving asBytes runs before the +// YAML-disabled panic). +// 3. Nil redactors panic with the standard "passed Redactor cannot be nil" +// message — same guard as the JSON path. +// ======================================= + +func TestYAMLRedact(t *testing.T) { + t.Parallel() + + t.Run("YAMLEqT/redactor-invoked-then-yaml-panics", func(t *testing.T) { + t.Parallel() + var calls atomic.Int32 + red := func() string { + calls.Add(1) + return "key: value" + } + didPanic := func() (panicked bool) { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = YAMLEqT(new(mockT), red, "key: value") + return false + }() + if !didPanic { + croakWantPanic(t, "YAMLEqT (with Redactor)") + } + // asBytes must have run at least once (one side is the redactor). + if got := calls.Load(); got != 1 { + t.Errorf("expected redactor to be invoked exactly once before YAML-disabled panic, got %d", got) + } + }) + + t.Run("YAMLUnmarshalAsT/redactor-bytes-invoked-then-yaml-panics", func(t *testing.T) { + t.Parallel() + var calls atomic.Int32 + red := func() []byte { + calls.Add(1) + return []byte("key: value") + } + didPanic := func() (panicked bool) { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = YAMLUnmarshalAsT(new(mockT), struct{}{}, red) + return false + }() + if !didPanic { + croakWantPanic(t, "YAMLUnmarshalAsT (with Redactor)") + } + if got := calls.Load(); got != 1 { + t.Errorf("expected redactor to be invoked exactly once before YAML-disabled panic, got %d", got) + } + }) + + // YAMLMarshalAsT runs `yaml.Marshal(object)` BEFORE evaluating + // `asBytes(expected)`, so when the YAML feature is disabled the + // redactor body is never reached. We can only verify that the call + // still panics; happy-path Redactor coverage for YAMLMarshalAsT + // belongs in the enable/yaml integration tests. + t.Run("YAMLMarshalAsT/with-redactor-still-panics", func(t *testing.T) { + t.Parallel() + red := func() string { return "key: value" } + didPanic := func() (panicked bool) { + defer func() { + if recover() != nil { + panicked = true + } + }() + _ = YAMLMarshalAsT(new(mockT), red, struct{}{}) + return false + }() + if !didPanic { + croakWantPanic(t, "YAMLMarshalAsT (with Redactor)") + } + }) + + // Nil redactors panic with the standard message before reaching YAMLEqBytes, + // so this asserts the asBytes guard fires regardless of the YAML feature. + t.Run("YAMLEqT/nil-func-string-panics-with-redactor-message", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "YAMLEqT", func() { + var nilFn func() string + _ = YAMLEqT(new(mockT), nilFn, "key: value") + }) + }) + + t.Run("YAMLUnmarshalAsT/nil-func-bytes-panics-with-redactor-message", func(t *testing.T) { + t.Parallel() + mustPanicWithRedactor(t, "YAMLUnmarshalAsT", func() { + var nilFn func() []byte + _ = YAMLUnmarshalAsT(new(mockT), struct{}{}, nilFn) + }) + }) + + // YAMLMarshalAsT/nil-redactor case omitted: yaml.Marshal panics first + // when YAML is disabled, so the nil-Redactor guard inside asBytes is + // unreachable here. Covered in the enable/yaml integration tests. +} diff --git a/require/require_assertions.go b/require/require_assertions.go index f7203b6ed..870964e8b 100644 --- a/require/require_assertions.go +++ b/require/require_assertions.go @@ -1868,6 +1868,8 @@ func IsType(t T, expectedType any, object any, msgAndArgs ...any) { // // Expected and actual must be valid JSON. // +// For dynamic redaction of the input text via a callback, use [JSONEqT]. +// // # Usage // // assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) @@ -1920,6 +1922,8 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) { // // Expected and actual must be valid JSON. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Usage // // assertions.JSONEqT(t, `{"hello": "world", "foo": "bar"}`, []byte(`{"foo": "bar", "hello": "world"}`)) @@ -1930,7 +1934,7 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) { // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) { +func JSONEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -1941,13 +1945,15 @@ func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any t.FailNow() } -// JSONMarshalAsT wraps [JSONEq] after [json.Marshal]. +// JSONMarshalAsT wraps [JSONEqT] after [json.Marshal]. // // The input JSON may be a string or []byte. // // It fails if the marshaling returns an error or if the expected JSON bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -1964,7 +1970,7 @@ func JSONEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // failure: `[{"foo": "bar"}, {"hello": "world"}]`, 1 // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) { +func JSONMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -1984,6 +1990,8 @@ func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed jazon value may be wrapped as a function to redact the input JSON dynamically. +// // # Usage // // expected := struct { @@ -2000,7 +2008,7 @@ func JSONMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // failure: 1, `[{"foo": "bar"}, {"hello": "world"}]` // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) { +func JSONUnmarshalAsT[Object any, ADoc RText](t T, expected Object, jazon ADoc, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -3782,6 +3790,8 @@ func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) { // "github.com/go-openapi/testify/enable/yaml/v2" // ) // +// For dynamic redaction of the input text via a callback, use [YAMLEqT]. +// // # Usage // // expected := `--- @@ -3819,13 +3829,15 @@ func YAMLEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) { // // See [YAMLEqBytes]. // +// NOTE: passed values (expected, actual) may be wrapped as functions to redact the input text dynamically. +// // # Examples // // panic: "key: value", "key: value" // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any) { +func YAMLEqT[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -3843,6 +3855,8 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // It fails if the marshaling returns an error or if the expected YAML bytes differ semantically // from the expected ones. // +// NOTE: passed expected value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // actual := struct { @@ -3859,7 +3873,7 @@ func YAMLEqT[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msgAndArgs ...any // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any) { +func YAMLMarshalAsT[EDoc RText](t T, expected EDoc, object any, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -3879,6 +3893,8 @@ func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // Be careful not to wrap the expected object into an "any" interface if this is not what you expected: // the unmarshaling would take this type to unmarshal as a map[string]any. // +// NOTE: passed yamlDoc value may be wrapped as a function to redact the input text dynamically. +// // # Usage // // expected := struct { @@ -3895,11 +3911,11 @@ func YAMLMarshalAsT[EDoc Text](t T, expected EDoc, object any, msgAndArgs ...any // should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLUnmarshalAsT[Object any, ADoc Text](t T, expected Object, jazon ADoc, msgAndArgs ...any) { +func YAMLUnmarshalAsT[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msgAndArgs ...any) { if h, ok := t.(H); ok { h.Helper() } - if assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, jazon, msgAndArgs...) { + if assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, yamlDoc, msgAndArgs...) { return } diff --git a/require/require_format.go b/require/require_format.go index d9605be77..65dd0e193 100644 --- a/require/require_format.go +++ b/require/require_format.go @@ -910,7 +910,7 @@ func JSONEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) // JSONEqTf is the same as [JSONEqT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) { +func JSONEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -924,7 +924,7 @@ func JSONEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args // JSONMarshalAsTf is the same as [JSONMarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) { +func JSONMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -938,7 +938,7 @@ func JSONMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args // JSONUnmarshalAsTf is the same as [JSONUnmarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func JSONUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) { +func JSONUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, jazon ADoc, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -1876,7 +1876,7 @@ func YAMLEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) // YAMLEqTf is the same as [YAMLEqT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args ...any) { +func YAMLEqTf[EDoc, ADoc RText](t T, expected EDoc, actual ADoc, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -1890,7 +1890,7 @@ func YAMLEqTf[EDoc, ADoc Text](t T, expected EDoc, actual ADoc, msg string, args // YAMLMarshalAsTf is the same as [YAMLMarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args ...any) { +func YAMLMarshalAsTf[EDoc RText](t T, expected EDoc, object any, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } @@ -1904,11 +1904,11 @@ func YAMLMarshalAsTf[EDoc Text](t T, expected EDoc, object any, msg string, args // YAMLUnmarshalAsTf is the same as [YAMLUnmarshalAsT], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. -func YAMLUnmarshalAsTf[Object any, ADoc Text](t T, expected Object, jazon ADoc, msg string, args ...any) { +func YAMLUnmarshalAsTf[Object any, ADoc RText](t T, expected Object, yamlDoc ADoc, msg string, args ...any) { if h, ok := t.(H); ok { h.Helper() } - if assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, jazon, forwardArgs(msg, args)) { + if assertions.YAMLUnmarshalAsT[Object, ADoc](t, expected, yamlDoc, forwardArgs(msg, args)) { return } diff --git a/require/require_types.go b/require/require_types.go index 2d429c38f..d8ca03865 100644 --- a/require/require_types.go +++ b/require/require_types.go @@ -86,6 +86,15 @@ type ( // for table driven tests. PanicAssertionFunc func(t T, f func(), msgAndArgs ...any) + // RText extends [Text] by supporting dynamic construction of the + // expected or actual value, e.g. "redact" functions. + RText = assertions.RText + + // Redactor allows dynamic construction of expected or actual values, e.g. "redacting" values dynamically. + // + // This is used by json and yaml assertions. + Redactor = assertions.Redactor + // RegExp is either a text containing a regular expression to compile (string or []byte), or directly the compiled regexp. // // This is used by [RegexpT] and [NotRegexpT].