diff --git a/matchers/have_patterns_test.go b/matchers/have_patterns_test.go new file mode 100644 index 00000000..5f19fb39 --- /dev/null +++ b/matchers/have_patterns_test.go @@ -0,0 +1,68 @@ +package matchers + +import ( + "bytes" + "strings" + "testing" +) + +// TestHavePatternsMatchString verifies that HavePatterns correctly matches +// string content (the type it receives after the validate-layer materialization fix). +func TestHavePatternsMatchString(t *testing.T) { + content := "Banner /etc/issue.net\nLogLevel INFO\n" + + m := HavePatterns([]interface{}{"Banner /etc/issue.net"}) + success, err := m.Match(content) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !success { + t.Fatal("expected Match to return true") + } +} + +// TestHavePatternsFailureResultString verifies that FailureResult returns the +// actual content as a string — not a Go type repr — when given string input. +// After the validate-layer fix, FailureResult always receives a materialized string. +func TestHavePatternsFailureResultString(t *testing.T) { + content := "Banner /etc/issue.net\nLogLevel INFO\n" + pattern := "nonexistent-pattern" + + m := HavePatterns([]interface{}{pattern}) + success, err := m.Match(content) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if success { + t.Fatal("expected Match to return false for missing pattern") + } + + result := m.FailureResult(content) + + actual, ok := result.Actual.(string) + if !ok { + t.Fatalf("FailureResult.Actual must be a string, got %T: %v", result.Actual, result.Actual) + } + if strings.Contains(actual, "object:") { + t.Errorf("FailureResult.Actual must not contain Go type repr, got: %q", actual) + } + if !strings.Contains(actual, "Banner") { + t.Errorf("FailureResult.Actual must contain the file content, got: %q", actual) + } +} + +// TestHavePatternsMatchBytesReader verifies that HavePatterns still accepts +// io.Reader directly (backwards-compat; the validate layer now sends strings). +func TestHavePatternsMatchBytesReader(t *testing.T) { + content := "pam_faillock.so preauth\n" + + m := HavePatterns([]interface{}{"pam_faillock.so"}) + reader := bytes.NewReader([]byte(content)) + success, err := m.Match(reader) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !success { + t.Fatal("expected Match to return true") + } +} diff --git a/resource/validate.go b/resource/validate.go index 720c7b7d..1914f940 100644 --- a/resource/validate.go +++ b/resource/validate.go @@ -159,7 +159,15 @@ func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, a case func() (any, error): foundValue, err = f() case func() (io.Reader, error): - foundValue, err = f() + var r io.Reader + r, err = f() + if err == nil { + var i interface{} + i, err = matchers.ReaderToString{}.Transform(r) + if err == nil { + foundValue = i.(string) + } + } gomegaMatcher = matchers.HavePatterns(expectedValue) default: err = fmt.Errorf("Unknown method signature: %t", f) diff --git a/resource/validate_test.go b/resource/validate_test.go index 55a45020..89f57ac8 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -128,6 +128,33 @@ func TestValidateContainsBadRegexErr(t *testing.T) { } } +// TestValidateContainsFailureActual asserts that when a file-contents check fails, +// the MatcherResult.Actual field contains the readable file content — not the +// Go internal type representation "object: *bytes.Reader" / "object: *os.File". +func TestValidateContainsFailureActual(t *testing.T) { + fileContent := "Banner /etc/issue.net\nLogLevel INFO\n" + missingPattern := "nonexistent-pattern-xyz" + + inFunc := func() (io.Reader, error) { + return strings.NewReader(fileContent), nil + } + got := ValidateValue(&FakeResource{""}, "contents", []interface{}{missingPattern}, inFunc, false) + + if got.Result != FAIL { + t.Fatalf("expected FAIL, got %v", got.Result) + } + actual, ok := got.MatcherResult.Actual.(string) + if !ok { + t.Fatalf("MatcherResult.Actual must be a string, got %T: %v", got.MatcherResult.Actual, got.MatcherResult.Actual) + } + if strings.Contains(actual, "object:") { + t.Errorf("MatcherResult.Actual must not contain Go type repr, got: %q", actual) + } + if !strings.Contains(actual, "Banner") { + t.Errorf("MatcherResult.Actual must contain file content, got: %q", actual) + } +} + func TestValidateContainsSkip(t *testing.T) { for _, c := range containsTests { inFunc := func() (io.Reader, error) { diff --git a/testdata/out_matching_basic_failing.1.documentation b/testdata/out_matching_basic_failing.1.documentation index 735d3a63..8c50f2b7 100644 --- a/testdata/out_matching_basic_failing.1.documentation +++ b/testdata/out_matching_basic_failing.1.documentation @@ -31,7 +31,7 @@ to satisfy at least one of these matchers [{"have-len":2}] Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were @@ -253,7 +253,7 @@ to satisfy at least one of these matchers Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were diff --git a/testdata/out_matching_basic_failing.1.rspecish b/testdata/out_matching_basic_failing.1.rspecish index 3d6ca262..8ee94905 100644 --- a/testdata/out_matching_basic_failing.1.rspecish +++ b/testdata/out_matching_basic_failing.1.rspecish @@ -40,7 +40,7 @@ to satisfy at least one of these matchers Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were diff --git a/testdata/out_matching_basic_failing.1.tap b/testdata/out_matching_basic_failing.1.tap index a60f1f65..348c081f 100644 --- a/testdata/out_matching_basic_failing.1.tap +++ b/testdata/out_matching_basic_failing.1.tap @@ -4,7 +4,7 @@ not ok 2 - Matching: basic_array_consists_of: matches: Expected ["foo","bar","mo not ok 3 - Matching: basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] not ok 4 - Matching: basic_int: matches: Expected 42 to be numerically eq 43 not ok 5 - Matching: basic_len: matches: Expected "123" to satisfy at least one of these matchers [{"have-len":2}] -not ok 6 - Matching: basic_reader: matches: Expected "object: *strings.Reader" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"] +not ok 6 - Matching: basic_reader: matches: Expected "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"] not ok 7 - Matching: basic_semver: matches: Expected "1.2.3" to satisfy at least one of these matchers [{"semver-constraint":">=9.9.0"}] not ok 8 - Matching: basic_string: matches: Expected "this is a test" to equal "this is a failing test" not ok 9 - Matching: basic_string_contain_substring: matches: Expected "foo" to contain substring "x"