package host_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/gin-gonic/gin" "github.com/go-logr/logr/testr" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache/informertest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "edge-infra.dev/pkg/sds/interlock/internal/config" "edge-infra.dev/pkg/sds/interlock/internal/errors" "edge-infra.dev/pkg/sds/interlock/topic/host" "edge-infra.dev/pkg/sds/interlock/websocket" "edge-infra.dev/pkg/sds/lib/k8s/retryclient" ) var testHostname = "test-node" func init() { gin.SetMode(gin.TestMode) // required to prevent errors around getting InCluster configuration for k8s // client os.Setenv("KUBERNETES_SERVICE_HOST", "1") os.Setenv("KUBERNETES_SERVICE_PORT", "1") // required to prevent hostname required error os.Setenv("NODE_NAME", testHostname) } func SetupTestCtx(t *testing.T) context.Context { logOptions := testr.Options{ LogTimestamp: true, Verbosity: -1, } ctx := ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions)) return ctx } // CreateScheme creates a new scheme, adds all types of the automatically generated // clientset, and returns it. func CreateScheme() *kruntime.Scheme { scheme := kruntime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) return scheme } // GetFakeKubeClient returns a fake client initialised with a slice of // Kubernets objects. To be used for testing purposes. func GetFakeKubeClient(initObjs ...client.Object) client.Client { return fake.NewClientBuilder().WithScheme(CreateScheme()).WithObjects(initObjs...).Build() } // GetTestHostNode returns a Node object initialised with name and uid fields // To be used for testing purposes. func GetTestHostNode(name, uid string) *v1.Node { return &v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: name, UID: types.UID(uid), }, } } // setupTestHostTopic returns an initialised host topic func setupTestHostTopic(t *testing.T, hostname, uid string) (*host.Host, error) { t.Setenv("NODE_NAME", hostname) node := GetTestHostNode(hostname, uid) cli := GetFakeKubeClient(node) cfg := &config.Config{ Fs: afero.NewMemMapFs(), KubeRetryClient: retryclient.New(cli, cli, retryclient.Config{}), Cache: &informertest.FakeInformers{}, } wm := websocket.NewManager() return host.New(SetupTestCtx(t), cfg, wm) } func TestGetState(t *testing.T) { h, err := setupTestHostTopic(t, testHostname, "uid") require.NoError(t, err) r := gin.Default() h.RegisterEndpoints(r) w := httptest.NewRecorder() req, err := http.NewRequest("GET", host.Path, nil) require.NoError(t, err) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) actual := &host.State{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), actual)) expected := &host.State{ Hostname: testHostname, Network: host.Network{ LANOutageDetected: false, LANOutageMode: false, }, NodeUID: "uid", VNC: host.VNCStates{}, Kpower: host.KpowerState{ Status: host.UNKNOWN, }, } assert.Equal(t, expected, actual) } type HostnameEditor struct { Hostname string `json:"hostname"` } type LANOutageDetectedEditor struct { Network Network `json:"network"` } type Network struct { LANOutageDetected bool `json:"lan-outage-detected"` } type Errors struct { Errors []*errors.Error `json:"errors"` } func TestPatchState(t *testing.T) { h, err := setupTestHostTopic(t, testHostname, "uid") require.NoError(t, err) r := gin.Default() h.RegisterEndpoints(r) testCases := map[string]struct { input interface{} expectedStatus int response interface{} expectedResponse interface{} }{ "Success": { input: LANOutageDetectedEditor{ Network: Network{ LANOutageDetected: true, }, }, expectedStatus: http.StatusAccepted, response: &host.State{}, expectedResponse: &host.State{ Hostname: testHostname, Network: host.Network{ LANOutageDetected: true, LANOutageMode: false, }, NodeUID: "uid", VNC: host.VNCStates{}, Kpower: host.KpowerState{ Status: host.UNKNOWN, }, }, }, "Faillure_ReadOnlyHostname": { input: HostnameEditor{ Hostname: "any-name", }, expectedStatus: http.StatusBadRequest, response: &Errors{}, expectedResponse: &Errors{ Errors: []*errors.Error{ { Detail: errors.NewReadOnlyFieldMessage("Hostname"), }, }, }, }, "Failure update VNC state via patch": { input: struct { VNC struct { RequestID string `json:"requestId"` Status host.VNCStatus `json:"status"` } `json:"vnc"` }{ VNC: struct { RequestID string `json:"requestId"` Status host.VNCStatus `json:"status"` }{ RequestID: "123", Status: host.Requested, }, }, expectedStatus: http.StatusBadRequest, response: &Errors{}, expectedResponse: &Errors{ Errors: []*errors.Error{ { Detail: errors.NewMessage("vnc", "Updating VNC state is not supported on this endpoint"), }, }, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { out, err := json.Marshal(tc.input) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", host.Path, bytes.NewBuffer(out)) r.ServeHTTP(w, req) assert.Equal(t, tc.expectedStatus, w.Code) require.NoError(t, json.Unmarshal(w.Body.Bytes(), tc.response)) assert.Equal(t, tc.expectedResponse, tc.response) }) } }