master
go 439 lines 9.62 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package iprange
4
5 import (
6 "errors"
7 "net/netip"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 )
13
14 func TestParseRanges(t *testing.T) {
15 t.Parallel()
16
17 tests := []struct {
18 name string
19 input string
20 wantRanges []Range
21 wantErr bool
22 }{
23 {
24 name: "empty string",
25 input: "",
26 },
27 {
28 name: "whitespace only",
29 input: " \t\n ",
30 },
31 {
32 name: "single range",
33 input: "192.0.2.0-192.0.2.10",
34 wantRanges: []Range{
35 mustParseRange(t, "192.0.2.0", "192.0.2.10"),
36 },
37 },
38 {
39 name: "multiple ranges with different formats",
40 input: "2001:db8::0 192.0.2.0-192.0.2.10 2001:db8::0/126 192.0.2.0/255.255.255.0",
41 wantRanges: []Range{
42 mustParseRange(t, "2001:db8::0", "2001:db8::0"),
43 mustParseRange(t, "192.0.2.0", "192.0.2.10"),
44 mustParseRange(t, "2001:db8::1", "2001:db8::2"),
45 mustParseRange(t, "192.0.2.1", "192.0.2.254"),
46 },
47 },
48 {
49 name: "single invalid syntax",
50 input: "192.0.2.0-192.0.2.",
51 wantErr: true,
52 },
53 {
54 name: "multiple with one invalid",
55 input: "2001:db8::0 192.0.2.0-192.0.2.10 2001:db8::0/999 192.0.2.0/255.255.255.0",
56 wantErr: true,
57 },
58 {
59 name: "extra whitespace",
60 input: " 192.0.2.0 192.0.2.1-192.0.2.2 ",
61 wantRanges: []Range{
62 mustParseRange(t, "192.0.2.0", "192.0.2.0"),
63 mustParseRange(t, "192.0.2.1", "192.0.2.2"),
64 },
65 },
66 }
67
68 for _, tt := range tests {
69 t.Run(tt.name, func(t *testing.T) {
70 t.Parallel()
71
72 ranges, err := ParseRanges(tt.input)
73
74 if tt.wantErr {
75 assert.Error(t, err)
76 assert.Nil(t, ranges)
77 } else {
78 assert.NoError(t, err)
79 assert.Equal(t, tt.wantRanges, ranges)
80 }
81 })
82 }
83 }
84
85 func TestParseRange(t *testing.T) {
86 t.Parallel()
87
88 tests := []struct {
89 name string
90 input string
91 wantRange Range
92 wantErr error
93 }{
94 // Empty input
95 {
96 name: "empty string",
97 input: "",
98 },
99 {
100 name: "whitespace only",
101 input: " ",
102 },
103
104 // IPv4 single IP
105 {
106 name: "IPv4 single IP",
107 input: "192.0.2.0",
108 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.0"),
109 },
110 {
111 name: "IPv4 invalid address",
112 input: "192.0.2.",
113 wantErr: ErrInvalidSyntax,
114 },
115
116 // IPv4 ranges
117 {
118 name: "IPv4 range",
119 input: "192.0.2.0-192.0.2.10",
120 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.10"),
121 },
122 {
123 name: "IPv4 range start equals end",
124 input: "192.0.2.0-192.0.2.0",
125 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.0"),
126 },
127 {
128 name: "IPv4 range start > end",
129 input: "192.0.2.10-192.0.2.0",
130 wantErr: ErrInvalidRange,
131 },
132 {
133 name: "IPv4 range invalid start",
134 input: "192.0.2.-192.0.2.10",
135 wantErr: ErrInvalidSyntax,
136 },
137 {
138 name: "IPv4 range invalid end",
139 input: "192.0.2.0-192.0.2.",
140 wantErr: ErrInvalidSyntax,
141 },
142 {
143 name: "IPv4 range with IPv6 start",
144 input: "2001:db8::0-192.0.2.10",
145 wantErr: ErrMixedAddressFamilies,
146 },
147 {
148 name: "IPv4 range with IPv6 end",
149 input: "192.0.2.0-2001:db8::0",
150 wantErr: ErrMixedAddressFamilies,
151 },
152 {
153 name: "IPv4 range with spaces",
154 input: " 192.0.2.0 - 192.0.2.10 ",
155 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.10"),
156 },
157
158 // IPv4 CIDR
159 {
160 name: "IPv4 CIDR /0",
161 input: "192.0.2.0/0",
162 wantRange: mustParseRange(t, "0.0.0.1", "255.255.255.254"),
163 },
164 {
165 name: "IPv4 CIDR /24",
166 input: "192.0.2.0/24",
167 wantRange: mustParseRange(t, "192.0.2.1", "192.0.2.254"),
168 },
169 {
170 name: "IPv4 CIDR /30",
171 input: "192.0.2.0/30",
172 wantRange: mustParseRange(t, "192.0.2.1", "192.0.2.2"),
173 },
174 {
175 name: "IPv4 CIDR /31",
176 input: "192.0.2.0/31",
177 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.1"),
178 },
179 {
180 name: "IPv4 CIDR /32",
181 input: "192.0.2.0/32",
182 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.0"),
183 },
184 {
185 name: "IPv4 CIDR non-network address",
186 input: "192.0.2.10/24",
187 wantRange: mustParseRange(t, "192.0.2.1", "192.0.2.254"),
188 },
189 {
190 name: "IPv4 CIDR missing prefix",
191 input: "192.0.2.0/",
192 wantErr: ErrInvalidSyntax,
193 },
194 {
195 name: "IPv4 CIDR invalid prefix",
196 input: "192.0.2.0/99",
197 wantErr: ErrInvalidSyntax,
198 },
199
200 // IPv4 subnet mask
201 {
202 name: "IPv4 mask /0",
203 input: "192.0.2.0/0.0.0.0",
204 wantRange: mustParseRange(t, "0.0.0.1", "255.255.255.254"),
205 },
206 {
207 name: "IPv4 mask /24",
208 input: "192.0.2.0/255.255.255.0",
209 wantRange: mustParseRange(t, "192.0.2.1", "192.0.2.254"),
210 },
211 {
212 name: "IPv4 mask /30",
213 input: "192.0.2.0/255.255.255.252",
214 wantRange: mustParseRange(t, "192.0.2.1", "192.0.2.2"),
215 },
216 {
217 name: "IPv4 mask /31",
218 input: "192.0.2.0/255.255.255.254",
219 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.1"),
220 },
221 {
222 name: "IPv4 mask /32",
223 input: "192.0.2.0/255.255.255.255",
224 wantRange: mustParseRange(t, "192.0.2.0", "192.0.2.0"),
225 },
226 {
227 name: "IPv4 mask invalid",
228 input: "192.0.2.0/mask",
229 wantErr: ErrInvalidSyntax,
230 },
231 {
232 name: "IPv4 mask non-contiguous",
233 input: "192.0.2.0/255.255.0.254",
234 wantErr: ErrInvalidSyntax,
235 },
236 {
237 name: "IPv4 mask with IPv6 address",
238 input: "2001:db8::/255.255.255.0",
239 wantErr: ErrInvalidSyntax,
240 },
241
242 // IPv6 single IP
243 {
244 name: "IPv6 single IP",
245 input: "2001:db8::0",
246 wantRange: mustParseRange(t, "2001:db8::0", "2001:db8::0"),
247 },
248 {
249 name: "IPv6 invalid address",
250 input: "2001:db8",
251 wantErr: ErrInvalidSyntax,
252 },
253
254 // IPv6 ranges
255 {
256 name: "IPv6 range",
257 input: "2001:db8::-2001:db8::10",
258 wantRange: mustParseRange(t, "2001:db8::", "2001:db8::10"),
259 },
260 {
261 name: "IPv6 range start equals end",
262 input: "2001:db8::-2001:db8::",
263 wantRange: mustParseRange(t, "2001:db8::", "2001:db8::"),
264 },
265 {
266 name: "IPv6 range start > end",
267 input: "2001:db8::10-2001:db8::",
268 wantErr: ErrInvalidRange,
269 },
270 {
271 name: "IPv6 range invalid start",
272 input: "2001:db8-2001:db8::10",
273 wantErr: ErrInvalidSyntax,
274 },
275 {
276 name: "IPv6 range invalid end",
277 input: "2001:db8::-2001:db8",
278 wantErr: ErrInvalidSyntax,
279 },
280 {
281 name: "IPv6 range with IPv4 start",
282 input: "192.0.2.0-2001:db8::10",
283 wantErr: ErrMixedAddressFamilies,
284 },
285 {
286 name: "IPv6 range with IPv4 end",
287 input: "2001:db8::-192.0.2.10",
288 wantErr: ErrMixedAddressFamilies,
289 },
290
291 // IPv6 CIDR
292 {
293 name: "IPv6 CIDR /0",
294 input: "2001:db8::/0",
295 wantRange: mustParseRange(t, "::1", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"),
296 },
297 {
298 name: "IPv6 CIDR /64",
299 input: "2001:db8::/64",
300 wantRange: mustParseRange(t, "2001:db8::1", "2001:db8::ffff:ffff:ffff:fffe"),
301 },
302 {
303 name: "IPv6 CIDR /126",
304 input: "2001:db8::/126",
305 wantRange: mustParseRange(t, "2001:db8::1", "2001:db8::2"),
306 },
307 {
308 name: "IPv6 CIDR /127",
309 input: "2001:db8::/127",
310 wantRange: mustParseRange(t, "2001:db8::", "2001:db8::1"),
311 },
312 {
313 name: "IPv6 CIDR /128",
314 input: "2001:db8::/128",
315 wantRange: mustParseRange(t, "2001:db8::", "2001:db8::"),
316 },
317 {
318 name: "IPv6 CIDR non-network address",
319 input: "2001:db8::10/64",
320 wantRange: mustParseRange(t, "2001:db8::1", "2001:db8::ffff:ffff:ffff:fffe"),
321 },
322 {
323 name: "IPv6 CIDR missing prefix",
324 input: "2001:db8::/",
325 wantErr: ErrInvalidSyntax,
326 },
327 {
328 name: "IPv6 CIDR invalid prefix",
329 input: "2001:db8::/999",
330 wantErr: ErrInvalidSyntax,
331 },
332
333 // Case sensitivity
334 {
335 name: "mixed case IPv6",
336 input: "2001:DB8::A-2001:DB8::F",
337 wantRange: mustParseRange(t, "2001:db8::a", "2001:db8::f"),
338 },
339 }
340
341 for _, tt := range tests {
342 t.Run(tt.name, func(t *testing.T) {
343 t.Parallel()
344
345 r, err := ParseRange(tt.input)
346
347 if tt.wantErr != nil {
348 assert.Error(t, err)
349 if !errors.Is(err, tt.wantErr) {
350 assert.ErrorContains(t, err, tt.wantErr.Error())
351 }
352 assert.Nil(t, r)
353 } else {
354 assert.NoError(t, err)
355 assert.Equal(t, tt.wantRange, r)
356 }
357 })
358 }
359 }
360
361 func TestMaskToPrefixLen(t *testing.T) {
362 t.Parallel()
363
364 tests := []struct {
365 name string
366 mask string
367 want int
368 wantOk bool
369 }{
370 {"all zeros", "0.0.0.0", 0, true},
371 {"all ones", "255.255.255.255", 32, true},
372 {"/8", "255.0.0.0", 8, true},
373 {"/16", "255.255.0.0", 16, true},
374 {"/24", "255.255.255.0", 24, true},
375 {"/25", "255.255.255.128", 25, true},
376 {"/30", "255.255.255.252", 30, true},
377 {"/31", "255.255.255.254", 31, true},
378 {"non-contiguous", "255.255.0.254", 0, false},
379 {"holes in mask", "255.0.255.0", 0, false},
380 }
381
382 for _, tt := range tests {
383 t.Run(tt.name, func(t *testing.T) {
384 t.Parallel()
385
386 mask := netip.MustParseAddr(tt.mask)
387 got, ok := maskToPrefixLen(mask)
388
389 assert.Equal(t, tt.wantOk, ok)
390 if ok {
391 assert.Equal(t, tt.want, got)
392 }
393 })
394 }
395 }
396
397 // Helper function to create a range for testing
398 func mustParseRange(t *testing.T, start, end string) Range {
399 t.Helper()
400
401 startAddr, err := netip.ParseAddr(start)
402 require.NoError(t, err)
403
404 endAddr, err := netip.ParseAddr(end)
405 require.NoError(t, err)
406
407 r := New(startAddr, endAddr)
408 require.NotNil(t, r)
409
410 return r
411 }
412
413 // Benchmark tests
414 func BenchmarkParseRange_IPv4(b *testing.B) {
415 inputs := []string{
416 "192.0.2.1",
417 "192.0.2.0-192.0.2.255",
418 "192.0.2.0/24",
419 "192.0.2.0/255.255.255.0",
420 }
421
422 b.ResetTimer()
423 for i := 0; i < b.N; i++ {
424 _, _ = ParseRange(inputs[i%len(inputs)])
425 }
426 }
427
428 func BenchmarkParseRange_IPv6(b *testing.B) {
429 inputs := []string{
430 "2001:db8::1",
431 "2001:db8::-2001:db8::ffff",
432 "2001:db8::/64",
433 }
434
435 b.ResetTimer()
436 for i := 0; i < b.N; i++ {
437 _, _ = ParseRange(inputs[i%len(inputs)])
438 }
439 }