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
40
41 os.Setenv("KUBERNETES_SERVICE_HOST", "1")
42 os.Setenv("KUBERNETES_SERVICE_PORT", "1")
43
44
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
59
60 func CreateScheme() *kruntime.Scheme {
61 scheme := kruntime.NewScheme()
62 utilruntime.Must(clientgoscheme.AddToScheme(scheme))
63 return scheme
64 }
65
66
67
68 func GetFakeKubeClient(initObjs ...client.Object) client.Client {
69 return fake.NewClientBuilder().WithScheme(CreateScheme()).WithObjects(initObjs...).Build()
70 }
71
72
73
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
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