master
go 256 lines 7.89 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package l2topology
4
5 import (
6 "context"
7 "errors"
8 "net/netip"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/require"
13 )
14
15 type fakeObservationProvider struct {
16 cidrObs []L2Observation
17 deviceObs []L2Observation
18 cidrErr error
19 deviceErr error
20 cidrReqs []CIDRRequest
21 deviceReqs []DeviceRequest
22 }
23
24 func (p *fakeObservationProvider) ObserveByCIDRs(_ context.Context, req CIDRRequest) ([]L2Observation, error) {
25 p.cidrReqs = append(p.cidrReqs, req)
26 if p.cidrErr != nil {
27 return nil, p.cidrErr
28 }
29 return p.cidrObs, nil
30 }
31
32 func (p *fakeObservationProvider) ObserveByDevices(_ context.Context, req DeviceRequest) ([]L2Observation, error) {
33 p.deviceReqs = append(p.deviceReqs, req)
34 if p.deviceErr != nil {
35 return nil, p.deviceErr
36 }
37 return p.deviceObs, nil
38 }
39
40 func TestNewRuntimeEngine_RequiresProvider(t *testing.T) {
41 _, err := NewRuntimeEngine(nil)
42 require.Error(t, err)
43 require.ErrorIs(t, err, ErrInvalidRequest)
44 }
45
46 func TestRuntimeEngine_DiscoverByDevices_BuildsResult(t *testing.T) {
47 provider := &fakeObservationProvider{
48 deviceObs: []L2Observation{
49 {
50 DeviceID: "switch-a",
51 Hostname: "switch-a.example.net",
52 ManagementIP: "10.0.0.1",
53 LLDPRemotes: []LLDPRemoteObservation{
54 {
55 LocalPortNum: "8",
56 LocalPortID: "Gi0/0",
57 SysName: "switch-b.example.net",
58 PortID: "Gi0/1",
59 },
60 },
61 },
62 {
63 DeviceID: "switch-b",
64 Hostname: "switch-b.example.net",
65 },
66 },
67 }
68 eng, err := NewRuntimeEngine(provider)
69 require.NoError(t, err)
70
71 req := DeviceRequest{
72 Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}},
73 Options: DiscoverOptions{EnableLLDP: true},
74 }
75
76 result, err := eng.DiscoverByDevices(context.Background(), req)
77 require.NoError(t, err)
78 require.Len(t, provider.deviceReqs, 1)
79 require.Len(t, result.Adjacencies, 1)
80 require.Equal(t, "lldp", result.Adjacencies[0].Protocol)
81 require.Equal(t, "switch-a", result.Adjacencies[0].SourceID)
82 require.Equal(t, "switch-b", result.Adjacencies[0].TargetID)
83 }
84
85 func TestRuntimeEngine_DiscoverByDevices_PropagatesCollectedAt(t *testing.T) {
86 sourceZone := time.FixedZone("UTC+02", 2*60*60)
87 collectedAt := time.Date(2026, time.April, 2, 3, 4, 5, 0, sourceZone)
88 expectedCollectedAt := collectedAt.UTC()
89 provider := &fakeObservationProvider{
90 deviceObs: []L2Observation{
91 {
92 DeviceID: "switch-a",
93 Hostname: "switch-a.example.net",
94 },
95 },
96 }
97 eng, err := NewRuntimeEngine(provider)
98 require.NoError(t, err)
99
100 req := DeviceRequest{
101 Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}},
102 Options: DiscoverOptions{CollectedAt: collectedAt},
103 }
104
105 result, err := eng.DiscoverByDevices(context.Background(), req)
106 require.NoError(t, err)
107 require.Len(t, provider.deviceReqs, 1)
108 require.Equal(t, expectedCollectedAt, provider.deviceReqs[0].Options.CollectedAt)
109 require.Equal(t, expectedCollectedAt, result.CollectedAt)
110 }
111
112 func TestRuntimeEngine_DiscoverByCIDRs_InvalidRequest(t *testing.T) {
113 eng, err := NewRuntimeEngine(&fakeObservationProvider{})
114 require.NoError(t, err)
115
116 _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{})
117 require.Error(t, err)
118 require.ErrorIs(t, err, ErrInvalidRequest)
119 }
120
121 func TestRuntimeEngine_DiscoverByCIDRs_InvalidPrefix(t *testing.T) {
122 provider := &fakeObservationProvider{}
123 eng, err := NewRuntimeEngine(provider)
124 require.NoError(t, err)
125
126 _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{
127 CIDRs: []netip.Prefix{{}},
128 })
129 require.Error(t, err)
130 require.ErrorIs(t, err, ErrInvalidRequest)
131 require.ErrorContains(t, err, "cidrs[0] has invalid prefix")
132 require.Empty(t, provider.cidrReqs)
133 }
134
135 func TestRuntimeEngine_DiscoverByDevices_InvalidRequest(t *testing.T) {
136 eng, err := NewRuntimeEngine(&fakeObservationProvider{})
137 require.NoError(t, err)
138
139 _, err = eng.DiscoverByDevices(context.Background(), DeviceRequest{})
140 require.Error(t, err)
141 require.ErrorIs(t, err, ErrInvalidRequest)
142 require.ErrorContains(t, err, "devices are required")
143 }
144
145 func TestRuntimeEngine_DiscoverByDevices_InvalidAddress(t *testing.T) {
146 eng, err := NewRuntimeEngine(&fakeObservationProvider{})
147 require.NoError(t, err)
148
149 _, err = eng.DiscoverByDevices(context.Background(), DeviceRequest{
150 Devices: []DeviceTarget{{Address: netip.Addr{}}},
151 })
152 require.Error(t, err)
153 require.ErrorIs(t, err, ErrInvalidRequest)
154 }
155
156 func TestRuntimeEngine_DiscoverByCIDRs_ProviderError(t *testing.T) {
157 providerErr := errors.New("provider failed")
158 provider := &fakeObservationProvider{cidrErr: providerErr}
159 eng, err := NewRuntimeEngine(provider)
160 require.NoError(t, err)
161
162 _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{
163 CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
164 })
165 require.Error(t, err)
166 require.ErrorIs(t, err, providerErr)
167 }
168
169 func TestRuntimeEngine_EmptyObservationsReturnEmptyResult(t *testing.T) {
170 provider := &fakeObservationProvider{}
171 eng, err := NewRuntimeEngine(provider)
172 require.NoError(t, err)
173
174 sourceZone := time.FixedZone("UTC-03", -3*60*60)
175 collectedAt := time.Date(2026, time.April, 2, 3, 4, 5, 0, sourceZone)
176 expectedCollectedAt := collectedAt.UTC()
177
178 result, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{
179 CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
180 Options: DiscoverOptions{CollectedAt: collectedAt},
181 })
182 require.NoError(t, err)
183 require.Len(t, provider.cidrReqs, 1)
184 require.Equal(t, expectedCollectedAt, provider.cidrReqs[0].Options.CollectedAt)
185 require.Equal(t, expectedCollectedAt, result.CollectedAt)
186 require.Empty(t, result.Devices)
187 require.Empty(t, result.Adjacencies)
188 require.Equal(t, 0, result.Stats["links_total"])
189 require.Equal(t, 0, result.Stats["identity_alias_endpoints_mapped"])
190 require.Equal(t, 0, result.Stats["identity_alias_endpoints_ambiguous_mac"])
191 require.Equal(t, 0, result.Stats["identity_alias_ips_merged"])
192 require.Equal(t, 0, result.Stats["identity_alias_ips_conflict_skipped"])
193 }
194
195 func TestRuntimeEngine_EmptyResultStatsSchemaMatchesPipelineResult(t *testing.T) {
196 emptyStats := emptyResult(time.Time{}).Stats
197 pipelineResult, err := BuildL2ResultFromObservations([]L2Observation{{DeviceID: "switch-a"}}, DiscoverOptions{})
198 require.NoError(t, err)
199
200 require.ElementsMatch(t, statsKeys(pipelineResult.Stats), statsKeys(emptyStats))
201 }
202
203 func TestRuntimeEngine_DiscoverByDevices_NilReceiver(t *testing.T) {
204 var eng *RuntimeEngine
205 _, err := eng.DiscoverByDevices(context.Background(), DeviceRequest{
206 Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}},
207 })
208 require.Error(t, err)
209 require.ErrorIs(t, err, ErrInvalidRequest)
210 }
211
212 func TestRuntimeEngine_DiscoverByDevices_NilProvider(t *testing.T) {
213 eng := &RuntimeEngine{}
214 _, err := eng.DiscoverByDevices(context.Background(), DeviceRequest{
215 Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}},
216 })
217 require.Error(t, err)
218 require.ErrorIs(t, err, ErrInvalidRequest)
219 }
220
221 func statsKeys(stats map[string]any) []string {
222 keys := make([]string, 0, len(stats))
223 for key := range stats {
224 keys = append(keys, key)
225 }
226 return keys
227 }
228
229 func TestRuntimeEngine_DiscoverByCIDRs_NilReceiver(t *testing.T) {
230 var eng *RuntimeEngine
231 _, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{
232 CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
233 })
234 require.Error(t, err)
235 require.ErrorIs(t, err, ErrInvalidRequest)
236 }
237
238 func TestRuntimeEngine_DiscoverByCIDRs_NilProvider(t *testing.T) {
239 eng := &RuntimeEngine{}
240 _, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{
241 CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
242 })
243 require.Error(t, err)
244 require.ErrorIs(t, err, ErrInvalidRequest)
245 }
246
247 func TestEnsureCollectedAt_DefaultsZeroToUTCNow(t *testing.T) {
248 before := time.Now().UTC()
249 opts := ensureCollectedAt(DiscoverOptions{})
250 after := time.Now().UTC()
251
252 require.False(t, opts.CollectedAt.IsZero())
253 require.Equal(t, time.UTC, opts.CollectedAt.Location())
254 require.False(t, opts.CollectedAt.Before(before))
255 require.False(t, opts.CollectedAt.After(after))
256 }