1 package server
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "github.com/gin-gonic/gin"
13 "github.com/go-logr/logr"
14 "github.com/stretchr/testify/assert"
15
16 rulesengine "edge-infra.dev/pkg/sds/emergencyaccess/rules"
17 )
18
19 type postNamesMock struct {
20 RulesEngine
21
22 dataRet rulesengine.AddNameResult
23 errRet error
24
25 callCount int
26 names []string
27 }
28
29 func (pnm *postNamesMock) AddCommands(_ context.Context, commands []rulesengine.PostCommandPayload) (rulesengine.AddNameResult, error) {
30 pnm.callCount = pnm.callCount + 1
31 for _, command := range commands {
32 pnm.names = append(pnm.names, command.Name)
33 }
34 return pnm.dataRet, pnm.errRet
35 }
36
37 func (pnm *postNamesMock) AddPrivileges(_ context.Context, privs []rulesengine.PostPrivilegePayload) (rulesengine.AddNameResult, error) {
38 pnm.callCount = pnm.callCount + 1
39 for _, priv := range privs {
40 pnm.names = append(pnm.names, priv.Name)
41 }
42 return pnm.dataRet, pnm.errRet
43 }
44
45 func TestPostNames(t *testing.T) {
46 t.Parallel()
47
48 tests := map[string]struct {
49 url string
50 reqBody string
51
52 mockDataRet rulesengine.AddNameResult
53 mockErrRet error
54
55 expMockCalledCount int
56 expNames []string
57 expCode int
58
59 jsonAssert StringAssertionFunc
60 }{
61 "Post Commands Ok": {
62 url: "/admin/commands",
63 reqBody: `[
64 {"name": "ls"},
65 {"name": "cat"}
66 ]`,
67
68 expMockCalledCount: 1,
69 expNames: []string{"ls", "cat"},
70 expCode: http.StatusOK,
71
72 jsonAssert: JSONEmpty(),
73 },
74 "Post Commands Invalid JSON": {
75 url: "/admin/commands",
76 reqBody: `[{"nam`,
77
78 expMockCalledCount: 0,
79 expCode: http.StatusBadRequest,
80
81 jsonAssert: JSONEmpty(),
82 },
83 "Post Commands Invalid Payload": {
84 url: "/admin/commands",
85 reqBody: `[
86 {"name": ""},
87 ]`,
88
89 expMockCalledCount: 0,
90 expCode: http.StatusBadRequest,
91
92 jsonAssert: JSONEmpty(),
93 },
94 "Post Commands Rules Engine Error": {
95 url: "/admin/commands",
96 reqBody: `[
97 {"name": "ls"},
98 {"name": "cat"}
99 ]`,
100
101 mockDataRet: rulesengine.AddNameResult{},
102 mockErrRet: fmt.Errorf("an error occurred"),
103
104 expMockCalledCount: 1,
105 expNames: []string{"ls", "cat"},
106 expCode: http.StatusInternalServerError,
107
108 jsonAssert: JSONEmpty(),
109 },
110 "Post Commands Rules Engine Conflict": {
111 url: "/admin/commands",
112 reqBody: `[
113 {"name": "ls"},
114 {"name": "cat"}
115 ]`,
116
117 mockDataRet: rulesengine.AddNameResult{Conflicts: []string{"ls", "cat"}},
118
119 expMockCalledCount: 1,
120 expNames: []string{"ls", "cat"},
121 expCode: http.StatusConflict,
122
123 jsonAssert: JSONEq(`{
124 "conflicts": [
125 "ls",
126 "cat"
127 ]
128 }`),
129 },
130 "Post Privilege Ok": {
131 url: "/admin/privileges",
132 reqBody: `[
133 {"name": "basic"},
134 {"name": "admin"}
135 ]`,
136
137 expMockCalledCount: 1,
138 expNames: []string{"basic", "admin"},
139 expCode: http.StatusOK,
140
141 jsonAssert: JSONEmpty(),
142 },
143 "Post Privilege Invalid JSON": {
144 url: "/admin/privileges",
145 reqBody: `[{"nam`,
146
147 expMockCalledCount: 0,
148 expCode: http.StatusBadRequest,
149
150 jsonAssert: JSONEmpty(),
151 },
152 "Post Privilege Invalid Payload": {
153 url: "/admin/privileges",
154 reqBody: `[
155 {"name": ""},
156 ]`,
157
158 expMockCalledCount: 0,
159 expCode: http.StatusBadRequest,
160
161 jsonAssert: JSONEmpty(),
162 },
163 "Post Privilege Rules Engine Error": {
164 url: "/admin/privileges",
165 reqBody: `[
166 {"name": "basic"},
167 {"name": "admin"}
168 ]`,
169
170 mockDataRet: rulesengine.AddNameResult{},
171 mockErrRet: fmt.Errorf("an error occurred"),
172
173 expMockCalledCount: 1,
174 expNames: []string{"basic", "admin"},
175 expCode: http.StatusInternalServerError,
176
177 jsonAssert: JSONEmpty(),
178 },
179 "Post Privilege Rules Engine Conflict": {
180 url: "/admin/privileges",
181 reqBody: `[
182 {"name": "basic"},
183 {"name": "admin"}
184 ]`,
185
186 mockDataRet: rulesengine.AddNameResult{Conflicts: []string{"basic", "admin"}},
187
188 expMockCalledCount: 1,
189 expNames: []string{"basic", "admin"},
190 expCode: http.StatusConflict,
191
192 jsonAssert: JSONEq(`{
193 "conflicts": [
194 "basic",
195 "admin"
196 ]
197 }`),
198 },
199 }
200
201 for name, tc := range tests {
202 tc := tc
203 t.Run(name, func(t *testing.T) {
204 t.Parallel()
205
206 ruleseng := postNamesMock{
207 dataRet: tc.mockDataRet,
208 errRet: tc.mockErrRet,
209 }
210
211 r := httptest.NewRecorder()
212 _, ginEngine := getTestGinContext(r)
213 _, err := New(ginEngine, &ruleseng, newLogger())
214 assert.NoError(t, err)
215
216 req, err := http.NewRequest(http.MethodPost, tc.url, strings.NewReader(tc.reqBody))
217 assert.NoError(t, err)
218
219 ginEngine.ServeHTTP(r, req)
220
221 assert.Equal(t, tc.expCode, r.Result().StatusCode)
222
223 assert.Equal(t, tc.expMockCalledCount, ruleseng.callCount)
224 assert.Equal(t, tc.expNames, ruleseng.names)
225
226 tc.jsonAssert(t, r.Body.String())
227 })
228 }
229 }
230
231 type deleteMock struct {
232 RulesEngine
233
234 command, privilege string
235
236 retDelete rulesengine.DeleteResult
237 retErr error
238 }
239
240 func (dm *deleteMock) DeleteCommand(_ context.Context, name string) (rulesengine.DeleteResult, error) {
241 dm.command = name
242 return dm.retDelete, dm.retErr
243 }
244
245 func (dm *deleteMock) DeletePrivilege(_ context.Context, name string) (rulesengine.DeleteResult, error) {
246 dm.privilege = name
247 return dm.retDelete, dm.retErr
248 }
249
250 func (dm *deleteMock) DeleteDefaultRule(_ context.Context, commandName string, privilegeName string) (rulesengine.DeleteResult, error) {
251 dm.command = commandName
252 dm.privilege = privilegeName
253 return dm.retDelete, dm.retErr
254 }
255
256 func TestDelete(t *testing.T) {
257 t.Parallel()
258
259 tests := map[string]struct {
260 url string
261 command string
262 privilege string
263
264 retRes rulesengine.DeleteResult
265 retErr error
266
267 expStatus int
268 expOut StringAssertionFunc
269 }{
270 "Delete Command: Success": {
271 command: "ls",
272 url: "/admin/commands/ls",
273
274 retRes: rulesengine.DeleteResult{RowsAffected: 1},
275 retErr: nil,
276
277 expStatus: http.StatusOK,
278 expOut: JSONEmpty(),
279 },
280 "Delete Command: No Change With Errors": {
281 command: "ls",
282 url: "/admin/commands/ls",
283
284 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
285 retErr: nil,
286
287 expStatus: http.StatusNotFound,
288 expOut: JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
289 },
290 "Delete Command: Rule With Conflicts": {
291 command: "ls",
292 url: "/admin/commands/ls",
293
294 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
295 retErr: nil,
296
297 expStatus: http.StatusConflict,
298 expOut: JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
299 },
300 "Delete Command: Application Error": {
301 command: "ls",
302 url: "/admin/commands/ls",
303
304 retRes: rulesengine.DeleteResult{RowsAffected: 1},
305 retErr: fmt.Errorf("an error"),
306
307 expStatus: http.StatusInternalServerError,
308 expOut: JSONEmpty(),
309 },
310 "Delete Privilege: Success": {
311 privilege: "basic",
312 url: "/admin/privileges/basic",
313
314 retRes: rulesengine.DeleteResult{RowsAffected: 1},
315 retErr: nil,
316
317 expStatus: http.StatusOK,
318 expOut: JSONEmpty(),
319 },
320 "Delete Privilege: No Change With Errors": {
321 privilege: "basic",
322 url: "/admin/privileges/basic",
323
324 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
325 retErr: nil,
326
327 expStatus: http.StatusNotFound,
328 expOut: JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
329 },
330 "Delete Privilege: With Conflicts": {
331 privilege: "basic",
332 url: "/admin/privileges/basic",
333
334 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
335 retErr: nil,
336
337 expStatus: http.StatusConflict,
338 expOut: JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
339 },
340 "Delete Privilege: Application Error": {
341 privilege: "basic",
342 url: "/admin/privileges/basic",
343
344 retRes: rulesengine.DeleteResult{RowsAffected: 1},
345 retErr: fmt.Errorf("an error"),
346
347 expStatus: http.StatusInternalServerError,
348 expOut: JSONEmpty(),
349 },
350 "Delete Rule: Success": {
351 command: "ls",
352 privilege: "basic",
353 url: "/admin/rules/default/commands/ls/privileges/basic",
354
355 retRes: rulesengine.DeleteResult{RowsAffected: 1},
356 retErr: nil,
357
358 expStatus: http.StatusOK,
359 expOut: JSONEmpty(),
360 },
361 "Delete Rule: No Change With Errors": {
362 command: "ls",
363 privilege: "basic",
364 url: "/admin/rules/default/commands/ls/privileges/basic",
365
366 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
367 retErr: nil,
368
369 expStatus: http.StatusNotFound,
370 expOut: JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
371 },
372 "Delete Rule: With Conflicts": {
373 command: "ls",
374 privilege: "basic",
375 url: "/admin/rules/default/commands/ls/privileges/basic",
376
377 retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
378 retErr: nil,
379
380 expStatus: http.StatusConflict,
381 expOut: JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
382 },
383 "Delete Rule: Application Error": {
384 command: "ls",
385 privilege: "basic",
386 url: "/admin/rules/default/commands/ls/privileges/basic",
387
388 retRes: rulesengine.DeleteResult{RowsAffected: 1},
389 retErr: fmt.Errorf("an error"),
390
391 expStatus: http.StatusInternalServerError,
392 expOut: JSONEmpty(),
393 },
394 "Delete rule: No command": {
395 command: "",
396 privilege: "",
397 url: "/admin/rules/default/commands//privileges/basic",
398
399 retErr: fmt.Errorf("an error"),
400
401 expStatus: http.StatusBadRequest,
402 expOut: JSONEmpty(),
403 },
404 "Delete rule: No Privilege Path": {
405 command: "",
406 privilege: "",
407 url: "/admin/rules/default/commands/ls/privileges",
408
409 retErr: fmt.Errorf("an error"),
410
411
412
413
414 expStatus: http.StatusNotFound,
415 expOut: StringEqual("404 page not found"),
416 },
417 "Delete rule: No Privilege": {
418 command: "",
419 privilege: "",
420 url: "/admin/rules/default/commands/ls/privileges/",
421
422 retErr: fmt.Errorf("an error"),
423
424
425
426
427 expStatus: http.StatusNotFound,
428 expOut: StringEqual("404 page not found"),
429 },
430 }
431
432 for name, tc := range tests {
433 tc := tc
434 t.Run(name, func(t *testing.T) {
435 t.Parallel()
436
437 log := newLogger()
438 mreng := deleteMock{
439 retDelete: tc.retRes,
440 retErr: tc.retErr,
441 }
442
443 r := httptest.NewRecorder()
444 _, ginEngine := getTestGinContext(r)
445 _, err := New(ginEngine, &mreng, log)
446 assert.NoError(t, err)
447
448 req, err := http.NewRequest(
449 http.MethodDelete,
450 tc.url,
451 nil,
452 )
453 assert.NoError(t, err)
454
455 ginEngine.ServeHTTP(r, req)
456
457 assert.Equal(t, tc.expStatus, r.Result().StatusCode)
458 assert.Equal(t, tc.command, mreng.command)
459 assert.Equal(t, tc.privilege, mreng.privilege)
460
461 tc.expOut(t, r.Body.String())
462 })
463 }
464 }
465
466 func TestReadNil(t *testing.T) {
467 tests := map[string]struct {
468 URL string
469 payload any
470 }{
471 "Read Rule": {
472 URL: "/admin/rules/default/commands/ls",
473 },
474 "Read Command": {
475 URL: "/admin/commands/ls",
476 },
477 "Read Priv": {
478 URL: "/admin/privileges/basic",
479 },
480 }
481 for name, tc := range tests {
482 t.Run(name, func(t *testing.T) {
483 log := newLogger()
484 t.Setenv("RCLI_RES_DATA_DIR", "./testdata")
485
486 ruleseng := MockRulesEngine{}
487
488 r := httptest.NewRecorder()
489 _, ginEngine := getTestGinContext(r)
490 _, err := New(ginEngine, &ruleseng, log)
491 assert.Nil(t, err)
492
493 req, err := http.NewRequest(http.MethodGet, tc.URL, nil)
494 assert.NoError(t, err)
495 ginEngine.ServeHTTP(r, req)
496 assert.Equal(t, http.StatusOK, r.Result().StatusCode)
497 assert.Equal(t, r.Body.String(), "null")
498 })
499 }
500 }
501
502 func TestHealth(t *testing.T) {
503 t.Parallel()
504
505 tests := map[string]struct {
506 checks []func() error
507
508 expCode int
509 expData string
510 }{
511 "No checks": {
512 checks: nil,
513 expCode: http.StatusOK,
514 expData: "ok",
515 },
516 "Passing check": {
517 checks: []func() error{func() error { return nil }},
518 expCode: http.StatusOK,
519 expData: "ok",
520 },
521 "Failing check": {
522 checks: []func() error{func() error { return fmt.Errorf("this is bad") }},
523 expCode: http.StatusServiceUnavailable,
524 expData: "failed health check: this is bad",
525 },
526 "Two checks": {
527 checks: []func() error{func() error { return nil }, func() error { return fmt.Errorf("this is bad") }},
528 expCode: http.StatusServiceUnavailable,
529 expData: "failed health check: this is bad",
530 },
531 }
532
533 for name, tc := range tests {
534 tc := tc
535 t.Run(name, func(t *testing.T) {
536 t.Parallel()
537
538 r := httptest.NewRecorder()
539 gin.SetMode(gin.TestMode)
540 _, ginEngine := gin.CreateTestContext(r)
541
542 _, err := New(ginEngine, nil, logr.Discard(), tc.checks...)
543 assert.NoError(t, err)
544
545 req, err := http.NewRequest(http.MethodGet, "/health", nil)
546 assert.NoError(t, err)
547
548 ginEngine.ServeHTTP(r, req)
549
550 assert.Equal(t, tc.expCode, r.Result().StatusCode)
551
552 data, err := io.ReadAll(r.Body)
553 assert.NoError(t, err)
554 assert.Equal(t, tc.expData, string(data))
555 })
556 }
557 }
558
View as plain text