HH-878: add control-plane route/method matrix coverage (#32)
This commit is contained in:
@@ -143,6 +143,181 @@ func TestControlPlaneAuthentication(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type controlPlaneRouteCase struct {
|
||||
method, pattern, path, body string
|
||||
wantAuthenticatedStatus int
|
||||
}
|
||||
|
||||
func TestControlPlaneRouteRegistrationMatrix(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(authenticate("operator", "unit-test-password"))
|
||||
registerHubWithNetwork(app, nil, nil, nil)
|
||||
registerPhaseA(app, nil, nil)
|
||||
routes := controlPlaneRouteMatrix()
|
||||
assertControlPlaneRouteMatrix(t, app, routes)
|
||||
|
||||
for _, route := range routes {
|
||||
if response := do(app, route.method, route.path, route.body); response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("registered route %s %s returned %d without credentials, want %d", route.method, route.path,
|
||||
response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
if response := do(app, http.MethodPost, "/api/not-registered", ""); response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unregistered route returned %d without credentials, want %d", response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlPlaneRegisteredRouteMatrix(t *testing.T) {
|
||||
databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run control-plane route coverage")
|
||||
}
|
||||
databaseURL = isolatedControlPlaneDatabaseURL(t, databaseURL)
|
||||
ctx := context.Background()
|
||||
phaseAStore, err := phasea.Open(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = phaseAStore.Close() })
|
||||
hubStore, err := hub.Open(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = hubStore.Close() })
|
||||
|
||||
webDirectory := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(webDirectory, "index.html"), []byte("index"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := newHandlerWithStores(webDirectory, "operator", "unit-test-password", phaseAStore, hubStore)
|
||||
routes := controlPlaneRouteMatrix()
|
||||
assertControlPlaneRouteMatrix(t, app, routes)
|
||||
|
||||
for _, route := range routes {
|
||||
route := route
|
||||
t.Run(route.method+" "+route.pattern, func(t *testing.T) {
|
||||
if response := do(app, route.method, route.path, route.body); response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauthenticated %s %s returned %d, want %d", route.method, route.path, response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
response := do(app, route.method, route.path, route.body, "operator", "unit-test-password")
|
||||
if response.Code != route.wantAuthenticatedStatus {
|
||||
t.Fatalf("authenticated %s %s returned %d, want %d: %s", route.method, route.path, response.Code,
|
||||
route.wantAuthenticatedStatus, response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, route := range []struct {
|
||||
method, path string
|
||||
want int
|
||||
}{
|
||||
{http.MethodPost, "/api/not-registered", http.StatusMethodNotAllowed},
|
||||
{http.MethodDelete, "/api/not-registered", http.StatusMethodNotAllowed},
|
||||
} {
|
||||
t.Run("unregistered "+route.method, func(t *testing.T) {
|
||||
if response := do(app, route.method, route.path, ""); response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauthenticated unregistered %s %s returned %d, want %d", route.method, route.path, response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if response := do(app, route.method, route.path, "", "operator", "unit-test-password"); response.Code != route.want {
|
||||
t.Fatalf("authenticated unregistered %s %s returned %d, want %d", route.method, route.path, response.Code, route.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertControlPlaneRouteMatrix(t *testing.T, app *fiber.App, routes []controlPlaneRouteCase) {
|
||||
t.Helper()
|
||||
expected := make(map[string]struct{}, len(routes))
|
||||
for _, route := range routes {
|
||||
key := controlPlaneRouteKey(route.method, route.pattern)
|
||||
if _, exists := expected[key]; exists {
|
||||
t.Fatalf("duplicate route matrix entry: %s", key)
|
||||
}
|
||||
expected[key] = struct{}{}
|
||||
}
|
||||
actual := registeredControlPlaneRoutes(app)
|
||||
for key := range expected {
|
||||
if _, exists := actual[key]; !exists {
|
||||
t.Errorf("route matrix is missing registered route: %s", key)
|
||||
}
|
||||
}
|
||||
for key := range actual {
|
||||
if _, exists := expected[key]; !exists {
|
||||
t.Errorf("registered API route is missing from route matrix: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func controlPlaneRouteKey(method, path string) string { return method + " " + path }
|
||||
|
||||
func registeredControlPlaneRoutes(app *fiber.App) map[string]struct{} {
|
||||
routes := map[string]struct{}{}
|
||||
for _, methodRoutes := range app.Stack() {
|
||||
for _, route := range methodRoutes {
|
||||
if !strings.HasPrefix(route.Path, "/api/") {
|
||||
continue
|
||||
}
|
||||
switch route.Method {
|
||||
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete:
|
||||
routes[controlPlaneRouteKey(route.Method, route.Path)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func controlPlaneRouteMatrix() []controlPlaneRouteCase {
|
||||
return []controlPlaneRouteCase{
|
||||
{http.MethodGet, "/api/browsers", "/api/browsers", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/browsers/:alias", "/api/browsers/missing", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/browsers", "/api/browsers", "", http.StatusBadRequest},
|
||||
{http.MethodPost, "/api/browsers/:alias/:action", "/api/browsers/missing/start", "", http.StatusNotFound},
|
||||
{http.MethodDelete, "/api/browsers/:alias", "/api/browsers/missing", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodGet, "/api/network-exits", "/api/network-exits", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/network-exits/:id", "/api/network-exits/missing", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/network-exits", "/api/network-exits", "", http.StatusBadRequest},
|
||||
{http.MethodPost, "/api/network-exits/:id/check", "/api/network-exits/missing/check", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/network-exits/:id/disable", "/api/network-exits/missing/disable", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodGet, "/api/browser-images", "/api/browser-images", "", http.StatusOK},
|
||||
{http.MethodPost, "/api/browser-images", "/api/browser-images", "", http.StatusBadRequest},
|
||||
{http.MethodPut, "/api/browser-images/:version", "/api/browser-images/999.0.0", `{"image_ref":"registry.example/browser:missing"}`, http.StatusNotFound},
|
||||
{http.MethodDelete, "/api/browser-images/:version", "/api/browser-images/999.0.0", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodGet, "/api/gateways", "/api/gateways", "", http.StatusOK},
|
||||
{http.MethodPost, "/api/gateways", "/api/gateways", "", http.StatusBadRequest},
|
||||
{http.MethodDelete, "/api/gateways/:name", "/api/gateways/missing", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodPost, "/api/phase-a/accounts", "/api/phase-a/accounts", "", http.StatusBadRequest},
|
||||
{http.MethodGet, "/api/phase-a/accounts", "/api/phase-a/accounts", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/phase-a/accounts/:id", "/api/phase-a/accounts/missing", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/pause", "/api/phase-a/accounts/missing/pause", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/resume", "/api/phase-a/accounts/missing/resume", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/revoke", "/api/phase-a/accounts/missing/revoke", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodPost, "/api/phase-a/drafts", "/api/phase-a/drafts", "", http.StatusBadRequest},
|
||||
{http.MethodGet, "/api/phase-a/drafts", "/api/phase-a/drafts", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/phase-a/drafts/:id", "/api/phase-a/drafts/missing", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodPost, "/api/phase-a/confirmations", "/api/phase-a/confirmations", "", http.StatusBadRequest},
|
||||
{http.MethodGet, "/api/phase-a/confirmations", "/api/phase-a/confirmations", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/phase-a/confirmations/:id", "/api/phase-a/confirmations/missing", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodPost, "/api/phase-a/tasks", "/api/phase-a/tasks", "", http.StatusBadRequest},
|
||||
{http.MethodGet, "/api/phase-a/tasks", "/api/phase-a/tasks", "", http.StatusOK},
|
||||
{http.MethodGet, "/api/phase-a/tasks/:id", "/api/phase-a/tasks/missing", "", http.StatusNotFound},
|
||||
{http.MethodGet, "/api/phase-a/attempts/:id", "/api/phase-a/attempts/missing", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/tasks/:id/cancel", "/api/phase-a/tasks/missing/cancel", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/tasks/:id/verify", "/api/phase-a/tasks/missing/verify", "", http.StatusBadRequest},
|
||||
{http.MethodPost, "/api/phase-a/tasks/:id/resume", "/api/phase-a/tasks/missing/resume", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/tasks/:id/finish", "/api/phase-a/tasks/missing/finish", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodPost, "/api/phase-a/mock/execute", "/api/phase-a/mock/execute", "", http.StatusBadRequest},
|
||||
{http.MethodGet, "/api/phase-a/audit", "/api/phase-a/audit", "", http.StatusOK},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorNotificationFiltersAndRedacts(t *testing.T) {
|
||||
previousLevel := logrus.GetLevel()
|
||||
t.Cleanup(func() { logrus.SetLevel(previousLevel) })
|
||||
|
||||
Reference in New Issue
Block a user