HH-878: add control-plane route/method matrix coverage (#32)

This commit is contained in:
2026-08-31 20:06:11 +08:00
parent 024bf14448
commit 2c2291998f
+175
View File
@@ -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) })