11package client
22
33import (
4+ "crypto/rand"
5+ "encoding/binary"
6+ "errors"
47 "fmt"
8+ mrand "math/rand"
9+ "net"
510 "os"
611 "testing"
712 "time"
@@ -49,6 +54,169 @@ func setUpClient(t *testing.T) *cloudconnexa.Client {
4954 return client
5055}
5156
57+ // cidrOverlaps returns true if two IPv4 networks overlap (IPv4 only, safe)
58+ func cidrOverlaps (a * net.IPNet , b * net.IPNet ) bool {
59+ if a == nil || b == nil {
60+ return false
61+ }
62+ if a .IP .To4 () == nil || b .IP .To4 () == nil {
63+ return false
64+ }
65+ return a .Contains (b .IP ) || b .Contains (a .IP )
66+ }
67+
68+ // parseCIDROrNil parses CIDR string and returns *net.IPNet or nil on error
69+ func parseCIDROrNil (cidr string ) * net.IPNet {
70+ _ , ipnet , err := net .ParseCIDR (cidr )
71+ if err != nil {
72+ return nil
73+ }
74+ return ipnet
75+ }
76+
77+ func shuffledRange (start , end int , rnd * mrand.Rand ) []int {
78+ n := end - start + 1
79+ arr := make ([]int , n )
80+ for i := 0 ; i < n ; i ++ {
81+ arr [i ] = start + i
82+ }
83+ rnd .Shuffle (n , func (i , j int ) { arr [i ], arr [j ] = arr [j ], arr [i ] })
84+ return arr
85+ }
86+
87+ // findAvailableInRange scans used subnets and returns a free 10.a.b.0/24 within [startA, endA]
88+ func findAvailableInRange (used []* net.IPNet , startA , endA int , rnd * mrand.Rand ) (string , bool ) {
89+ // Skip the commonly reserved 10.200.0.0/16 range first
90+ reserved := parseCIDROrNil ("10.200.0.0/16" )
91+ for _ , a := range shuffledRange (startA , endA , rnd ) {
92+ for _ , b := range shuffledRange (0 , 255 , rnd ) {
93+ candidate := fmt .Sprintf ("10.%d.%d.0/24" , a , b )
94+ _ , ipn , err := net .ParseCIDR (candidate )
95+ if err != nil {
96+ continue
97+ }
98+ if reserved != nil && cidrOverlaps (ipn , reserved ) {
99+ continue
100+ }
101+ overlap := false
102+ for _ , u := range used {
103+ if cidrOverlaps (ipn , u ) {
104+ overlap = true
105+ break
106+ }
107+ }
108+ if ! overlap {
109+ return candidate , true
110+ }
111+ }
112+ }
113+ return "" , false
114+ }
115+
116+ // findAvailableInRange172 scans used subnets for a free 172.16-31.b.0/24
117+ func findAvailableInRange172 (used []* net.IPNet , rnd * mrand.Rand ) (string , bool ) {
118+ for _ , a := range shuffledRange (16 , 31 , rnd ) {
119+ for _ , b := range shuffledRange (0 , 255 , rnd ) {
120+ candidate := fmt .Sprintf ("172.%d.%d.0/24" , a , b )
121+ _ , ipn , err := net .ParseCIDR (candidate )
122+ if err != nil {
123+ continue
124+ }
125+ overlap := false
126+ for _ , u := range used {
127+ if cidrOverlaps (ipn , u ) {
128+ overlap = true
129+ break
130+ }
131+ }
132+ if ! overlap {
133+ return candidate , true
134+ }
135+ }
136+ }
137+ return "" , false
138+ }
139+
140+ // findAvailableInRange192168 scans used subnets for a free 192.168.b.0/24
141+ func findAvailableInRange192168 (used []* net.IPNet , rnd * mrand.Rand ) (string , bool ) {
142+ for _ , b := range shuffledRange (0 , 255 , rnd ) {
143+ candidate := fmt .Sprintf ("192.168.%d.0/24" , b )
144+ _ , ipn , err := net .ParseCIDR (candidate )
145+ if err != nil {
146+ continue
147+ }
148+ overlap := false
149+ for _ , u := range used {
150+ if cidrOverlaps (ipn , u ) {
151+ overlap = true
152+ break
153+ }
154+ }
155+ if ! overlap {
156+ return candidate , true
157+ }
158+ }
159+ return "" , false
160+ }
161+
162+ // findAvailableIPv4Subnet scans existing networks' routes and system subnets
163+ // and returns an available RFC1918 /24 subnet that does not overlap
164+ func findAvailableIPv4Subnet (c * cloudconnexa.Client ) (string , error ) {
165+ var seedBytes [8 ]byte
166+ _ , _ = rand .Read (seedBytes [:])
167+ rnd := mrand .New (mrand .NewSource (int64 (binary .LittleEndian .Uint64 (seedBytes [:]))))
168+
169+ networks , err := c .Networks .List ()
170+ if err != nil {
171+ return "" , err
172+ }
173+
174+ var used []* net.IPNet
175+ for _ , n := range networks {
176+ // Collect existing routes via API to ensure we see them
177+ routes , err := c .Routes .List (n .ID )
178+ if err == nil {
179+ for _ , r := range routes {
180+ if r .Subnet == "" {
181+ continue
182+ }
183+ if ipn := parseCIDROrNil (r .Subnet ); ipn != nil {
184+ used = append (used , ipn )
185+ }
186+ }
187+ }
188+ // Collect system subnets from GET network (may not be present in List)
189+ if nn , err := c .Networks .Get (n .ID ); err == nil && nn != nil {
190+ for _ , s := range nn .SystemSubnets {
191+ if ipn := parseCIDROrNil (s ); ipn != nil {
192+ used = append (used , ipn )
193+ }
194+ }
195+ }
196+ }
197+
198+ // Try 10.0.0.0/8 excluding known reserved 10.200.0.0/16, prefer higher ranges
199+ if candidate , ok := findAvailableInRange (used , 201 , 254 , rnd ); ok {
200+ return candidate , nil
201+ }
202+ if candidate , ok := findAvailableInRange (used , 0 , 199 , rnd ); ok {
203+ return candidate , nil
204+ }
205+ if candidate , ok := findAvailableInRange (used , 200 , 200 , rnd ); ok {
206+ return candidate , nil
207+ }
208+ // Try 172.16.0.0/12
209+ if candidate , ok := findAvailableInRange172 (used , rnd ); ok {
210+ return candidate , nil
211+ }
212+ // Try 192.168.0.0/16
213+ if candidate , ok := findAvailableInRange192168 (used , rnd ); ok {
214+ return candidate , nil
215+ }
216+
217+ return "" , fmt .Errorf ("no available /24 subnet found in RFC1918 ranges" )
218+ }
219+
52220// TestListNetworks tests the retrieval of networks using pagination
53221// It verifies that networks can be retrieved successfully
54222func TestListNetworks (t * testing.T ) {
@@ -115,11 +283,7 @@ func TestCreateNetwork(t *testing.T) {
115283 Name : testName ,
116284 VpnRegionID : "it-mxp" ,
117285 }
118- route := cloudconnexa.Route {
119- Description : "test" ,
120- Type : "IP_V4" ,
121- Subnet : fmt .Sprintf ("10.%d.%d.0/24" , timestamp % 256 , (timestamp / 256 )% 256 ),
122- }
286+
123287 network := cloudconnexa.Network {
124288 Description : "test" ,
125289 Egress : false ,
@@ -131,15 +295,46 @@ func TestCreateNetwork(t *testing.T) {
131295 response , err := c .Networks .Create (network )
132296 require .NoError (t , err )
133297 fmt .Printf ("created %s network\n " , response .ID )
134- test , err := c .Routes .Create (response .ID , route )
135- require .NoError (t , err )
136- fmt .Printf ("created %s route\n " , test .ID )
298+ // Ensure cleanup even if subsequent steps fail
299+ defer func () { _ = c .Networks .Delete (response .ID ) }()
300+
301+ // Attempt to create a non-overlapping route with retries to avoid CI matrix collisions
302+ var testRoute * cloudconnexa.Route
303+ var lastErr error
304+ for attempts := 0 ; attempts < 20 ; attempts ++ {
305+ subnet , serr := findAvailableIPv4Subnet (c )
306+ require .NoError (t , serr )
307+ route := cloudconnexa.Route {
308+ Description : "test" ,
309+ Type : "IP_V4" ,
310+ Subnet : subnet ,
311+ }
312+ testRoute , err = c .Routes .Create (response .ID , route )
313+ if err == nil {
314+ fmt .Printf ("created %s route\n " , testRoute .ID )
315+ break
316+ }
317+ lastErr = err
318+ var apiErr * cloudconnexa.ErrClientResponse
319+ if errors .As (err , & apiErr ) {
320+ if apiErr .StatusCode () == 400 {
321+ // Overlap or validation error, refresh and retry
322+ time .Sleep (500 * time .Millisecond )
323+ continue
324+ }
325+ }
326+ // Unexpected error
327+ require .NoError (t , err )
328+ }
329+ require .NoError (t , lastErr )
330+ require .NotNil (t , testRoute )
331+
137332 serviceConfig := cloudconnexa.IPServiceConfig {
138333 ServiceTypes : []string {"ANY" },
139334 }
140335 ipServiceRoute := cloudconnexa.IPServiceRoute {
141336 Description : "test" ,
142- Value : fmt . Sprintf ( "10.%d.%d.0/24" , timestamp % 256 , ( timestamp / 256 ) % 256 ) ,
337+ Value : testRoute . Subnet ,
143338 }
144339 service := cloudconnexa.IPService {
145340 Name : testName ,
0 commit comments