package services import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" api "edge-infra.dev/pkg/edge/device-registrar/api/v1alpha1" "edge-infra.dev/pkg/edge/device-registrar/config" testClient "edge-infra.dev/pkg/edge/device-registrar/utils/test" "edge-infra.dev/pkg/edge/iam/api/v1alpha1" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" ) func setupTest() (*gin.Engine, *runtime.Scheme) { gin.SetMode(gin.TestMode) router := gin.Default() router.Use(gin.Logger()) // Add CRDs to scheme scheme := runtime.NewScheme() err := corev1.AddToScheme(scheme) if err != nil { panic(err) } config.AddToScheme(scheme) return router, scheme } func GenerateMockConnectClient(scheme *runtime.Scheme, test string) client.WithWatch { deviceSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example-device-example-application-secret", Namespace: "device-registrar-clients", }, Data: map[string][]byte{ "client_id": []byte("example-client-id"), "client_secret": []byte("example-client-secret"), }, } ienNode := &v1ien.IENode{ ObjectMeta: metav1.ObjectMeta{ Name: "mock-ienode", Namespace: "device-registrar-clients", }, Spec: v1ien.IENodeSpec{ Role: "worker", Lane: "1", Network: []v1ien.Network{ { MacAddress: "90:74:c2:0f:d8:77", Addresses: []string{"192.168.1.2/24"}, Gateway4: "192.168.1.1", DHCP4: false, DHCP6: false, }, }, PrimaryInterface: &v1ien.PrimaryInterface{ InterfaceID: "9647e74f-aba3-447f-b327-31ecce7f8d4c", MacAddresses: []string{"90:74:c2:0f:d8:77"}, }, }, } externalApplication := &api.ExternalApplication{ ObjectMeta: metav1.ObjectMeta{ Name: "example-application", Namespace: "device-registrar-clients", }, Spec: api.ExternalApplicationSpec{ ID: "app-id", Description: "Example external application", ClientTemplate: v1alpha1.Client{ ObjectMeta: metav1.ObjectMeta{ Name: "example-client", Namespace: "device-registrar-clients", }, Spec: v1alpha1.ClientSpec{ SecretName: "example-secret", }, }, }, } edgeInfo := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "edge-info", Namespace: "kube-public", }, Data: map[string]string{ "cluster.edge.id": "mock-cluster-edge-id", }, } clientCertSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example-device-cert", Namespace: "emissary", }, Data: map[string][]byte{ "tls.crt": []byte("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM5RENDQXBxZ0F3SUJBZ0lRT1Z4M3VNaElrdjBxMWdRSFRtdmswREFLQmdncWhrak9QUVFEQWpBZU1Sd3cKR2dZRFZRUURFeE5rWlhacFkyVXRjbVZuYVhOMGNtRnlMV05oTUI0WERUSTBNVEV5TVRFek1EWTBOVm9YRFRJMQpNVEV5TVRFek1EWTBOVm93WGpGY01CWUdBMVVFQ3hNUGRYSnVPbk4wYjNKbFNVUTZNVEl6TUJvR0ExVUVDeE1UCmRYSnVPbE5PT2tSVFJrVTNSRk5EV0ZnNU1EQW1CZ05WQkFzVEgzVnlianBrWlhacFkyVk9ZVzFsT25SbGMzUXQKWkdWMmFXTmxMVzVoYldVd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNwYi9aMwpXTjRIUEZlQ1dkUmthcVVvajFxUU5QRkJSblVDTmsyZUMzQ3FCOGlIMjV3eVdFSWk1eFl4NUxadDQ4dWZmREwyClNvWmszMlgzUFBNcUdIQmdFbDZ4TzFSRStHaUNkZ3Flc3JtZzJsdlEzdENaanhEZjh2WnpGcTE3dUpQZWNtckkKenJia1M5bDI4emRZaFZKazU1K0h5NlNDTUJBQ0JmbUZvd1NCaHEzb1NQWUJwdHZnUjJJTkhleFRBT2YwbVU5dgozT1hCenBla01sT0VKVmFnS3dPaitOdnl6OFF1ekRZRUlhVVpiYmhsQzROUXRvM2t2Z0R0dy9XZ01hVjVmQ2NWCjZWSEFIMWRFb3RoU1U2Snd4NUtGREFQOU1PTmoxVDJwL3p0QkpuUTRwU2xUWDd1WExiTEZpMWlsZU1qSnVxQncKSmhNY29nMHBqVWVJbnZLMUFnTUJBQUdqZ2E0d2dhc3dFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUhBd0l3REFZRApWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JSMXU5NTFSSHhCNmdxaEFuMTFwbC9MdW5rc29EQmxCZ05WCkhSRUVYakJjZ2hObFpHZGxMbk4wYjNKbExtNWpjaTVqYjNKd2hoOTFjbTQ2WkdWMmFXTmxUbUZ0WlRwMFpYTjAKTFdSbGRtbGpaUzF1WVcxbGhnOTFjbTQ2YzNSdmNtVkpSRG94TWpPR0UzVnlianBUVGpwRVUwWkZOMFJUUTFoWQpPVEF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnVWF3akRaT0Q5RS9JSUY3STBVak1BSHhldGVyaFFXcnYrb1R0CnpuQ2ZYbnNDSVFDMjNkcUdic0FhVjBLdlJvN1NhdGVEWXlHU052T09JTkhFMk9xYUtPcStiZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"), "tls.key": []byte("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcVcvMmQxamVCenhYZ2xuVVpHcWxLSTlha0RUeFFVWjFBalpObmd0d3FnZkloOXVjCk1saENJdWNXTWVTMmJlUExuM3d5OWtxR1pOOWw5enp6S2hod1lCSmVzVHRVUlBob2duWUtucks1b05wYjBON1EKbVk4UTMvTDJjeGF0ZTdpVDNuSnF5TTYyNUV2WmR2TTNXSVZTWk9lZmg4dWtnakFRQWdYNWhhTUVnWWF0NkVqMgpBYWJiNEVkaURSM3NVd0RuOUpsUGI5emx3YzZYcERKVGhDVldvQ3NEby9qYjhzL0VMc3cyQkNHbEdXMjRaUXVEClVMYU41TDRBN2NQMW9ER2xlWHduRmVsUndCOVhSS0xZVWxPaWNNZVNoUXdEL1REalk5VTlxZjg3UVNaME9LVXAKVTErN2x5Mnl4WXRZcFhqSXlicWdjQ1lUSEtJTktZMUhpSjd5dFFJREFRQUJBb0lCQUNjako0a1Z5K05iK3RLdgpNVElvdUJHUVcxam9BYm9VTGo3ZWtEc3JaVkRCRDM3aUtVZ3Z2c2NWSGJsVW5SYnhBVDNMa0hIM2NPZW4rb09MClhwZWdvWVJ2ZWRQeVlscTBEbC9rS0R2VUNMQ3cvM2hWbjFWNUNHclNVajd2UkE5SjZVMyttOC9hZjhCb0RNay8KRU0rdnJBS0d4Z0MxOXovakhpOTVkZE42ZmpYMHA2K0dCd2RObUFoK01lOVBJWlNXeFJWbUpDR3ZOTlJER0Y1SQorNkdVZGFNQU94UGNsK01Nc2hmWkNTNGNGTEVwUHozblpKTlJiZUxiTUhXeks5TFpBZzFXemVBVStwaEZabjRaCitiOFVFNVd3eGtBaWhOZnBCdlRTYTRxL0RmTCtlU0V4YWNoSXQvNUlPK1lHTXVpcTJGSGNaZmNLWkJZdWVEeXMKb2hmd1RrRUNnWUVBeG9HS29DR0dVQTdDNkRUUjkreE9TRzQyZHM1VGVWZ3ZxRlJTbDg5aEgyTk5sdENITlNtSgpQR3V2V2hmN21XK2piZjRwdlhoWWl1V3hjTWNpWmNlQTJaYkttMjVCOEJ0MHE4VjA0YXQ0ZEltQ2p4bktOWndmCjk2WDlQS0pibE1acDRaNDVqMmxQVHMyS3RKTW5oOVcvYWx6S3JTR3IvOEFvQnZwZGtvQ253SGtDZ1lFQTJvTVgKWVVCRjVhWU5XTmZGZG5PL1J6WmdQL1h2TDRxcWw4SERvT0FvcGFSMjZLVzQwVTZHWnF6RllLZTBmYkRLMjlvQQplYXBuSDVJNWw0cjgyeUpnQTQwS2xDbkExb3Vhd3JVTTQxSk53Zkpzd1dINkRZU1U0VnhZVWJXMlBhRkF2Mk92CjFuNjBRVDNZaDhmekxBeUtYNGVjWVBldTR3VUpkUGRtSFRHWURSMENnWUFCamNnSkF0b3JURUpJVVFtSHVFalEKbGxSRXo4NmxkNFEvL0JEOWNUa2dac1dYdGFBcFVWN3FveWtuT21MVXk2UHEyMzkySlRnRU5sSVNRT3pMQVNuSQpDajhod2xZdnkvYzQxUDNhT2w1aUF5V0xlemN5L2pyZDFHWE1FTFZJejlqS1ZGTzlCS1VEUithYkRUL1U5MTVkCk5jYThYalFiZDJTWTBXTGtINit3ZVFLQmdRQ3dQcVFROE1KdjVHdEhpV0hmbEtSblQ5aDZQbWRadFVLN2ZMSEoKaElQRWRzN2gveWoreVpObUpWeGVCV1p6S3JHMGVqVi83STJZelZ4ZWV1QlA3MzM1M3p6MUhHaEpvL2lEcTN4bApyZkRCeWtNbUIxeWtvcGRpM2hUdWN0NDIvMlUxK2JYT0VBeGJ3d0p2SWp0bEFBaHIzUG1vekozbXhoMUdsblZxCmZxSGhrUUtCZ0dYK2N0Z2kzQW5YcGhZdk8rZ0s2Snc2TmhnWTdhaW0zcGJLd3Awd0hwODNGa0hTWHFiMmxmc0kKaTNhTXh1cEZwdXE4S2FmbkUvTERTOHE3dVcwK0ozR0w2WENFZmNqaHFkUUZjQ0RlSlVhL21ZOVhOZi9aT01OMwpVS1UzR0dhSGF2eXJBVllMVWdpVG5NazcycWx4aEJQR25VbUNIWElHSkZLY1N6ZFFYOHdJCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="), }, } var mockClient client.WithWatch if test == "noSecret" { mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, ienNode, externalApplication) } else if test == "noexternalapp" { mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, deviceSecret, ienNode) } else { mockClient = testClient.NewErrorInjectingFakeClient(scheme, true, deviceSecret, ienNode, externalApplication, edgeInfo, clientCertSecret) } return mockClient } func TestConnectDevice(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "") injectmockClient(router, mockClient, t) // Create a test context ctx := context.TODO() createDeviceBinding(ctx, mockClient, t) c, w := setupCall(mockClient, t, "") // Call the ConnectDevice function ConnectDevice(c) // Assert the response assert.Equal(t, http.StatusOK, w.Code) var resp DeviceConnectResponse err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, "example-client-id", resp.Applications[0].Config.ClientID) assert.Equal(t, "example-client-secret", resp.Applications[0].Config.ClientSecret) assert.Equal(t, "store.ncr.corp", resp.HostMapping.Host) assert.Equal(t, "", resp.HostMapping.VIP) } func TestConnectNoSecret(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "noSecret") injectmockClient(router, mockClient, t) // Create a test context ctx := context.TODO() createDeviceBinding(ctx, mockClient, t) c, w := setupCall(mockClient, t, "") // Call the ConnectDevice function ConnectDevice(c) // Assert the response assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestConnectNoExternalApp(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "noexternalapp") injectmockClient(router, mockClient, t) // Create a test context ctx := context.TODO() createDeviceBinding(ctx, mockClient, t) c, w := setupCall(mockClient, t, "") // Call the ConnectDevice function ConnectDevice(c) // Assert the response assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestBadConnectActivation(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "") injectmockClient(router, mockClient, t) config.AddToScheme(scheme) c, w := setupCall(mockClient, t, "badactivation") // Call the ConnectDevice function ConnectDevice(c) // Check the status code assert.Equal(t, http.StatusUnauthorized, w.Code) // Check the response body assert.JSONEq(t, `{"error":"invalid activation code"}`, w.Body.String()) } func TestBadConnectRequest(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "") injectmockClient(router, mockClient, t) config.AddToScheme(scheme) c, w := setupCall(mockClient, t, "badRequest") // Call the ConnectDevice function ConnectDevice(c) // Check the status code assert.Equal(t, http.StatusBadRequest, w.Code) // Check the response body assert.JSONEq(t, `{"error":"Device Name is required"}`, w.Body.String()) } func TestConnectNoActivation(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "") injectmockClient(router, mockClient, t) config.AddToScheme(scheme) c, w := setupCall(mockClient, t, "noactivation") // Call the ConnectDevice function ConnectDevice(c) // Check the status code assert.Equal(t, http.StatusBadRequest, w.Code) // Check the response body assert.JSONEq(t, `{"error":"activationCode is required"}`, w.Body.String()) } func TestConnectNoSN(t *testing.T) { router, scheme := setupTest() mockClient := GenerateMockConnectClient(scheme, "") injectmockClient(router, mockClient, t) // Create a test context ctx := context.TODO() createDeviceBinding(ctx, mockClient, t) c, w := setupCall(mockClient, t, "noSN") // Call the ConnectDevice function ConnectDevice(c) // Check the status code assert.Equal(t, http.StatusBadRequest, w.Code) // Check the response body assert.JSONEq(t, `{"error":"Serial Number is required"}`, w.Body.String()) } func setupCall(mockClient client.WithWatch, t *testing.T, test string) (*gin.Context, *httptest.ResponseRecorder) { var deviceConnectRequest DeviceConnectRequest if test == "badRequest" { deviceConnectRequest = DeviceConnectRequest{ Device: api.ExternalDeviceSpec{}, ApplicationIDs: []string{"app-id"}, } } else if test == "noSN" { deviceConnectRequest = DeviceConnectRequest{ Device: api.ExternalDeviceSpec{ Name: "example-device", SN: "", }, ApplicationIDs: []string{"app-id"}, } } else { deviceConnectRequest = DeviceConnectRequest{ Device: api.ExternalDeviceSpec{ Name: "example-device", SN: "example-sn", }, ApplicationIDs: []string{"app-id"}, } } requestBody, err := json.Marshal(deviceConnectRequest) assert.NoError(t, err) // Create a new HTTP request var req *http.Request if test == "noactivation" { req, err = http.NewRequest(http.MethodPost, "/connect/", bytes.NewBuffer(requestBody)) } else if test == "badactivation" { req, err = http.NewRequest(http.MethodPost, "/connect/WRONGCODE", bytes.NewBuffer(requestBody)) } else { req, err = http.NewRequest(http.MethodPost, "/connect/F7RAXZXBFZQRPAU", bytes.NewBuffer(requestBody)) } assert.NoError(t, err) req.Header.Set("Content-Type", "application/json") // Create a response recorder w := httptest.NewRecorder() // Create a Gin context c, _ := gin.CreateTestContext(w) c.Request = req if test == "noactivation" { c.Params = gin.Params{gin.Param{Key: "activationCode", Value: ""}} } else if test == "badactivation" { c.Params = gin.Params{gin.Param{Key: "activationCode", Value: "WRONGCODE"}} } else { c.Params = gin.Params{gin.Param{Key: "activationCode", Value: "F7RAXZXBFZQRPAU"}} } // Set the mock client in the context c.Set("k8sClient", mockClient) return c, w } func createDeviceBinding(ctx context.Context, mockClient client.WithWatch, t *testing.T) { // Create a test device binding deviceBinding := &api.DeviceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "example-device-binding", Namespace: "device-registrar-clients", }, Spec: api.DeviceBindingSpec{ Device: api.DeviceSubject{ APIGroup: api.GroupVersion.Group, Kind: "ExternalDevice", Name: "example-device", }, Applications: []api.ApplicationSubject{ { APIGroup: api.GroupVersion.Group, Kind: "ExternalApplication", Name: "example-app", ID: "app-id", }, }, }, Status: api.DeviceBindingStatus{ ActivationCode: "F7RAXZXBFZQRPAU", Timestamp: metav1.Time{ Time: time.Now(), }, CodeUsed: false, }, } err := mockClient.Create(ctx, deviceBinding) assert.NoError(t, err) }