1 package services
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "github.com/gin-gonic/gin"
13 "github.com/stretchr/testify/assert"
14 corev1 "k8s.io/api/core/v1"
15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16 "k8s.io/apimachinery/pkg/runtime"
17 "sigs.k8s.io/controller-runtime/pkg/client"
18
19 api "edge-infra.dev/pkg/edge/device-registrar/api/v1alpha1"
20 "edge-infra.dev/pkg/edge/device-registrar/config"
21 testClient "edge-infra.dev/pkg/edge/device-registrar/utils/test"
22 "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
23 v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
24 )
25
26 func setupTest() (*gin.Engine, *runtime.Scheme) {
27 gin.SetMode(gin.TestMode)
28 router := gin.Default()
29 router.Use(gin.Logger())
30
31
32 scheme := runtime.NewScheme()
33 err := corev1.AddToScheme(scheme)
34 if err != nil {
35 panic(err)
36 }
37 config.AddToScheme(scheme)
38
39 return router, scheme
40 }
41
42 func GenerateMockConnectClient(scheme *runtime.Scheme, test string) client.WithWatch {
43 deviceSecret := &corev1.Secret{
44 ObjectMeta: metav1.ObjectMeta{
45 Name: "example-device-example-application-secret",
46 Namespace: "device-registrar-clients",
47 },
48 Data: map[string][]byte{
49 "client_id": []byte("example-client-id"),
50 "client_secret": []byte("example-client-secret"),
51 },
52 }
53
54 ienNode := &v1ien.IENode{
55 ObjectMeta: metav1.ObjectMeta{
56 Name: "mock-ienode",
57 Namespace: "device-registrar-clients",
58 },
59 Spec: v1ien.IENodeSpec{
60 Role: "worker",
61 Lane: "1",
62 Network: []v1ien.Network{
63 {
64 MacAddress: "90:74:c2:0f:d8:77",
65 Addresses: []string{"192.168.1.2/24"},
66 Gateway4: "192.168.1.1",
67 DHCP4: false,
68 DHCP6: false,
69 },
70 },
71 PrimaryInterface: &v1ien.PrimaryInterface{
72 InterfaceID: "9647e74f-aba3-447f-b327-31ecce7f8d4c",
73 MacAddresses: []string{"90:74:c2:0f:d8:77"},
74 },
75 },
76 }
77
78 externalApplication := &api.ExternalApplication{
79 ObjectMeta: metav1.ObjectMeta{
80 Name: "example-application",
81 Namespace: "device-registrar-clients",
82 },
83 Spec: api.ExternalApplicationSpec{
84 ID: "app-id",
85 Description: "Example external application",
86 ClientTemplate: v1alpha1.Client{
87 ObjectMeta: metav1.ObjectMeta{
88 Name: "example-client",
89 Namespace: "device-registrar-clients",
90 },
91 Spec: v1alpha1.ClientSpec{
92 SecretName: "example-secret",
93 },
94 },
95 },
96 }
97
98 edgeInfo := &corev1.ConfigMap{
99 ObjectMeta: metav1.ObjectMeta{
100 Name: "edge-info",
101 Namespace: "kube-public",
102 },
103 Data: map[string]string{
104 "cluster.edge.id": "mock-cluster-edge-id",
105 },
106 }
107
108 clientCertSecret := &corev1.Secret{
109 ObjectMeta: metav1.ObjectMeta{
110 Name: "example-device-cert",
111 Namespace: "emissary",
112 },
113 Data: map[string][]byte{
114 "tls.crt": []byte("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM5RENDQXBxZ0F3SUJBZ0lRT1Z4M3VNaElrdjBxMWdRSFRtdmswREFLQmdncWhrak9QUVFEQWpBZU1Sd3cKR2dZRFZRUURFeE5rWlhacFkyVXRjbVZuYVhOMGNtRnlMV05oTUI0WERUSTBNVEV5TVRFek1EWTBOVm9YRFRJMQpNVEV5TVRFek1EWTBOVm93WGpGY01CWUdBMVVFQ3hNUGRYSnVPbk4wYjNKbFNVUTZNVEl6TUJvR0ExVUVDeE1UCmRYSnVPbE5PT2tSVFJrVTNSRk5EV0ZnNU1EQW1CZ05WQkFzVEgzVnlianBrWlhacFkyVk9ZVzFsT25SbGMzUXQKWkdWMmFXTmxMVzVoYldVd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNwYi9aMwpXTjRIUEZlQ1dkUmthcVVvajFxUU5QRkJSblVDTmsyZUMzQ3FCOGlIMjV3eVdFSWk1eFl4NUxadDQ4dWZmREwyClNvWmszMlgzUFBNcUdIQmdFbDZ4TzFSRStHaUNkZ3Flc3JtZzJsdlEzdENaanhEZjh2WnpGcTE3dUpQZWNtckkKenJia1M5bDI4emRZaFZKazU1K0h5NlNDTUJBQ0JmbUZvd1NCaHEzb1NQWUJwdHZnUjJJTkhleFRBT2YwbVU5dgozT1hCenBla01sT0VKVmFnS3dPaitOdnl6OFF1ekRZRUlhVVpiYmhsQzROUXRvM2t2Z0R0dy9XZ01hVjVmQ2NWCjZWSEFIMWRFb3RoU1U2Snd4NUtGREFQOU1PTmoxVDJwL3p0QkpuUTRwU2xUWDd1WExiTEZpMWlsZU1qSnVxQncKSmhNY29nMHBqVWVJbnZLMUFnTUJBQUdqZ2E0d2dhc3dFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUhBd0l3REFZRApWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JSMXU5NTFSSHhCNmdxaEFuMTFwbC9MdW5rc29EQmxCZ05WCkhSRUVYakJjZ2hObFpHZGxMbk4wYjNKbExtNWpjaTVqYjNKd2hoOTFjbTQ2WkdWMmFXTmxUbUZ0WlRwMFpYTjAKTFdSbGRtbGpaUzF1WVcxbGhnOTFjbTQ2YzNSdmNtVkpSRG94TWpPR0UzVnlianBUVGpwRVUwWkZOMFJUUTFoWQpPVEF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnVWF3akRaT0Q5RS9JSUY3STBVak1BSHhldGVyaFFXcnYrb1R0CnpuQ2ZYbnNDSVFDMjNkcUdic0FhVjBLdlJvN1NhdGVEWXlHU052T09JTkhFMk9xYUtPcStiZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"),
115 "tls.key": []byte("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcVcvMmQxamVCenhYZ2xuVVpHcWxLSTlha0RUeFFVWjFBalpObmd0d3FnZkloOXVjCk1saENJdWNXTWVTMmJlUExuM3d5OWtxR1pOOWw5enp6S2hod1lCSmVzVHRVUlBob2duWUtucks1b05wYjBON1EKbVk4UTMvTDJjeGF0ZTdpVDNuSnF5TTYyNUV2WmR2TTNXSVZTWk9lZmg4dWtnakFRQWdYNWhhTUVnWWF0NkVqMgpBYWJiNEVkaURSM3NVd0RuOUpsUGI5emx3YzZYcERKVGhDVldvQ3NEby9qYjhzL0VMc3cyQkNHbEdXMjRaUXVEClVMYU41TDRBN2NQMW9ER2xlWHduRmVsUndCOVhSS0xZVWxPaWNNZVNoUXdEL1REalk5VTlxZjg3UVNaME9LVXAKVTErN2x5Mnl4WXRZcFhqSXlicWdjQ1lUSEtJTktZMUhpSjd5dFFJREFRQUJBb0lCQUNjako0a1Z5K05iK3RLdgpNVElvdUJHUVcxam9BYm9VTGo3ZWtEc3JaVkRCRDM3aUtVZ3Z2c2NWSGJsVW5SYnhBVDNMa0hIM2NPZW4rb09MClhwZWdvWVJ2ZWRQeVlscTBEbC9rS0R2VUNMQ3cvM2hWbjFWNUNHclNVajd2UkE5SjZVMyttOC9hZjhCb0RNay8KRU0rdnJBS0d4Z0MxOXovakhpOTVkZE42ZmpYMHA2K0dCd2RObUFoK01lOVBJWlNXeFJWbUpDR3ZOTlJER0Y1SQorNkdVZGFNQU94UGNsK01Nc2hmWkNTNGNGTEVwUHozblpKTlJiZUxiTUhXeks5TFpBZzFXemVBVStwaEZabjRaCitiOFVFNVd3eGtBaWhOZnBCdlRTYTRxL0RmTCtlU0V4YWNoSXQvNUlPK1lHTXVpcTJGSGNaZmNLWkJZdWVEeXMKb2hmd1RrRUNnWUVBeG9HS29DR0dVQTdDNkRUUjkreE9TRzQyZHM1VGVWZ3ZxRlJTbDg5aEgyTk5sdENITlNtSgpQR3V2V2hmN21XK2piZjRwdlhoWWl1V3hjTWNpWmNlQTJaYkttMjVCOEJ0MHE4VjA0YXQ0ZEltQ2p4bktOWndmCjk2WDlQS0pibE1acDRaNDVqMmxQVHMyS3RKTW5oOVcvYWx6S3JTR3IvOEFvQnZwZGtvQ253SGtDZ1lFQTJvTVgKWVVCRjVhWU5XTmZGZG5PL1J6WmdQL1h2TDRxcWw4SERvT0FvcGFSMjZLVzQwVTZHWnF6RllLZTBmYkRLMjlvQQplYXBuSDVJNWw0cjgyeUpnQTQwS2xDbkExb3Vhd3JVTTQxSk53Zkpzd1dINkRZU1U0VnhZVWJXMlBhRkF2Mk92CjFuNjBRVDNZaDhmekxBeUtYNGVjWVBldTR3VUpkUGRtSFRHWURSMENnWUFCamNnSkF0b3JURUpJVVFtSHVFalEKbGxSRXo4NmxkNFEvL0JEOWNUa2dac1dYdGFBcFVWN3FveWtuT21MVXk2UHEyMzkySlRnRU5sSVNRT3pMQVNuSQpDajhod2xZdnkvYzQxUDNhT2w1aUF5V0xlemN5L2pyZDFHWE1FTFZJejlqS1ZGTzlCS1VEUithYkRUL1U5MTVkCk5jYThYalFiZDJTWTBXTGtINit3ZVFLQmdRQ3dQcVFROE1KdjVHdEhpV0hmbEtSblQ5aDZQbWRadFVLN2ZMSEoKaElQRWRzN2gveWoreVpObUpWeGVCV1p6S3JHMGVqVi83STJZelZ4ZWV1QlA3MzM1M3p6MUhHaEpvL2lEcTN4bApyZkRCeWtNbUIxeWtvcGRpM2hUdWN0NDIvMlUxK2JYT0VBeGJ3d0p2SWp0bEFBaHIzUG1vekozbXhoMUdsblZxCmZxSGhrUUtCZ0dYK2N0Z2kzQW5YcGhZdk8rZ0s2Snc2TmhnWTdhaW0zcGJLd3Awd0hwODNGa0hTWHFiMmxmc0kKaTNhTXh1cEZwdXE4S2FmbkUvTERTOHE3dVcwK0ozR0w2WENFZmNqaHFkUUZjQ0RlSlVhL21ZOVhOZi9aT01OMwpVS1UzR0dhSGF2eXJBVllMVWdpVG5NazcycWx4aEJQR25VbUNIWElHSkZLY1N6ZFFYOHdJCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="),
116 },
117 }
118
119 var mockClient client.WithWatch
120 if test == "noSecret" {
121 mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, ienNode, externalApplication)
122 } else if test == "noexternalapp" {
123 mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, deviceSecret, ienNode)
124 } else {
125 mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, deviceSecret, ienNode, externalApplication, edgeInfo, clientCertSecret)
126 }
127
128 return mockClient
129 }
130
131 func TestConnectDevice(t *testing.T) {
132 router, scheme := setupTest()
133 mockClient := GenerateMockConnectClient(scheme, "")
134 injectmockClient(router, mockClient, t)
135
136
137 ctx := context.TODO()
138 createDeviceBinding(ctx, mockClient, t)
139
140 c, w := setupCall(mockClient, t, "")
141
142
143 ConnectDevice(c)
144
145
146 assert.Equal(t, http.StatusOK, w.Code)
147 var resp DeviceConnectResponse
148 err := json.Unmarshal(w.Body.Bytes(), &resp)
149 assert.NoError(t, err)
150 assert.Equal(t, "example-client-id", resp.Applications[0].Config.ClientID)
151 assert.Equal(t, "example-client-secret", resp.Applications[0].Config.ClientSecret)
152 assert.Equal(t, "store.ncr.corp", resp.HostMapping.Host)
153 assert.Equal(t, "", resp.HostMapping.VIP)
154 }
155
156 func TestConnectNoSecret(t *testing.T) {
157 router, scheme := setupTest()
158 mockClient := GenerateMockConnectClient(scheme, "noSecret")
159 injectmockClient(router, mockClient, t)
160
161
162 ctx := context.TODO()
163 createDeviceBinding(ctx, mockClient, t)
164
165 c, w := setupCall(mockClient, t, "")
166
167
168 ConnectDevice(c)
169
170
171 assert.Equal(t, http.StatusInternalServerError, w.Code)
172 }
173
174 func TestConnectNoExternalApp(t *testing.T) {
175 router, scheme := setupTest()
176 mockClient := GenerateMockConnectClient(scheme, "noexternalapp")
177 injectmockClient(router, mockClient, t)
178
179
180 ctx := context.TODO()
181 createDeviceBinding(ctx, mockClient, t)
182
183 c, w := setupCall(mockClient, t, "")
184
185
186 ConnectDevice(c)
187
188
189 assert.Equal(t, http.StatusInternalServerError, w.Code)
190 }
191
192 func TestBadConnectActivation(t *testing.T) {
193 router, scheme := setupTest()
194 mockClient := GenerateMockConnectClient(scheme, "")
195 injectmockClient(router, mockClient, t)
196
197 config.AddToScheme(scheme)
198 c, w := setupCall(mockClient, t, "badactivation")
199
200
201 ConnectDevice(c)
202
203
204 assert.Equal(t, http.StatusUnauthorized, w.Code)
205
206
207 assert.JSONEq(t, `{"error":"invalid activation code"}`, w.Body.String())
208 }
209
210 func TestBadConnectRequest(t *testing.T) {
211 router, scheme := setupTest()
212 mockClient := GenerateMockConnectClient(scheme, "")
213 injectmockClient(router, mockClient, t)
214
215 config.AddToScheme(scheme)
216 c, w := setupCall(mockClient, t, "badRequest")
217
218
219 ConnectDevice(c)
220
221
222 assert.Equal(t, http.StatusBadRequest, w.Code)
223
224
225 assert.JSONEq(t, `{"error":"Device Name is required"}`, w.Body.String())
226 }
227
228 func TestConnectNoActivation(t *testing.T) {
229 router, scheme := setupTest()
230 mockClient := GenerateMockConnectClient(scheme, "")
231 injectmockClient(router, mockClient, t)
232
233 config.AddToScheme(scheme)
234 c, w := setupCall(mockClient, t, "noactivation")
235
236
237 ConnectDevice(c)
238
239
240 assert.Equal(t, http.StatusBadRequest, w.Code)
241
242
243 assert.JSONEq(t, `{"error":"activationCode is required"}`, w.Body.String())
244 }
245
246 func TestConnectNoSN(t *testing.T) {
247 router, scheme := setupTest()
248 mockClient := GenerateMockConnectClient(scheme, "")
249 injectmockClient(router, mockClient, t)
250
251
252 ctx := context.TODO()
253 createDeviceBinding(ctx, mockClient, t)
254
255 c, w := setupCall(mockClient, t, "noSN")
256
257
258 ConnectDevice(c)
259
260
261 assert.Equal(t, http.StatusBadRequest, w.Code)
262
263
264 assert.JSONEq(t, `{"error":"Serial Number is required"}`, w.Body.String())
265 }
266
267 func setupCall(mockClient client.WithWatch, t *testing.T, test string) (*gin.Context, *httptest.ResponseRecorder) {
268 var deviceConnectRequest DeviceConnectRequest
269 if test == "badRequest" {
270 deviceConnectRequest = DeviceConnectRequest{
271 Device: api.ExternalDeviceSpec{},
272 ApplicationIDs: []string{"app-id"},
273 }
274 } else if test == "noSN" {
275 deviceConnectRequest = DeviceConnectRequest{
276 Device: api.ExternalDeviceSpec{
277 Name: "example-device",
278 SN: "",
279 },
280 ApplicationIDs: []string{"app-id"},
281 }
282 } else {
283 deviceConnectRequest = DeviceConnectRequest{
284 Device: api.ExternalDeviceSpec{
285 Name: "example-device",
286 SN: "example-sn",
287 },
288 ApplicationIDs: []string{"app-id"},
289 }
290 }
291
292 requestBody, err := json.Marshal(deviceConnectRequest)
293 assert.NoError(t, err)
294
295 var req *http.Request
296 if test == "noactivation" {
297 req, err = http.NewRequest(http.MethodPost, "/connect/", bytes.NewBuffer(requestBody))
298 } else if test == "badactivation" {
299 req, err = http.NewRequest(http.MethodPost, "/connect/WRONGCODE", bytes.NewBuffer(requestBody))
300 } else {
301 req, err = http.NewRequest(http.MethodPost, "/connect/F7RAXZXBFZQRPAU", bytes.NewBuffer(requestBody))
302 }
303
304 assert.NoError(t, err)
305 req.Header.Set("Content-Type", "application/json")
306
307
308 w := httptest.NewRecorder()
309
310
311 c, _ := gin.CreateTestContext(w)
312 c.Request = req
313 if test == "noactivation" {
314 c.Params = gin.Params{gin.Param{Key: "activationCode", Value: ""}}
315 } else if test == "badactivation" {
316 c.Params = gin.Params{gin.Param{Key: "activationCode", Value: "WRONGCODE"}}
317 } else {
318 c.Params = gin.Params{gin.Param{Key: "activationCode", Value: "F7RAXZXBFZQRPAU"}}
319 }
320
321
322 c.Set("k8sClient", mockClient)
323
324 return c, w
325 }
326
327 func createDeviceBinding(ctx context.Context, mockClient client.WithWatch, t *testing.T) {
328
329 deviceBinding := &api.DeviceBinding{
330 ObjectMeta: metav1.ObjectMeta{
331 Name: "example-device-binding",
332 Namespace: "device-registrar-clients",
333 },
334 Spec: api.DeviceBindingSpec{
335 Device: api.DeviceSubject{
336 APIGroup: api.GroupVersion.Group,
337 Kind: "ExternalDevice",
338 Name: "example-device",
339 },
340 Applications: []api.ApplicationSubject{
341 {
342 APIGroup: api.GroupVersion.Group,
343 Kind: "ExternalApplication",
344 Name: "example-app",
345 ID: "app-id",
346 },
347 },
348 },
349 Status: api.DeviceBindingStatus{
350 ActivationCode: "F7RAXZXBFZQRPAU",
351 Timestamp: metav1.Time{
352 Time: time.Now(),
353 },
354 CodeUsed: false,
355 },
356 }
357 err := mockClient.Create(ctx, deviceBinding)
358 assert.NoError(t, err)
359 }
360
View as plain text