package filters import ( "encoding/json" roots "git.wisehodl.dev/jay/go-roots/filters" "github.com/stretchr/testify/assert" "testing" ) // Helpers func intPtr(i int) *int { return &i } func expectEqualHeartwoodFilters(t *testing.T, got, want HeartwoodFilter) { t.Helper() assert.Equal(t, want.Root.IDs, got.Root.IDs) assert.Equal(t, want.Root.Authors, got.Root.Authors) assert.Equal(t, want.Root.Kinds, got.Root.Kinds) assert.Equal(t, want.Root.Since, got.Root.Since) assert.Equal(t, want.Root.Until, got.Root.Until) assert.Equal(t, want.Root.Limit, got.Root.Limit) assert.Equal(t, want.Root.Tags, got.Root.Tags) assert.Equal(t, len(want.Graph), len(got.Graph)) for i := range want.Graph { expectEqualGraphFilters(t, got.Graph[i], want.Graph[i]) } } func expectEqualGraphFilters(t *testing.T, got, want GraphFilter) { t.Helper() assert.Equal(t, want.IDs, got.IDs) assert.Equal(t, want.Authors, got.Authors) assert.Equal(t, want.Kinds, got.Kinds) assert.Equal(t, want.Since, got.Since) assert.Equal(t, want.Until, got.Until) assert.Equal(t, want.Limit, got.Limit) assert.Equal(t, want.Tags, got.Tags) assert.Equal(t, want.Distance, got.Distance) assert.Equal(t, len(want.Graph), len(got.Graph)) for i := range want.Graph { expectEqualGraphFilters(t, got.Graph[i], want.Graph[i]) } assert.Equal(t, want.Extensions, got.Extensions) } // Tests func TestMarshalJSON(t *testing.T) { cases := []struct { name string filter HeartwoodFilter expected string }{ { name: "empty filter", filter: HeartwoodFilter{}, expected: `{}`, }, { name: "root fields only", filter: HeartwoodFilter{ Root: roots.Filter{ IDs: []string{"abc"}, Kinds: []int{1}, Since: intPtr(1000), }, }, expected: `{"ids":["abc"],"kinds":[1],"since":1000}`, }, { name: "empty graph field", filter: HeartwoodFilter{ Graph: []GraphFilter{}, }, expected: `{"graph":[]}`, }, { name: "graph field only", filter: HeartwoodFilter{ Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, }, }, expected: `{"graph":[{"kinds":[1]}]}`, }, { name: "root and graph present", filter: HeartwoodFilter{ Root: roots.Filter{ IDs: []string{"abc"}, }, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, }, }, expected: `{"ids":["abc"],"graph":[{"kinds":[1]}]}`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result, err := MarshalJSON(tc.filter) assert.NoError(t, err) var expectedMap, actualMap map[string]interface{} assert.NoError(t, json.Unmarshal([]byte(tc.expected), &expectedMap)) assert.NoError(t, json.Unmarshal(result, &actualMap)) assert.Equal(t, expectedMap, actualMap) }) } } func TestUnmarshalJSON(t *testing.T) { cases := []struct { name string input string expected HeartwoodFilter }{ { name: "empty object", input: `{}`, expected: HeartwoodFilter{}, }, { name: "root fields only", input: `{"ids":["abc"],"kinds":[1],"since":1000}`, expected: HeartwoodFilter{ Root: roots.Filter{ IDs: []string{"abc"}, Kinds: []int{1}, Since: intPtr(1000), }, }, }, { name: "empty graph field", input: `{"graph":[]}`, expected: HeartwoodFilter{ Graph: []GraphFilter{}, }, }, { name: "graph field only", input: `{"graph":[{"kinds":[1]}]}`, expected: HeartwoodFilter{ Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, }, }, }, { name: "root and graph present", input: `{"ids":["abc"],"graph":[{"kinds":[1]}]}`, expected: HeartwoodFilter{ Root: roots.Filter{ IDs: []string{"abc"}, }, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, }, }, }, { name: "graph is removed from root extensions", input: `{"ids":["abc"],"graph":[{"kinds":[1]}],"search":"bitcoin"}`, expected: HeartwoodFilter{ Root: roots.Filter{ IDs: []string{"abc"}, Extensions: map[string]json.RawMessage{ "search": json.RawMessage(`"bitcoin"`), }, }, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var result HeartwoodFilter err := UnmarshalJSON([]byte(tc.input), &result) assert.NoError(t, err) expectEqualHeartwoodFilters(t, result, tc.expected) // Ensure graph extension was popped from root filter assert.Nil(t, result.Root.Extensions["graph"]) }) } } func TestMarshalGraphJSON(t *testing.T) { cases := []struct { name string filter GraphFilter expected string }{ { name: "empty filter", filter: GraphFilter{}, expected: `{}`, }, { name: "standard fields", filter: GraphFilter{ IDs: []string{"abc"}, Authors: []string{"def"}, Kinds: []json.RawMessage{json.RawMessage(`1`)}, Since: json.RawMessage(`1000`), Until: []byte("2000"), Limit: intPtr(10), }, expected: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10}`, }, { name: "tag field", filter: GraphFilter{ Tags: roots.TagFilters{"e": {"event1"}}, }, expected: `{"#e":["event1"]}`, }, { name: "distance present", filter: GraphFilter{ Distance: &Distance{Min: 1, Max: 10}, }, expected: `{"distance":{"min":1,"max":10}}`, }, { name: "distance absent", filter: GraphFilter{Distance: nil}, expected: `{}`, }, { name: "empty graph", filter: GraphFilter{ Kinds: []json.RawMessage{json.RawMessage(`1`)}, Graph: []GraphFilter{}, }, expected: `{"kinds":[1],"graph":[]}`, }, { name: "nested graph", filter: GraphFilter{ Kinds: []json.RawMessage{json.RawMessage(`1`)}, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, }, }, expected: `{"kinds":[1],"graph":[{"kinds":[7]}]}`, }, { name: "extensions", filter: GraphFilter{ Extensions: roots.FilterExtensions{ "search": json.RawMessage(`"abc"`), }, }, expected: `{"search":"abc"}`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result, err := MarshalGraphJSON(tc.filter) assert.NoError(t, err) var expectedMap, actualMap map[string]interface{} assert.NoError(t, json.Unmarshal([]byte(tc.expected), &expectedMap)) assert.NoError(t, json.Unmarshal(result, &actualMap)) assert.Equal(t, expectedMap, actualMap) }) } } func TestUnmarshalGraphJSON(t *testing.T) { cases := []struct { name string input string expected GraphFilter }{ { name: "empty object", input: `{}`, expected: GraphFilter{}, }, { name: "standard fields", input: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10}`, expected: GraphFilter{ IDs: []string{"abc"}, Authors: []string{"def"}, Kinds: []json.RawMessage{json.RawMessage(`1`)}, Since: json.RawMessage(`1000`), Until: json.RawMessage(`2000`), Limit: intPtr(10), }, }, { name: "tag field", input: `{"#e":["event1"]}`, expected: GraphFilter{ Tags: roots.TagFilters{"e": {"event1"}}, }, }, { name: "distance present", input: `{"distance":{"min":1,"max":10}}`, expected: GraphFilter{ Distance: &Distance{Min: 1, Max: 10}, }, }, { name: "distance absent", input: `{}`, expected: GraphFilter{Distance: nil}, }, { name: "empty graph", input: `{"kinds":[1],"graph":[]}`, expected: GraphFilter{ Kinds: []json.RawMessage{json.RawMessage(`1`)}, Graph: []GraphFilter{}, }, }, { name: "nested graph", input: `{"kinds":[1],"graph":[{"kinds":[7]}]}`, expected: GraphFilter{ Kinds: []json.RawMessage{json.RawMessage(`1`)}, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, }, }, }, { name: "fully populated", input: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10,"#e":["event1"],"distance":{"min":1,"max":5},"graph":[{"kinds":[7]}]}`, expected: GraphFilter{ IDs: []string{"abc"}, Authors: []string{"def"}, Kinds: []json.RawMessage{json.RawMessage(`1`)}, Since: json.RawMessage(`1000`), Until: json.RawMessage(`2000`), Limit: intPtr(10), Tags: roots.TagFilters{"e": {"event1"}}, Distance: &Distance{Min: 1, Max: 5}, Graph: []GraphFilter{ {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, }, }, }, { name: "unknown fields routed to extensions", input: `{"search":"abc"}`, expected: GraphFilter{ Extensions: roots.FilterExtensions{ "search": json.RawMessage(`"abc"`), }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var result GraphFilter err := UnmarshalGraphJSON([]byte(tc.input), &result) assert.NoError(t, err) expectEqualGraphFilters(t, result, tc.expected) }) } }