387 lines
10 KiB
Go
387 lines
10 KiB
Go
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(`<ok/>`))
|
|
|
|
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")
|
|
}
|
|
}
|