@@ -3,14 +3,22 @@ package main
33import (
44 "bytes"
55 "context"
6+ "encoding/json"
67 "io"
78 "net/http"
9+ "net/http/httptest"
10+ "strings"
811 "testing"
12+ "time"
913
1014 "github.com/Azure/azure-container-networking/cns"
1115 "github.com/Azure/azure-container-networking/cns/fakes"
1216 "github.com/Azure/azure-container-networking/cns/logger"
17+ "github.com/Azure/azure-container-networking/crd/multitenancy/api/v1alpha1"
1318 "github.com/stretchr/testify/assert"
19+ corev1 "k8s.io/api/core/v1"
20+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+ "k8s.io/client-go/rest"
1422)
1523
1624// MockHTTPClient is a mock implementation of HTTPClient
@@ -69,3 +77,181 @@ func TestSendRegisterNodeRequest_StatusAccepted(t *testing.T) {
6977
7078 assert .Error (t , sendRegisterNodeRequest (ctx , mockClient , httpServiceFake , nodeRegisterReq , url ))
7179}
80+
81+ func TestCreateOrUpdateNodeInfoCRD_WithTestifyMock_DirectCall (t * testing.T ) {
82+ // Create mock IMDS server
83+ mockIMDSServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
84+ if strings .Contains (r .URL .Path , "/metadata/instance/compute" ) {
85+ w .Header ().Set ("Content-Type" , "application/json" )
86+ w .WriteHeader (http .StatusOK )
87+ response := map [string ]interface {}{
88+ "vmId" : "test-vm-unique-id-12345" ,
89+ "name" : "test-vm" ,
90+ "resourceGroupName" : "test-rg" ,
91+ }
92+ json .NewEncoder (w ).Encode (response )
93+ return
94+ }
95+ w .WriteHeader (http .StatusNotFound )
96+ }))
97+ defer mockIMDSServer .Close ()
98+
99+ // Create mock CNS server
100+ mockCNSServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
101+ if strings .Contains (r .URL .Path , "/homeaz" ) || strings .Contains (r .URL .Path , "homeaz" ) {
102+ w .Header ().Set ("Content-Type" , "application/json" )
103+ w .WriteHeader (http .StatusOK )
104+ response := map [string ]interface {}{
105+ "ReturnCode" : 0 ,
106+ "Message" : "" ,
107+ "HomeAzResponse" : map [string ]interface {}{
108+ "IsSupported" : true ,
109+ "HomeAz" : uint (2 ),
110+ },
111+ }
112+ json .NewEncoder (w ).Encode (response )
113+ return
114+ }
115+ w .WriteHeader (http .StatusNotFound )
116+ }))
117+ defer mockCNSServer .Close ()
118+
119+ // Set up HTTP transport to mock IMDS and CNS
120+ originalTransport := http .DefaultTransport
121+ defer func () { http .DefaultTransport = originalTransport }()
122+
123+ http .DefaultTransport = & mockTransport {
124+ imdsServer : mockIMDSServer ,
125+ cnsServer : mockCNSServer ,
126+ original : originalTransport ,
127+ }
128+
129+ // Create a mock Kubernetes server that captures the NodeInfo being created
130+ var capturedNodeInfo * v1alpha1.NodeInfo
131+
132+ mockK8sServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
133+ // Handle specific API group discovery - multitenancy.acn.azure.com
134+ if r .URL .Path == "/apis/multitenancy.acn.azure.com/v1alpha1" && r .Method == "GET" {
135+ w .Header ().Set ("Content-Type" , "application/json" )
136+ w .WriteHeader (http .StatusOK )
137+ json .NewEncoder (w ).Encode (map [string ]interface {}{
138+ "kind" : "APIResourceList" ,
139+ "groupVersion" : "multitenancy.acn.azure.com/v1alpha1" ,
140+ "resources" : []map [string ]interface {}{
141+ {
142+ "name" : "nodeinfos" ,
143+ "singularName" : "nodeinfo" ,
144+ "namespaced" : false ,
145+ "kind" : "NodeInfo" ,
146+ "verbs" : []string {"create" , "delete" , "get" , "list" , "patch" , "update" , "watch" },
147+ },
148+ },
149+ })
150+ return
151+ }
152+
153+ // Handle NodeInfo resource requests
154+ if strings .Contains (r .URL .Path , "nodeinfos" ) || strings .Contains (r .URL .Path , "multitenancy" ) {
155+ if r .Method == "POST" || r .Method == "PATCH" || r .Method == "PUT" {
156+ body , _ := io .ReadAll (r .Body )
157+
158+ // Try to parse the NodeInfo from the request
159+ var nodeInfo v1alpha1.NodeInfo
160+ if err := json .Unmarshal (body , & nodeInfo ); err == nil {
161+ capturedNodeInfo = & nodeInfo
162+ }
163+
164+ w .Header ().Set ("Content-Type" , "application/json" )
165+ w .WriteHeader (http .StatusOK )
166+ // Return the created NodeInfo
167+ json .NewEncoder (w ).Encode (map [string ]interface {}{
168+ "apiVersion" : "multitenancy.acn.azure.com/v1alpha1" ,
169+ "kind" : "NodeInfo" ,
170+ "metadata" : map [string ]interface {}{
171+ "name" : "test-node" ,
172+ },
173+ "spec" : map [string ]interface {}{
174+ "vmUniqueID" : "test-vm-unique-id-12345" ,
175+ "homeAZ" : "AZ02" ,
176+ },
177+ })
178+ return
179+ }
180+
181+ // Handle GET requests (checking if NodeInfo exists)
182+ if r .Method == "GET" {
183+ w .Header ().Set ("Content-Type" , "application/json" )
184+ w .WriteHeader (http .StatusNotFound ) // Simulate NodeInfo doesn't exist yet
185+ json .NewEncoder (w ).Encode (map [string ]interface {}{
186+ "kind" : "Status" ,
187+ "status" : "Failure" ,
188+ "code" : 404 ,
189+ })
190+ return
191+ }
192+ }
193+
194+ // Default success response for any other API calls
195+ w .Header ().Set ("Content-Type" , "application/json" )
196+ w .WriteHeader (http .StatusOK )
197+ json .NewEncoder (w ).Encode (map [string ]interface {}{
198+ "kind" : "Status" ,
199+ "status" : "Success" ,
200+ })
201+ }))
202+ defer mockK8sServer .Close ()
203+
204+ // Test the function with mocked dependencies
205+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
206+ defer cancel ()
207+
208+ // Point to our mock Kubernetes server
209+ restConfig := & rest.Config {
210+ Host : mockK8sServer .URL ,
211+ }
212+
213+ node := & corev1.Node {
214+ ObjectMeta : metav1.ObjectMeta {Name : "test-node" },
215+ }
216+
217+ // Call the createOrUpdateNodeInfoCRD function
218+ err := createOrUpdateNodeInfoCRD (ctx , restConfig , node )
219+
220+ // Verify the function succeeded
221+ assert .NoError (t , err , "Function should succeed with mocked dependencies" )
222+
223+ // Verify the captured values
224+ assert .NotNil (t , capturedNodeInfo , "NodeInfo should have been captured from K8s API call" )
225+ if capturedNodeInfo != nil {
226+ assert .Equal (t , "test-node" , capturedNodeInfo .Name , "NodeInfo name should match" )
227+ assert .Equal (t , "test-vm-unique-id-12345" , capturedNodeInfo .Spec .VMUniqueID , "VMUniqueID should be from IMDS" )
228+ assert .Equal (t , "AZ02" , capturedNodeInfo .Spec .HomeAZ , "HomeAZ should be formatted from CNS response" )
229+ }
230+ }
231+
232+ // mockTransport redirects HTTP requests to mock servers for testing.
233+ // It intercepts requests to IMDS and CNS endpoints and routes them to local test servers.
234+ type mockTransport struct {
235+ imdsServer * httptest.Server
236+ cnsServer * httptest.Server
237+ original http.RoundTripper
238+ }
239+
240+ func (m * mockTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
241+ // Redirect IMDS calls to mock IMDS server
242+ if req .URL .Host == "169.254.169.254" {
243+ req .URL .Scheme = "http"
244+ req .URL .Host = strings .TrimPrefix (m .imdsServer .URL , "http://" )
245+ return m .original .RoundTrip (req )
246+ }
247+
248+ // Redirect CNS calls to mock CNS server
249+ if req .URL .Host == "localhost:10090" || strings .Contains (req .URL .Host , "10090" ) {
250+ req .URL .Scheme = "http"
251+ req .URL .Host = strings .TrimPrefix (m .cnsServer .URL , "http://" )
252+ return m .original .RoundTrip (req )
253+ }
254+
255+ // All other calls go through original transport
256+ return m .original .RoundTrip (req )
257+ }
0 commit comments