all repos — postern

@ 9799a3215ac0185fb9b4e7d6ec0645c2cb2ce55b

Modern mail management

clone
git clone https:///postern.git

internal/policy/http_test.go (view raw)

  1package policy
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"net/http"
  7	"net/http/httptest"
  8	"os"
  9	"path"
 10	"testing"
 11
 12	"postern/internal/db"
 13
 14	"go.starlark.net/starlark"
 15)
 16
 17// jsonEqual reports whether two JSON-like Go values are semantically equal
 18// by comparing their canonical JSON encodings. This tolerates numeric type
 19// shifts across the round trip (e.g. float64 integer -> starlark.Int -> int64).
 20func jsonEqual(a, b any) bool {
 21	ja, err := json.Marshal(a)
 22	if err != nil {
 23		return false
 24	}
 25	jb, err := json.Marshal(b)
 26	if err != nil {
 27		return false
 28	}
 29	return bytes.Equal(ja, jb)
 30}
 31
 32func TestHTTPModule(t *testing.T) {
 33	posternDB, err := db.OpenDB(&db.Config{
 34		DBPath: path.Join(t.TempDir(), "meta.db"),
 35	})
 36	if err != nil {
 37		t.Fatal(err)
 38	}
 39
 40	defer posternDB.Close()
 41
 42	// Create a test server
 43	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 44		// Check for expected headers
 45		if r.Header.Get("Authorization") == "Bearer secret" {
 46			w.Header().Set("Content-Type", "application/json")
 47			w.WriteHeader(http.StatusOK)
 48			w.Write([]byte(`{"key": "value"}`))
 49			return
 50		}
 51
 52		if r.Header.Get("Authorization") == "Basic dXNlcjpwYXNz" {
 53			w.Header().Set("X-Test", "abc")
 54			w.WriteHeader(http.StatusOK)
 55			w.Write([]byte("success"))
 56			return
 57		}
 58
 59		if r.URL.Path == "/error" {
 60			w.WriteHeader(http.StatusInternalServerError)
 61			w.Write([]byte("Internal Server Error"))
 62			return
 63		}
 64
 65		if r.URL.Path == "/delay" {
 66			// Sleep for 200ms to test timeout
 67			// This will be caught by the 50ms timeout in the test
 68			// Note: This is a test to ensure timeout works properly
 69			// We'll check for this in the timeout test case
 70			w.WriteHeader(http.StatusOK)
 71			w.Write([]byte("delayed response"))
 72			return
 73		}
 74
 75		w.WriteHeader(http.StatusOK)
 76		w.Write([]byte("ok"))
 77	}))
 78	defer server.Close()
 79
 80	policy := `
 81def init():
 82    return None
 83
 84def test_post_json():
 85    res = http.post("` + server.URL + `") \
 86        .bearer("secret") \
 87        .json({"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}) \
 88        .send()
 89    return res.json()["key"]
 90
 91def test_basic_auth():
 92    res = http.get("` + server.URL + `") \
 93        .basic_auth("user", "pass") \
 94        .header("X-Test", "abc") \
 95        .send()
 96    return res.text()
 97
 98def test_error_status():
 99    res = http.get("` + server.URL + `/error") \
100        .send()
101    return res.is_ok
102
103def test_timeout():
104    # This should timeout since server delays response
105    res = http.get("` + server.URL + `/delay") \
106        .timeout(0.05) \
107        .send()
108    return res.text()
109`
110
111	policyFile := path.Join(t.TempDir(), "policy.star")
112	err = os.WriteFile(policyFile, []byte(policy), 0644)
113	if err != nil {
114		t.Fatal(err)
115	}
116
117	e, err := NewEngine(policyFile, posternDB)
118	if err != nil {
119		t.Fatal(err)
120	}
121
122	// Test 1: Chained POST JSON
123	hookVal, ok := e.globals["test_post_json"]
124	if !ok {
125		t.Fatal("test_post_json not found in engine globals")
126	}
127
128	res, err := starlark.Call(e.thread, hookVal, nil, nil)
129	if err != nil {
130		t.Fatal(err)
131	}
132
133	if want := starlark.String("value"); res != want {
134		t.Fatalf("got %s, want %s", res, want)
135	}
136
137	// Test 2: Basic Auth + Custom Headers
138	hookVal, ok = e.globals["test_basic_auth"]
139	if !ok {
140		t.Fatal("test_basic_auth not found in engine globals")
141	}
142
143	res, err = starlark.Call(e.thread, hookVal, nil, nil)
144	if err != nil {
145		t.Fatal(err)
146	}
147
148	if want := starlark.String("success"); res != want {
149		t.Fatalf("got %s, want %s", res, want)
150	}
151
152	// Test 3: Error status handling
153	hookVal, ok = e.globals["test_error_status"]
154	if !ok {
155		t.Fatal("test_error_status not found in engine globals")
156	}
157
158	res, err = starlark.Call(e.thread, hookVal, nil, nil)
159	if err != nil {
160		t.Fatal(err)
161	}
162
163	if want := starlark.Bool(false); res != want {
164		t.Fatalf("got %s, want %s", res, want)
165	}
166
167	// Test 4: Timeout (this should time out)
168	hookVal, ok = e.globals["test_timeout"]
169	if !ok {
170		t.Fatal("test_timeout not found in engine globals")
171	}
172
173	_, err = starlark.Call(e.thread, hookVal, nil, nil)
174	if err == nil {
175		t.Fatal("expected timeout to raise an error")
176	}
177}
178
179func TestStarlarkJSONRoundTrip(t *testing.T) {
180	tests := []struct {
181		name    string
182		goValue any
183	}{
184		{"string", "hello"},
185		{"empty string", ""},
186		{"bool true", true},
187		{"bool false", false},
188		{"nil", nil},
189		{"float", 3.14},
190		{"integer as float", 42.0},
191		{"negative integer", -7.0},
192		{"list of strings", []any{"a", "b", "c"}},
193		{"empty list", []any{}},
194		{"map", map[string]any{"name": "Alice", "active": true}},
195		{"empty map", map[string]any{}},
196		{"nested map", map[string]any{"list": []any{"x", "y"}, "num": 3.14, "nested": map[string]any{"k": "v"}}},
197		{"list of mixed", []any{"x", 3.14, true, nil}},
198		{"list of lists", []any{[]any{1.0, 2.0}, []any{"a"}}},
199		{"deeply nested", map[string]any{"users": []any{
200			map[string]any{"name": "a", "age": 30.0},
201			map[string]any{"name": "b", "age": 25.0},
202		}}},
203	}
204
205	for _, tt := range tests {
206		t.Run(tt.name, func(t *testing.T) {
207			star, err := jsonToStarlark(tt.goValue)
208			if err != nil {
209				t.Fatalf("jsonToStarlark() unexpected error: %v", err)
210			}
211
212			got, err := starlarkToJSON(star)
213			if err != nil {
214				t.Fatalf("starlarkToJSON() unexpected error: %v", err)
215			}
216
217			if !jsonEqual(got, tt.goValue) {
218				t.Errorf("round trip mismatch: got %#v, want %#v", got, tt.goValue)
219			}
220		})
221	}
222}
223
224func TestStarlarkToJSONErrors(t *testing.T) {
225	// Build a dict with a non-string key to trigger an error.
226	badDict := starlark.NewDict(1)
227	_ = badDict.SetKey(starlark.MakeInt(1), starlark.String("v"))
228
229	tests := []struct {
230		name  string
231		input starlark.Value
232	}{
233		{"unsupported type", starlark.NewSet(0)},
234		{"dict with non-string key", badDict},
235	}
236
237	for _, tt := range tests {
238		t.Run(tt.name, func(t *testing.T) {
239			_, err := starlarkToJSON(tt.input)
240			if err == nil {
241				t.Fatal("starlarkToJSON() expected error, got nil")
242			}
243		})
244	}
245}
246
247func TestJSONToStarlarkErrors(t *testing.T) {
248	tests := []struct {
249		name  string
250		input any
251	}{
252		{"unsupported int", 5},
253		{"unsupported struct", struct{ X int }{X: 1}},
254		{"unsupported byte slice", []byte("raw")},
255	}
256
257	for _, tt := range tests {
258		t.Run(tt.name, func(t *testing.T) {
259			_, err := jsonToStarlark(tt.input)
260			if err == nil {
261				t.Fatal("jsonToStarlark() expected error, got nil")
262			}
263		})
264	}
265}