package policy import ( "bytes" "encoding/json" "log" "net/http" "net/http/httptest" "os" "path" "testing" "postern/internal/db" "go.starlark.net/starlark" ) // jsonEqual reports whether two JSON-like Go values are semantically equal // by comparing their canonical JSON encodings. This tolerates numeric type // shifts across the round trip (e.g. float64 integer -> starlark.Int -> int64). func jsonEqual(a, b any) bool { ja, err := json.Marshal(a) if err != nil { return false } jb, err := json.Marshal(b) if err != nil { return false } return bytes.Equal(ja, jb) } func TestHTTPModule(t *testing.T) { posternDB, err := db.OpenDB(&db.Config{ DBPath: path.Join(t.TempDir(), "meta.db"), }) if err != nil { t.Fatal(err) } defer func(posternDB *db.DB) { err := posternDB.Close() if err != nil { log.Fatal(err) } }(posternDB) // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for expected headers if r.Header.Get("Authorization") == "Bearer secret" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"key": "value"}`)) return } if r.Header.Get("Authorization") == "Basic dXNlcjpwYXNz" { w.Header().Set("X-Test", "abc") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("success")) return } if r.URL.Path == "/error" { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("Internal Server Error")) return } if r.URL.Path == "/delay" { // Sleep for 200ms to test timeout // This will be caught by the 50ms timeout in the test // Note: This is a test to ensure timeout works properly // We'll check for this in the timeout test case w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("delayed response")) return } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) })) defer server.Close() policy := ` def init(): return None def test_post_json(): res = http.post("` + server.URL + `") \ .bearer("secret") \ .json({"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}) \ .send() return res.json()["key"] def test_basic_auth(): res = http.get("` + server.URL + `") \ .basic_auth("user", "pass") \ .header("X-Test", "abc") \ .send() return res.text() def test_error_status(): res = http.get("` + server.URL + `/error") \ .send() return res.is_ok def test_timeout(): # This should timeout since server delays response res = http.get("` + server.URL + `/delay") \ .timeout(0.05) \ .send() return res.text() ` policyFile := path.Join(t.TempDir(), "policy.star") err = os.WriteFile(policyFile, []byte(policy), 0644) if err != nil { t.Fatal(err) } e, err := NewEngine(policyFile, posternDB) if err != nil { t.Fatal(err) } // Test 1: Chained POST JSON hookVal, ok := e.globals["test_post_json"] if !ok { t.Fatal("test_post_json not found in engine globals") } res, err := starlark.Call(e.thread, hookVal, nil, nil) if err != nil { t.Fatal(err) } if want := starlark.String("value"); res != want { t.Fatalf("got %s, want %s", res, want) } // Test 2: Basic Auth + Custom Headers hookVal, ok = e.globals["test_basic_auth"] if !ok { t.Fatal("test_basic_auth not found in engine globals") } res, err = starlark.Call(e.thread, hookVal, nil, nil) if err != nil { t.Fatal(err) } if want := starlark.String("success"); res != want { t.Fatalf("got %s, want %s", res, want) } // Test 3: Error status handling hookVal, ok = e.globals["test_error_status"] if !ok { t.Fatal("test_error_status not found in engine globals") } res, err = starlark.Call(e.thread, hookVal, nil, nil) if err != nil { t.Fatal(err) } if want := starlark.Bool(false); res != want { t.Fatalf("got %s, want %s", res, want) } // Test 4: Timeout (this should time out) hookVal, ok = e.globals["test_timeout"] if !ok { t.Fatal("test_timeout not found in engine globals") } _, err = starlark.Call(e.thread, hookVal, nil, nil) if err == nil { t.Fatal("expected timeout to raise an error") } } func TestStarlarkJSONRoundTrip(t *testing.T) { tests := []struct { name string goValue any }{ {"string", "hello"}, {"empty string", ""}, {"bool true", true}, {"bool false", false}, {"nil", nil}, {"float", 3.14}, {"integer as float", 42.0}, {"negative integer", -7.0}, {"list of strings", []any{"a", "b", "c"}}, {"empty list", []any{}}, {"map", map[string]any{"name": "Alice", "active": true}}, {"empty map", map[string]any{}}, {"nested map", map[string]any{"list": []any{"x", "y"}, "num": 3.14, "nested": map[string]any{"k": "v"}}}, {"list of mixed", []any{"x", 3.14, true, nil}}, {"list of lists", []any{[]any{1.0, 2.0}, []any{"a"}}}, {"deeply nested", map[string]any{"users": []any{ map[string]any{"name": "a", "age": 30.0}, map[string]any{"name": "b", "age": 25.0}, }}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { star, err := jsonToStarlark(tt.goValue) if err != nil { t.Fatalf("jsonToStarlark() unexpected error: %v", err) } got, err := starlarkToJSON(star) if err != nil { t.Fatalf("starlarkToJSON() unexpected error: %v", err) } if !jsonEqual(got, tt.goValue) { t.Errorf("round trip mismatch: got %#v, want %#v", got, tt.goValue) } }) } } func TestStarlarkToJSONErrors(t *testing.T) { // Build a dict with a non-string key to trigger an error. badDict := starlark.NewDict(1) _ = badDict.SetKey(starlark.MakeInt(1), starlark.String("v")) tests := []struct { name string input starlark.Value }{ {"unsupported type", starlark.NewSet(0)}, {"dict with non-string key", badDict}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := starlarkToJSON(tt.input) if err == nil { t.Fatal("starlarkToJSON() expected error, got nil") } }) } } func TestJSONToStarlarkErrors(t *testing.T) { tests := []struct { name string input any }{ {"unsupported int", 5}, {"unsupported struct", struct{ X int }{X: 1}}, {"unsupported byte slice", []byte("raw")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := jsonToStarlark(tt.input) if err == nil { t.Fatal("jsonToStarlark() expected error, got nil") } }) } }