...

Source file src/edge-infra.dev/pkg/sds/interlock/topic/host/host_int_test.go

Documentation: edge-infra.dev/pkg/sds/interlock/topic/host

     1  package host_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"testing"
    11  
    12  	"github.com/gin-gonic/gin"
    13  	"github.com/go-logr/logr/testr"
    14  	"github.com/spf13/afero"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	v1 "k8s.io/api/core/v1"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	kruntime "k8s.io/apimachinery/pkg/runtime"
    20  	"k8s.io/apimachinery/pkg/types"
    21  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    22  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    23  	ctrl "sigs.k8s.io/controller-runtime"
    24  	"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
    25  	"sigs.k8s.io/controller-runtime/pkg/client"
    26  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    27  
    28  	"edge-infra.dev/pkg/sds/interlock/internal/config"
    29  	"edge-infra.dev/pkg/sds/interlock/internal/errors"
    30  	"edge-infra.dev/pkg/sds/interlock/topic/host"
    31  	"edge-infra.dev/pkg/sds/interlock/websocket"
    32  	"edge-infra.dev/pkg/sds/lib/k8s/retryclient"
    33  )
    34  
    35  var testHostname = "test-node"
    36  
    37  func init() {
    38  	gin.SetMode(gin.TestMode)
    39  	// required to prevent errors around getting InCluster configuration for k8s
    40  	// client
    41  	os.Setenv("KUBERNETES_SERVICE_HOST", "1")
    42  	os.Setenv("KUBERNETES_SERVICE_PORT", "1")
    43  
    44  	// required to prevent hostname required error
    45  	os.Setenv("NODE_NAME", testHostname)
    46  }
    47  
    48  func SetupTestCtx(t *testing.T) context.Context {
    49  	logOptions := testr.Options{
    50  		LogTimestamp: true,
    51  		Verbosity:    -1,
    52  	}
    53  
    54  	ctx := ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions))
    55  	return ctx
    56  }
    57  
    58  // CreateScheme creates a new scheme, adds all types of the automatically generated
    59  // clientset, and returns it.
    60  func CreateScheme() *kruntime.Scheme {
    61  	scheme := kruntime.NewScheme()
    62  	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
    63  	return scheme
    64  }
    65  
    66  // GetFakeKubeClient returns a fake client initialised with a slice of
    67  // Kubernets objects. To be used for testing purposes.
    68  func GetFakeKubeClient(initObjs ...client.Object) client.Client {
    69  	return fake.NewClientBuilder().WithScheme(CreateScheme()).WithObjects(initObjs...).Build()
    70  }
    71  
    72  // GetTestHostNode returns a Node object initialised with name and uid fields
    73  // To be used for testing purposes.
    74  func GetTestHostNode(name, uid string) *v1.Node {
    75  	return &v1.Node{
    76  		ObjectMeta: metav1.ObjectMeta{
    77  			Name: name,
    78  			UID:  types.UID(uid),
    79  		},
    80  	}
    81  }
    82  
    83  // setupTestHostTopic returns an initialised host topic
    84  func setupTestHostTopic(t *testing.T, hostname, uid string) (*host.Host, error) {
    85  	t.Setenv("NODE_NAME", hostname)
    86  
    87  	node := GetTestHostNode(hostname, uid)
    88  	cli := GetFakeKubeClient(node)
    89  	cfg := &config.Config{
    90  		Fs:              afero.NewMemMapFs(),
    91  		KubeRetryClient: retryclient.New(cli, cli, retryclient.Config{}),
    92  		Cache:           &informertest.FakeInformers{},
    93  	}
    94  
    95  	wm := websocket.NewManager()
    96  	return host.New(SetupTestCtx(t), cfg, wm)
    97  }
    98  
    99  func TestGetState(t *testing.T) {
   100  	h, err := setupTestHostTopic(t, testHostname, "uid")
   101  	require.NoError(t, err)
   102  
   103  	r := gin.Default()
   104  	h.RegisterEndpoints(r)
   105  
   106  	w := httptest.NewRecorder()
   107  	req, err := http.NewRequest("GET", host.Path, nil)
   108  	require.NoError(t, err)
   109  	r.ServeHTTP(w, req)
   110  
   111  	assert.Equal(t, http.StatusOK, w.Code)
   112  
   113  	actual := &host.State{}
   114  	require.NoError(t, json.Unmarshal(w.Body.Bytes(), actual))
   115  
   116  	expected := &host.State{
   117  		Hostname: testHostname,
   118  		Network: host.Network{
   119  			LANOutageDetected: false,
   120  			LANOutageMode:     false,
   121  		},
   122  		NodeUID: "uid",
   123  		VNC:     host.VNCStates{},
   124  		Kpower: host.KpowerState{
   125  			Status: host.UNKNOWN,
   126  		},
   127  	}
   128  	assert.Equal(t, expected, actual)
   129  }
   130  
   131  type HostnameEditor struct {
   132  	Hostname string `json:"hostname"`
   133  }
   134  
   135  type LANOutageDetectedEditor struct {
   136  	Network Network `json:"network"`
   137  }
   138  
   139  type Network struct {
   140  	LANOutageDetected bool `json:"lan-outage-detected"`
   141  }
   142  
   143  type Errors struct {
   144  	Errors []*errors.Error `json:"errors"`
   145  }
   146  
   147  func TestPatchState(t *testing.T) {
   148  	h, err := setupTestHostTopic(t, testHostname, "uid")
   149  	require.NoError(t, err)
   150  
   151  	r := gin.Default()
   152  	h.RegisterEndpoints(r)
   153  
   154  	testCases := map[string]struct {
   155  		input            interface{}
   156  		expectedStatus   int
   157  		response         interface{}
   158  		expectedResponse interface{}
   159  	}{
   160  		"Success": {
   161  			input: LANOutageDetectedEditor{
   162  				Network: Network{
   163  					LANOutageDetected: true,
   164  				},
   165  			},
   166  			expectedStatus: http.StatusAccepted,
   167  			response:       &host.State{},
   168  			expectedResponse: &host.State{
   169  				Hostname: testHostname,
   170  				Network: host.Network{
   171  					LANOutageDetected: true,
   172  					LANOutageMode:     false,
   173  				},
   174  				NodeUID: "uid",
   175  				VNC:     host.VNCStates{},
   176  				Kpower: host.KpowerState{
   177  					Status: host.UNKNOWN,
   178  				},
   179  			},
   180  		},
   181  		"Faillure_ReadOnlyHostname": {
   182  			input: HostnameEditor{
   183  				Hostname: "any-name",
   184  			},
   185  			expectedStatus: http.StatusBadRequest,
   186  			response:       &Errors{},
   187  			expectedResponse: &Errors{
   188  				Errors: []*errors.Error{
   189  					{
   190  						Detail: errors.NewReadOnlyFieldMessage("Hostname"),
   191  					},
   192  				},
   193  			},
   194  		},
   195  		"Failure update VNC state via patch": {
   196  			input: struct {
   197  				VNC struct {
   198  					RequestID string         `json:"requestId"`
   199  					Status    host.VNCStatus `json:"status"`
   200  				} `json:"vnc"`
   201  			}{
   202  				VNC: struct {
   203  					RequestID string         `json:"requestId"`
   204  					Status    host.VNCStatus `json:"status"`
   205  				}{
   206  					RequestID: "123",
   207  					Status:    host.Requested,
   208  				},
   209  			},
   210  			expectedStatus: http.StatusBadRequest,
   211  			response:       &Errors{},
   212  			expectedResponse: &Errors{
   213  				Errors: []*errors.Error{
   214  					{
   215  						Detail: errors.NewMessage("vnc", "Updating VNC state is not supported on this endpoint"),
   216  					},
   217  				},
   218  			},
   219  		},
   220  	}
   221  
   222  	for name, tc := range testCases {
   223  		t.Run(name, func(t *testing.T) {
   224  			out, err := json.Marshal(tc.input)
   225  			require.NoError(t, err)
   226  
   227  			w := httptest.NewRecorder()
   228  			req, _ := http.NewRequest("PATCH", host.Path, bytes.NewBuffer(out))
   229  			r.ServeHTTP(w, req)
   230  
   231  			assert.Equal(t, tc.expectedStatus, w.Code)
   232  
   233  			require.NoError(t, json.Unmarshal(w.Body.Bytes(), tc.response))
   234  			assert.Equal(t, tc.expectedResponse, tc.response)
   235  		})
   236  	}
   237  }
   238  

View as plain text