master
go 208 lines 5.57 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 topologyv1 "github.com/netdata/netdata/go/plugins/pkg/topology/v1"
13 )
14
15 func TestTopologyV1FixturesValidate(t *testing.T) {
16 schemaBytes := readTestFile(t, filepath.Join("..", "..", "..", "..", "plugins.d", "FUNCTION_TOPOLOGY_SCHEMA.json"))
17 fixtures, err := filepath.Glob(filepath.Join("..", "fixtures", "topology-v1", "*.json"))
18 if err != nil {
19 t.Fatalf("glob fixtures: %v", err)
20 }
21 if len(fixtures) != 4 {
22 t.Fatalf("expected 4 topology fixtures, got %d", len(fixtures))
23 }
24
25 for _, fixture := range fixtures {
26 t.Run(filepath.Base(fixture), func(t *testing.T) {
27 inputBytes := readTestFile(t, fixture)
28 if _, err := validateJSON(schemaBytes, inputBytes); err != nil {
29 t.Fatalf("validate fixture: %v", err)
30 }
31 })
32 }
33 }
34
35 func TestTopologyV1EnvelopeVersionValidates(t *testing.T) {
36 schemaBytes := readTestFile(t, filepath.Join("..", "..", "..", "..", "plugins.d", "FUNCTION_TOPOLOGY_SCHEMA.json"))
37 fixture := readTestFile(t, filepath.Join("..", "fixtures", "topology-v1", "snmp-l2.json"))
38
39 var payload map[string]any
40 if err := json.Unmarshal(fixture, &payload); err != nil {
41 t.Fatalf("decode fixture: %v", err)
42 }
43 payload["v"] = float64(3)
44
45 inputBytes, err := json.Marshal(payload)
46 if err != nil {
47 t.Fatalf("encode fixture: %v", err)
48 }
49 if _, err := validateJSON(schemaBytes, inputBytes); err != nil {
50 t.Fatalf("validate fixture with envelope version: %v", err)
51 }
52 }
53
54 func TestFunctionUISchemaValidationSkipsTopologySemanticsForTableResponses(t *testing.T) {
55 schemaBytes := readTestFile(t, filepath.Join("..", "..", "..", "..", "plugins.d", "FUNCTION_UI_SCHEMA.json"))
56 input := []byte(`{
57 "status": 200,
58 "type": "table",
59 "columns": {
60 "name": {"index": 0, "name": "Name", "type": "string", "visualization": "value"}
61 },
62 "data": [["row-a"]]
63 }`)
64
65 if _, err := validateJSON(schemaBytes, input); err != nil {
66 t.Fatalf("validate table response: %v", err)
67 }
68 }
69
70 func TestValidationSkipsTopologySemanticsForNonTopologyObjectData(t *testing.T) {
71 schemaBytes := []byte(`{"type": "object"}`)
72 input := []byte(`{
73 "status": 200,
74 "type": "custom",
75 "data": {
76 "schema_version": "netdata.topology.v1"
77 }
78 }`)
79
80 if _, err := validateJSON(schemaBytes, input); err != nil {
81 t.Fatalf("validate custom response: %v", err)
82 }
83 }
84
85 func TestCountRowsUsesActorsWhenTopologyHasNoLinks(t *testing.T) {
86 payload := decodeTestJSON(t, `{
87 "status": 200,
88 "type": "topology",
89 "data": {
90 "schema_version": "netdata.topology.v1",
91 "dictionaries": {"strings": ["node"]},
92 "actors": {
93 "rows": 2,
94 "columns": [{"id": "type", "type": "string_ref", "dictionary": "strings"}],
95 "values": [{"codec": "const", "value": 0}]
96 },
97 "links": {"rows": 0, "columns": [], "values": []}
98 }
99 }`)
100
101 rows, err := countRows(payload)
102 if err != nil {
103 t.Fatalf("count rows: %v", err)
104 }
105 if rows != 2 {
106 t.Fatalf("expected actor rows when there are no links, got %d", rows)
107 }
108 }
109
110 func TestCountRowsDoesNotTreatNonTopologyObjectDataAsGraph(t *testing.T) {
111 payload := decodeTestJSON(t, `{
112 "status": 200,
113 "type": "custom",
114 "data": {
115 "schema_version": "netdata.topology.v1",
116 "actors": {"rows": 4, "columns": [], "values": []},
117 "links": {"rows": 1, "columns": [], "values": []}
118 }
119 }`)
120
121 _, err := countRows(payload)
122 if err == nil {
123 t.Fatal("expected non-array data error")
124 }
125 if !strings.Contains(err.Error(), "data is not an array") {
126 t.Fatalf("expected non-array data error, got %v", err)
127 }
128 }
129
130 func TestTopologySemanticChecksRejectColumnLengthMismatch(t *testing.T) {
131 payload := decodeTestJSON(t, `{
132 "status": 200,
133 "type": "topology",
134 "data": {
135 "schema_version": "netdata.topology.v1",
136 "dictionaries": {"strings": ["node"]},
137 "actors": {
138 "rows": 2,
139 "columns": [{"id": "type", "type": "string_ref", "dictionary": "strings"}],
140 "values": [{"codec": "values", "values": [0]}]
141 },
142 "links": {"rows": 0, "columns": [], "values": []}
143 }
144 }`)
145
146 err := topologyv1.ValidateDecodedResponse(payload)
147 if err == nil {
148 t.Fatal("expected validation error")
149 }
150 if !strings.Contains(err.Error(), "decoded length mismatch") {
151 t.Fatalf("expected decoded length mismatch, got %v", err)
152 }
153 }
154
155 func TestTopologySemanticChecksRejectOutOfBoundsActorReference(t *testing.T) {
156 payload := decodeTestJSON(t, `{
157 "status": 200,
158 "type": "topology",
159 "data": {
160 "schema_version": "netdata.topology.v1",
161 "dictionaries": {"strings": ["node", "depends_on"]},
162 "actors": {
163 "rows": 1,
164 "columns": [{"id": "type", "type": "string_ref", "dictionary": "strings"}],
165 "values": [{"codec": "const", "value": 0}]
166 },
167 "links": {
168 "rows": 1,
169 "columns": [
170 {"id": "src_actor", "type": "actor_ref"},
171 {"id": "dst_actor", "type": "actor_ref"},
172 {"id": "type", "type": "string_ref", "dictionary": "strings"}
173 ],
174 "values": [
175 {"codec": "const", "value": 0},
176 {"codec": "const", "value": 2},
177 {"codec": "const", "value": 1}
178 ]
179 }
180 }
181 }`)
182
183 err := topologyv1.ValidateDecodedResponse(payload)
184 if err == nil {
185 t.Fatal("expected validation error")
186 }
187 if !strings.Contains(err.Error(), "actor reference out of bounds") {
188 t.Fatalf("expected actor reference bounds error, got %v", err)
189 }
190 }
191
192 func readTestFile(t *testing.T, path string) []byte {
193 t.Helper()
194 b, err := os.ReadFile(path)
195 if err != nil {
196 t.Fatalf("read %s: %v", path, err)
197 }
198 return b
199 }
200
201 func decodeTestJSON(t *testing.T, input string) any {
202 t.Helper()
203 var payload any
204 if err := json.Unmarshal([]byte(input), &payload); err != nil {
205 t.Fatalf("decode test JSON: %v", err)
206 }
207 return payload
208 }