all repos — postern

@ v0.1.0-alpha

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