...

Source file src/edge-infra.dev/pkg/edge/device-registrar/services/device_connect_service_test.go

Documentation: edge-infra.dev/pkg/edge/device-registrar/services

     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  	// Add CRDs to scheme
    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  	// Create a test context
   137  	ctx := context.TODO()
   138  	createDeviceBinding(ctx, mockClient, t)
   139  
   140  	c, w := setupCall(mockClient, t, "")
   141  
   142  	// Call the ConnectDevice function
   143  	ConnectDevice(c)
   144  
   145  	// Assert the response
   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  	// Create a test context
   162  	ctx := context.TODO()
   163  	createDeviceBinding(ctx, mockClient, t)
   164  
   165  	c, w := setupCall(mockClient, t, "")
   166  
   167  	// Call the ConnectDevice function
   168  	ConnectDevice(c)
   169  
   170  	// Assert the response
   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  	// Create a test context
   180  	ctx := context.TODO()
   181  	createDeviceBinding(ctx, mockClient, t)
   182  
   183  	c, w := setupCall(mockClient, t, "")
   184  
   185  	// Call the ConnectDevice function
   186  	ConnectDevice(c)
   187  
   188  	// Assert the response
   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  	// Call the ConnectDevice function
   201  	ConnectDevice(c)
   202  
   203  	// Check the status code
   204  	assert.Equal(t, http.StatusUnauthorized, w.Code)
   205  
   206  	// Check the response body
   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  	// Call the ConnectDevice function
   219  	ConnectDevice(c)
   220  
   221  	// Check the status code
   222  	assert.Equal(t, http.StatusBadRequest, w.Code)
   223  
   224  	// Check the response body
   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  	// Call the ConnectDevice function
   237  	ConnectDevice(c)
   238  
   239  	// Check the status code
   240  	assert.Equal(t, http.StatusBadRequest, w.Code)
   241  
   242  	// Check the response body
   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  	// Create a test context
   252  	ctx := context.TODO()
   253  	createDeviceBinding(ctx, mockClient, t)
   254  
   255  	c, w := setupCall(mockClient, t, "noSN")
   256  
   257  	// Call the ConnectDevice function
   258  	ConnectDevice(c)
   259  
   260  	// Check the status code
   261  	assert.Equal(t, http.StatusBadRequest, w.Code)
   262  
   263  	// Check the response body
   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  	// Create a new HTTP request
   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  	// Create a response recorder
   308  	w := httptest.NewRecorder()
   309  
   310  	// Create a Gin context
   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  	// Set the mock client in the context
   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  	// Create a test device binding
   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