diff --git a/pkg/approvaltest/approvals.go b/pkg/approvaltest/approvals.go index c4268d2..8f6fda2 100644 --- a/pkg/approvaltest/approvals.go +++ b/pkg/approvaltest/approvals.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -53,16 +54,16 @@ const ( func ApproveEvents(t testing.TB, name string, hits []espoll.SearchHit, dynamic ...string) { t.Helper() - // Sort events for repeatable diffs. - sort.Slice(hits, func(i, j int) bool { - return compareDocumentFields(hits[i].RawFields, hits[j].RawFields) < 0 - }) - sources := make([][]byte, len(hits)) for i, hit := range hits { sources[i] = hit.RawSource } - approveEventDocs(t, filepath.Join("approvals", name), sources, dynamic...) + rewriteDynamic(t, sources, false, dynamic...) + // Rewrite dynamic fields and sort them for repeatable diffs. + sort.Slice(sources, func(i, j int) bool { + return compareDocumentFields(sources[i], sources[j]) < 0 + }) + approveEventDocs(t, filepath.Join("approvals", name), sources) } // ApproveFields compares the fields of the search hits with the @@ -79,26 +80,23 @@ func ApproveEvents(t testing.TB, name string, hits []espoll.SearchHit, dynamic . func ApproveFields(t testing.TB, name string, hits []espoll.SearchHit, dynamic ...string) { t.Helper() - // Sort events for repeatable diffs. - sort.Slice(hits, func(i, j int) bool { - return compareDocumentFields(hits[i].RawFields, hits[j].RawFields) < 0 - }) - fields := make([][]byte, len(hits)) for i, hit := range hits { fields[i] = hit.RawFields } - approveFields(t, filepath.Join("approvals", name), fields, dynamic...) + // Rewrite dynamic fields and sort them for repeatable diffs. + rewriteDynamic(t, fields, true, dynamic...) + sort.Slice(fields, func(i, j int) bool { + return compareDocumentFields(fields[i], fields[j]) < 0 + }) + approveFields(t, filepath.Join("approvals", name), fields) } -// approveEventDocs compares the given event documents with -// the contents of the file in ".approved.json". -// -// Any specified dynamic fields (e.g. @timestamp, observer.id) -// will be replaced with a static string for comparison. -// -// If the events differ, then the test will fail. -func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ...string) { +// rewriteDynamic rewrites all dynamic fields to have a known value, so dynamic +// fields don't affect diffs. The flattenedKeys parameter defines how the +// field should be queried in the source, if flattenedKeys is passed as true +// then the source will be queried for the dynamic fields as flattened keys. +func rewriteDynamic(t testing.TB, srcs [][]byte, flattenedKeys bool, dynamic ...string) { t.Helper() // Fields generated by the server (e.g. observer.*) @@ -115,22 +113,44 @@ func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ... "observer.version", }, dynamic...) - // Rewrite all dynamic fields to have a known value, - // so dynamic fields don't affect diffs. - events := make([]interface{}, len(eventDocs)) - for i, doc := range eventDocs { + for i := range srcs { for _, field := range dynamic { - existing := gjson.GetBytes(doc, field) + if flattenedKeys { + field = strings.ReplaceAll(field, ".", "\\.") + } + existing := gjson.GetBytes(srcs[i], field) if !existing.Exists() { continue } + var v interface{} + if existing.IsArray() { + v = []any{"dynamic"} + } else { + v = "dynamic" + } + var err error - doc, err = sjson.SetBytes(doc, field, "dynamic") + srcs[i], err = sjson.SetBytes(srcs[i], field, v) if err != nil { t.Fatal(err) } } + } +} + +// approveEventDocs compares the given event documents with +// the contents of the file in ".approved.json". +// +// Any specified dynamic fields (e.g. @timestamp, observer.id) +// will be replaced with a static string for comparison. +// +// If the events differ, then the test will fail. +func approveEventDocs(t testing.TB, name string, eventDocs [][]byte) { + t.Helper() + + events := make([]interface{}, len(eventDocs)) + for i, doc := range eventDocs { var event map[string]interface{} if err := json.Unmarshal(doc, &event); err != nil { @@ -143,23 +163,9 @@ func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ... approve(t, name, received) } -func approveFields(t testing.TB, name string, docs [][]byte, dynamic ...string) { +func approveFields(t testing.TB, name string, docs [][]byte) { t.Helper() - // Fields generated by the server (e.g. observer.*) - // agent which may change between tests. - // - // Ignore their values in comparisons, but compare - // existence: either the field exists in both, or neither. - dynamic = append([]string{ - "ecs.version", - "event.ingested", - "observer.ephemeral_id", - "observer.hostname", - "observer.id", - "observer.version", - }, dynamic...) - // Rewrite all dynamic fields to have a known value, // so dynamic fields don't affect diffs. decodedDocs := make([]any, len(docs)) @@ -168,11 +174,6 @@ func approveFields(t testing.TB, name string, docs [][]byte, dynamic ...string) if err := json.Unmarshal(doc, &fields); err != nil { t.Fatal(err) } - for _, field := range dynamic { - if _, ok := fields[field]; ok { - fields[field] = []any{"dynamic"} - } - } decodedDocs[i] = fields }