master
go 389 lines 11.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 type typedLicenseRowOption func(*ddsnmp.LicenseRow)
16
17 func profileWithRows(rows ...ddsnmp.LicenseRow) []*ddsnmp.ProfileMetrics {
18 return []*ddsnmp.ProfileMetrics{{
19 Source: "profiles/test-profile.yaml",
20 LicenseRows: rows,
21 }}
22 }
23
24 func typedLicenseRow(id, name string, opts ...typedLicenseRowOption) ddsnmp.LicenseRow {
25 structuralID := ""
26 if id != "" {
27 structuralID = "test-profile|scalar|" + id
28 }
29 row := ddsnmp.LicenseRow{
30 OriginProfileID: "test-profile",
31 StructuralID: structuralID,
32 ID: id,
33 Name: name,
34 }
35 for _, opt := range opts {
36 opt(&row)
37 }
38 return row
39 }
40
41 func withOriginProfileID(id string) typedLicenseRowOption {
42 return func(row *ddsnmp.LicenseRow) {
43 row.OriginProfileID = id
44 }
45 }
46
47 func withStructuralID(id string) typedLicenseRowOption {
48 return func(row *ddsnmp.LicenseRow) {
49 row.StructuralID = id
50 }
51 }
52
53 func withTable(oid, name, rowKey string) typedLicenseRowOption {
54 return func(row *ddsnmp.LicenseRow) {
55 row.TableOID = oid
56 row.Table = name
57 row.RowKey = rowKey
58 if row.StructuralID == "" && row.OriginProfileID != "" && oid != "" && rowKey != "" {
59 row.StructuralID = row.OriginProfileID + "|table|" + oid + "|" + rowKey
60 }
61 }
62 }
63
64 func withState(severity int64, raw string) typedLicenseRowOption {
65 return func(row *ddsnmp.LicenseRow) {
66 row.State.Has = true
67 row.State.Severity = severity
68 row.State.Raw = raw
69 }
70 }
71
72 func withRawState(raw string) typedLicenseRowOption {
73 return func(row *ddsnmp.LicenseRow) {
74 row.State.Raw = raw
75 }
76 }
77
78 func withExpiry(timestamp int64) typedLicenseRowOption {
79 return func(row *ddsnmp.LicenseRow) {
80 row.Expiry.Has = true
81 row.Expiry.Timestamp = timestamp
82 }
83 }
84
85 func withExpiryRemaining(seconds int64) typedLicenseRowOption {
86 return func(row *ddsnmp.LicenseRow) {
87 row.Expiry.Has = true
88 row.Expiry.RemainingSeconds = seconds
89 }
90 }
91
92 func withAuthorizationRemaining(seconds int64) typedLicenseRowOption {
93 return func(row *ddsnmp.LicenseRow) {
94 row.Authorization.Has = true
95 row.Authorization.RemainingSeconds = seconds
96 }
97 }
98
99 func withCertificateRemaining(seconds int64) typedLicenseRowOption {
100 return func(row *ddsnmp.LicenseRow) {
101 row.Certificate.Has = true
102 row.Certificate.RemainingSeconds = seconds
103 }
104 }
105
106 func withGraceRemaining(seconds int64) typedLicenseRowOption {
107 return func(row *ddsnmp.LicenseRow) {
108 row.Grace.Has = true
109 row.Grace.RemainingSeconds = seconds
110 }
111 }
112
113 func withExpirySource(source string) typedLicenseRowOption {
114 return func(row *ddsnmp.LicenseRow) {
115 row.Expiry.SourceOID = source
116 }
117 }
118
119 func withUsage(used int64) typedLicenseRowOption {
120 return func(row *ddsnmp.LicenseRow) {
121 row.Usage.HasUsed = true
122 row.Usage.Used = used
123 }
124 }
125
126 func withCapacity(capacity int64) typedLicenseRowOption {
127 return func(row *ddsnmp.LicenseRow) {
128 row.Usage.HasCapacity = true
129 row.Usage.Capacity = capacity
130 }
131 }
132
133 func withAvailable(available int64) typedLicenseRowOption {
134 return func(row *ddsnmp.LicenseRow) {
135 row.Usage.HasAvailable = true
136 row.Usage.Available = available
137 }
138 }
139
140 func withUsagePercent(percent int64) typedLicenseRowOption {
141 return func(row *ddsnmp.LicenseRow) {
142 row.Usage.HasPercent = true
143 row.Usage.Percent = percent
144 }
145 }
146
147 func withPerpetual() typedLicenseRowOption {
148 return func(row *ddsnmp.LicenseRow) {
149 row.IsPerpetual = true
150 }
151 }
152
153 func withUnlimited() typedLicenseRowOption {
154 return func(row *ddsnmp.LicenseRow) {
155 row.IsUnlimited = true
156 }
157 }
158
159 func TestExtractLicenseRows_FromTypedRows(t *testing.T) {
160 now := time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC)
161 expiry := now.Add(48 * time.Hour).Unix()
162
163 tests := map[string]struct {
164 rows []ddsnmp.LicenseRow
165 assert func(t *testing.T, rows []licenseRow)
166 }{
167 "copies identity descriptors state timer and usage": {
168 rows: []ddsnmp.LicenseRow{
169 typedLicenseRow("base", "Base Firewall",
170 withState(0, "active"),
171 withExpiry(expiry),
172 withUsage(75),
173 withCapacity(100),
174 ),
175 },
176 assert: func(t *testing.T, rows []licenseRow) {
177 require.Len(t, rows, 1)
178 row := rows[0]
179 assert.Equal(t, "base", row.ID)
180 assert.Equal(t, "Base Firewall", row.Name)
181 assert.Equal(t, "test-profile", row.Source)
182 assert.True(t, row.HasState)
183 assert.EqualValues(t, 0, row.StateSeverity)
184 assert.True(t, row.HasExpiry)
185 assert.EqualValues(t, expiry, row.ExpiryTS)
186 assert.True(t, row.HasUsage)
187 assert.EqualValues(t, 75, row.Usage)
188 assert.True(t, row.HasCapacity)
189 assert.EqualValues(t, 100, row.Capacity)
190 assert.True(t, row.HasUsagePct)
191 assert.InDelta(t, 75.0, row.UsagePercent, 0.001)
192 assert.Equal(t, licenseStateBucketHealthy, row.StateBucket)
193 },
194 },
195 "derives usage from capacity minus available": {
196 rows: []ddsnmp.LicenseRow{
197 typedLicenseRow("pool", "Connection pool",
198 withCapacity(100),
199 withAvailable(25),
200 ),
201 },
202 assert: func(t *testing.T, rows []licenseRow) {
203 require.Len(t, rows, 1)
204 assert.True(t, rows[0].HasUsage)
205 assert.EqualValues(t, 75, rows[0].Usage)
206 assert.True(t, rows[0].HasUsagePct)
207 assert.InDelta(t, 75.0, rows[0].UsagePercent, 0.001)
208 },
209 },
210 "rebases remaining timers on collect time": {
211 rows: []ddsnmp.LicenseRow{
212 typedLicenseRow("auth", "Auth", withAuthorizationRemaining(3600)),
213 typedLicenseRow("cert", "Cert", withCertificateRemaining(7200)),
214 typedLicenseRow("grace", "Grace", withGraceRemaining(1800)),
215 typedLicenseRow("sub", "Sub", withExpiryRemaining(600)),
216 },
217 assert: func(t *testing.T, rows []licenseRow) {
218 require.Len(t, rows, 4)
219 byID := make(map[string]licenseRow, len(rows))
220 for _, row := range rows {
221 byID[row.ID] = row
222 }
223 assert.EqualValues(t, now.Unix()+3600, byID["auth"].AuthorizationExpiry)
224 assert.EqualValues(t, now.Unix()+7200, byID["cert"].CertificateExpiry)
225 assert.EqualValues(t, now.Unix()+1800, byID["grace"].GraceExpiry)
226 assert.EqualValues(t, now.Unix()+600, byID["sub"].ExpiryTS)
227 },
228 },
229 "drops rows without identity or signal": {
230 rows: []ddsnmp.LicenseRow{
231 typedLicenseRow("present", "Present", withState(0, "")),
232 typedLicenseRow("", "No ID", withState(0, "")),
233 typedLicenseRow("stub", "No signal"),
234 },
235 assert: func(t *testing.T, rows []licenseRow) {
236 require.Len(t, rows, 1)
237 assert.Equal(t, "present", rows[0].ID)
238 },
239 },
240 "uses table OID as the internal table identity": {
241 rows: []ddsnmp.LicenseRow{
242 typedLicenseRow("FortiCare", "FortiCare",
243 withTable("1.3.6.1.4.1.12356.101.4.6.3.1", "fgLicContractTable", "1"),
244 withCapacity(100),
245 ),
246 typedLicenseRow("FortiCare", "FortiCare",
247 withTable("1.3.6.1.4.1.12356.101.4.6.4.1", "fgLicVersionTable", "1"),
248 withCapacity(200),
249 ),
250 },
251 assert: func(t *testing.T, rows []licenseRow) {
252 require.Len(t, rows, 2)
253 assert.NotEqual(t, rows[0].Table, rows[1].Table)
254 caps := []int64{rows[0].Capacity, rows[1].Capacity}
255 assert.Contains(t, caps, int64(100))
256 assert.Contains(t, caps, int64(200))
257 },
258 },
259 "ignores private metrics": {
260 rows: nil,
261 assert: func(t *testing.T, _ []licenseRow) {
262 pm := &ddsnmp.ProfileMetrics{
263 Source: "profiles/test-profile.yaml",
264 HiddenMetrics: []ddsnmp.Metric{{
265 Name: "_private_metric",
266 Value: 0,
267 Tags: map[string]string{"component": "private"},
268 }},
269 }
270 assert.Empty(t, extractLicenseRows([]*ddsnmp.ProfileMetrics{pm}, now))
271 },
272 },
273 }
274
275 for name, tc := range tests {
276 t.Run(name, func(t *testing.T) {
277 rows := extractLicenseRows(profileWithRows(tc.rows...), now)
278 tc.assert(t, rows)
279 })
280 }
281 }
282
283 func TestNormalizeLicenseStateBucket(t *testing.T) {
284 now := time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC)
285
286 tests := map[string]struct {
287 row ddsnmp.LicenseRow
288 want licenseStateBucket
289 }{
290 "fresh severity recovers from stale broken raw state": {
291 row: typedLicenseRow("renewed", "Renewed", withState(0, "expired")),
292 want: licenseStateBucketHealthy,
293 },
294 "ignored raw state suppresses severity zero": {
295 row: typedLicenseRow("inactive", "Inactive", withState(0, "none")),
296 want: licenseStateBucketIgnored,
297 },
298 "eval raw state becomes informational instead of degraded": {
299 row: typedLicenseRow("eval", "Evaluation", withState(1, "Evaluation")),
300 want: licenseStateBucketInformational,
301 },
302 "raw broken state is used when no fresh severity exists": {
303 row: typedLicenseRow("raw-broken", "Raw Broken", withRawState("expired"), withUsage(1)),
304 want: licenseStateBucketBroken,
305 },
306 "raw degraded state is used when no fresh severity exists": {
307 row: typedLicenseRow("raw-degraded", "Raw Degraded", withRawState("degraded"), withUsage(1)),
308 want: licenseStateBucketDegraded,
309 },
310 "expired timer forces broken over valid raw state": {
311 row: typedLicenseRow("expired", "Expired", withRawState("valid"), withExpiry(now.Add(-time.Hour).Unix())),
312 want: licenseStateBucketBroken,
313 },
314 "grace timer without expiry is degraded": {
315 row: typedLicenseRow("grace", "Grace", withGraceRemaining(3600)),
316 want: licenseStateBucketDegraded,
317 },
318 "fully consumed usage is broken": {
319 row: typedLicenseRow("pool", "Pool", withUsagePercent(100)),
320 want: licenseStateBucketBroken,
321 },
322 }
323
324 for name, tc := range tests {
325 t.Run(name, func(t *testing.T) {
326 rows := extractLicenseRows(profileWithRows(tc.row), now)
327 require.Len(t, rows, 1)
328 assert.Equal(t, tc.want, rows[0].StateBucket)
329 })
330 }
331 }
332
333 func TestExtractLicenseRows_CheckPointPublicFixtureValues(t *testing.T) {
334 now := time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC)
335 tests := map[string]struct {
336 rows []ddsnmp.LicenseRow
337 }{
338 "public Check Point sample values": {
339 rows: []ddsnmp.LicenseRow{
340 typedLicenseRow("0", "Firewall", withState(2, "Not Entitled")),
341 typedLicenseRow("4", "Application Ctrl", withState(1, "Evaluation"), withExpiry(1619246913)),
342 typedLicenseRow("2", "IPS", withState(1, "Evaluation"), withExpiry(1619246941)),
343 },
344 },
345 }
346
347 for name, tc := range tests {
348 t.Run(name, func(t *testing.T) {
349 rows := extractLicenseRows(profileWithRows(tc.rows...), now)
350 require.Len(t, rows, 3)
351
352 for _, row := range rows {
353 switch row.ID {
354 case "0":
355 assert.Equal(t, licenseStateBucketBroken, row.StateBucket, "Not Entitled -> broken")
356 assert.False(t, row.HasExpiry)
357 case "4":
358 assert.Equal(t, licenseStateBucketBroken, row.StateBucket, "expired Evaluation -> broken")
359 assert.True(t, row.HasExpiry)
360 assert.EqualValues(t, 1619246913, row.ExpiryTS)
361 case "2":
362 assert.Equal(t, licenseStateBucketBroken, row.StateBucket, "expired Evaluation -> broken")
363 assert.True(t, row.HasExpiry)
364 assert.EqualValues(t, 1619246941, row.ExpiryTS)
365 default:
366 t.Fatalf("unexpected row id %q", row.ID)
367 }
368 }
369 })
370 }
371 }
372
373 func TestLicenseRowUniqueKeyHandlesEmbeddedNULs(t *testing.T) {
374 tests := map[string]struct {
375 left licenseRow
376 right licenseRow
377 }{
378 "embedded NULs stay unambiguous": {
379 left: licenseRow{Source: "vendor", Table: "table", ID: "row\x00suffix"},
380 right: licenseRow{Source: "vendor", Table: "table\x00row", ID: "suffix"},
381 },
382 }
383
384 for name, tc := range tests {
385 t.Run(name, func(t *testing.T) {
386 assert.NotEqual(t, licenseRowUniqueKey(tc.left), licenseRowUniqueKey(tc.right))
387 })
388 }
389 }