package handlers import ( "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "go.starlark.net/starlark" ) // ─── matchAPIRoute ────────────────────────── func TestMatchAPIRoute_BooleanTrue(t *testing.T) { m := map[string]any{"api_routes": true} if !matchAPIRoute(m, "GET", "/anything") { t.Error("boolean true should match any route") } if !matchAPIRoute(m, "POST", "/foo/bar") { t.Error("boolean true should match any route") } } func TestMatchAPIRoute_BooleanFalse(t *testing.T) { m := map[string]any{"api_routes": false} if matchAPIRoute(m, "GET", "/anything") { t.Error("boolean false should not match") } } func TestMatchAPIRoute_Missing(t *testing.T) { m := map[string]any{} if matchAPIRoute(m, "GET", "/anything") { t.Error("missing api_routes should not match") } } func TestMatchAPIRoute_ExactMatch(t *testing.T) { m := map[string]any{ "api_routes": []any{ map[string]any{"method": "GET", "path": "/status"}, map[string]any{"method": "POST", "path": "/webhook"}, }, } tests := []struct { method, path string want bool }{ {"GET", "/status", true}, {"POST", "/webhook", true}, {"GET", "/webhook", false}, // wrong method {"POST", "/status", false}, // wrong method {"GET", "/unknown", false}, // undeclared path {"GET", "/status/", false}, // trailing slash mismatch } for _, tt := range tests { got := matchAPIRoute(m, tt.method, tt.path) if got != tt.want { t.Errorf("matchAPIRoute(%s %s) = %v, want %v", tt.method, tt.path, got, tt.want) } } } func TestMatchAPIRoute_WildcardMethod(t *testing.T) { m := map[string]any{ "api_routes": []any{ map[string]any{"method": "*", "path": "/anything"}, }, } for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH"} { if !matchAPIRoute(m, method, "/anything") { t.Errorf("wildcard method should match %s", method) } } } func TestMatchAPIRoute_PrefixMatch(t *testing.T) { m := map[string]any{ "api_routes": []any{ map[string]any{"method": "*", "path": "/proxy/*"}, }, } tests := []struct { path string want bool }{ {"/proxy/", true}, {"/proxy/foo", true}, {"/proxy/foo/bar/baz", true}, {"/prox", false}, // not the prefix {"/other", false}, // different path } for _, tt := range tests { got := matchAPIRoute(m, "GET", tt.path) if got != tt.want { t.Errorf("matchAPIRoute(GET %s) = %v, want %v", tt.path, got, tt.want) } } } func TestMatchAPIRoute_CaseInsensitiveMethod(t *testing.T) { m := map[string]any{ "api_routes": []any{ map[string]any{"method": "get", "path": "/status"}, }, } if !matchAPIRoute(m, "GET", "/status") { t.Error("method matching should be case-insensitive") } } func TestMatchAPIRoute_EmptyArray(t *testing.T) { m := map[string]any{"api_routes": []any{}} if matchAPIRoute(m, "GET", "/anything") { t.Error("empty array should not match") } } func TestMatchAPIRoute_MalformedEntries(t *testing.T) { m := map[string]any{ "api_routes": []any{ "not a map", 42, map[string]any{"method": "GET"}, // missing path map[string]any{"method": "GET", "path": "/ok"}, }, } // Malformed entries skipped, valid one matches if !matchAPIRoute(m, "GET", "/ok") { t.Error("should skip malformed entries and match valid one") } if matchAPIRoute(m, "GET", "/not-declared") { t.Error("undeclared route should not match") } } // ─── HasAPIRoutes ─────────────────────────── func TestHasAPIRoutes(t *testing.T) { tests := []struct { name string manifest map[string]any want bool }{ {"empty manifest", map[string]any{}, false}, {"boolean true", map[string]any{"api_routes": true}, true}, {"boolean false", map[string]any{"api_routes": false}, false}, {"non-empty array", map[string]any{"api_routes": []any{ map[string]any{"method": "GET", "path": "/x"}, }}, true}, {"empty array", map[string]any{"api_routes": []any{}}, false}, {"wrong type", map[string]any{"api_routes": "yes"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := HasAPIRoutes(tt.manifest) if got != tt.want { t.Errorf("HasAPIRoutes() = %v, want %v", got, tt.want) } }) } } // ─── writeStarlarkResponse ────────────────── func TestWriteStarlarkResponse_FullDict(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) hdrs := starlark.NewDict(1) _ = hdrs.SetKey(starlark.String("X-Custom"), starlark.String("hello")) resp := starlark.NewDict(3) _ = resp.SetKey(starlark.String("status"), starlark.MakeInt(201)) _ = resp.SetKey(starlark.String("headers"), hdrs) _ = resp.SetKey(starlark.String("body"), starlark.String(`{"created":true}`)) writeStarlarkResponse(c, resp) if w.Code != 201 { t.Errorf("status = %d, want 201", w.Code) } if w.Header().Get("X-Custom") != "hello" { t.Errorf("X-Custom = %q, want %q", w.Header().Get("X-Custom"), "hello") } if w.Header().Get("Content-Type") != "application/json" { t.Errorf("Content-Type = %q, want application/json", w.Header().Get("Content-Type")) } if w.Body.String() != `{"created":true}` { t.Errorf("body = %q", w.Body.String()) } } func TestWriteStarlarkResponse_DefaultStatus(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) resp := starlark.NewDict(1) _ = resp.SetKey(starlark.String("body"), starlark.String("ok")) writeStarlarkResponse(c, resp) if w.Code != 200 { t.Errorf("status = %d, want 200 (default)", w.Code) } } func TestWriteStarlarkResponse_None(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) writeStarlarkResponse(c, starlark.None) if w.Code != 204 { t.Errorf("status = %d, want 204 for None", w.Code) } } func TestWriteStarlarkResponse_Nil(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) writeStarlarkResponse(c, nil) if w.Code != 204 { t.Errorf("status = %d, want 204 for nil", w.Code) } } func TestWriteStarlarkResponse_NotDict(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) writeStarlarkResponse(c, starlark.String("oops")) if w.Code != 500 { t.Errorf("status = %d, want 500 for non-dict", w.Code) } if !strings.Contains(w.Body.String(), "must return a dict") { t.Errorf("body = %q, expected error about dict", w.Body.String()) } } func TestWriteStarlarkResponse_PlainText(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) resp := starlark.NewDict(1) _ = resp.SetKey(starlark.String("body"), starlark.String("plain text")) writeStarlarkResponse(c, resp) if w.Header().Get("Content-Type") != "text/plain; charset=utf-8" { t.Errorf("Content-Type = %q, want text/plain", w.Header().Get("Content-Type")) } } func TestWriteStarlarkResponse_ExplicitContentType(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) hdrs := starlark.NewDict(1) _ = hdrs.SetKey(starlark.String("Content-Type"), starlark.String("text/xml")) resp := starlark.NewDict(2) _ = resp.SetKey(starlark.String("headers"), hdrs) _ = resp.SetKey(starlark.String("body"), starlark.String(``)) writeStarlarkResponse(c, resp) if w.Header().Get("Content-Type") != "text/xml" { t.Errorf("Content-Type = %q, want text/xml (explicit)", w.Header().Get("Content-Type")) } } // ─── buildRequestDict ─────────────────────── func TestBuildRequestDict(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/s/my-ext/api/data?page=2&limit=10", strings.NewReader(`{"input":"test"}`)) c.Request.Header.Set("Content-Type", "application/json") c.Request.Header.Set("X-Token", "abc") c.Set("user_id", "user-uuid-123") d, err := buildRequestDict(c, "/data") if err != nil { t.Fatalf("buildRequestDict: %v", err) } // Method mv, _, _ := d.Get(starlark.String("method")) if s, _ := starlark.AsString(mv); s != "POST" { t.Errorf("method = %q, want POST", s) } // Path pv, _, _ := d.Get(starlark.String("path")) if s, _ := starlark.AsString(pv); s != "/data" { t.Errorf("path = %q, want /data", s) } // Body bv, _, _ := d.Get(starlark.String("body")) if s, _ := starlark.AsString(bv); s != `{"input":"test"}` { t.Errorf("body = %q", s) } // User ID uv, _, _ := d.Get(starlark.String("user_id")) if s, _ := starlark.AsString(uv); s != "user-uuid-123" { t.Errorf("user_id = %q", s) } // Headers hv, _, _ := d.Get(starlark.String("headers")) hd := hv.(*starlark.Dict) ctv, found, _ := hd.Get(starlark.String("content-type")) if !found { t.Fatal("missing content-type header") } if s, _ := starlark.AsString(ctv); s != "application/json" { t.Errorf("content-type = %q", s) } // Query qv, _, _ := d.Get(starlark.String("query")) qd := qv.(*starlark.Dict) pageV, found, _ := qd.Get(starlark.String("page")) if !found { t.Fatal("missing page query param") } if s, _ := starlark.AsString(pageV); s != "2" { t.Errorf("query page = %q, want 2", s) } } func TestBuildRequestDict_EmptyBody(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/s/ext/api/health", nil) d, err := buildRequestDict(c, "/health") if err != nil { t.Fatalf("buildRequestDict: %v", err) } bv, _, _ := d.Get(starlark.String("body")) if s, _ := starlark.AsString(bv); s != "" { t.Errorf("body should be empty for GET, got %q", s) } } // ─── hasPermission ────────────────────────── func TestHasPermission(t *testing.T) { granted := []string{"secrets.read", "api.http", "notifications.send"} if !hasPermission(granted, "api.http") { t.Error("should find api.http") } if hasPermission(granted, "db.read") { t.Error("should not find db.read") } if hasPermission(nil, "api.http") { t.Error("nil slice should return false") } if hasPermission([]string{}, "api.http") { t.Error("empty slice should return false") } }