all repos — postern

@ 50dc541db37adabbbb220f4abf62aba899c48f5d

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
128	// Test 1: Chained POST JSON
129	hookVal, ok := e.globals["test_post_json"]
130	if !ok {
131		t.Fatal("test_post_json not found in engine globals")
132	}
133
134	res, err := starlark.Call(e.thread, hookVal, nil, nil)
135	if err != nil {
136		t.Fatal(err)
137	}
138
139	if want := starlark.String("value"); res != want {
140		t.Fatalf("got %s, want %s", res, want)
141	}
142
143	// Test 2: Basic Auth + Custom Headers
144	hookVal, ok = e.globals["test_basic_auth"]
145	if !ok {
146		t.Fatal("test_basic_auth not found in engine globals")
147	}
148
149	res, err = starlark.Call(e.thread, hookVal, nil, nil)
150	if err != nil {
151		t.Fatal(err)
152	}
153
154	if want := starlark.String("success"); res != want {
155		t.Fatalf("got %s, want %s", res, want)
156	}
157
158	// Test 3: Error status handling
159	hookVal, ok = e.globals["test_error_status"]
160	if !ok {
161		t.Fatal("test_error_status not found in engine globals")
162	}
163
164	res, err = starlark.Call(e.thread, hookVal, nil, nil)
165	if err != nil {
166		t.Fatal(err)
167	}
168
169	if want := starlark.Bool(false); res != want {
170		t.Fatalf("got %s, want %s", res, want)
171	}
172
173	// Test 4: Timeout (this should time out)
174	hookVal, ok = e.globals["test_timeout"]
175	if !ok {
176		t.Fatal("test_timeout not found in engine globals")
177	}
178
179	_, err = starlark.Call(e.thread, hookVal, nil, nil)
180	if err == nil {
181		t.Fatal("expected timeout to raise an error")
182	}
183}
184
185func TestStarlarkJSONRoundTrip(t *testing.T) {
186	tests := []struct {
187		name    string
188		goValue any
189	}{
190		{"string", "hello"},
191		{"empty string", ""},
192		{"bool true", true},
193		{"bool false", false},
194		{"nil", nil},
195		{"float", 3.14},
196		{"integer as float", 42.0},
197		{"negative integer", -7.0},
198		{"list of strings", []any{"a", "b", "c"}},
199		{"empty list", []any{}},
200		{"map", map[string]any{"name": "Alice", "active": true}},
201		{"empty map", map[string]any{}},
202		{"nested map", map[string]any{"list": []any{"x", "y"}, "num": 3.14, "nested": map[string]any{"k": "v"}}},
203		{"list of mixed", []any{"x", 3.14, true, nil}},
204		{"list of lists", []any{[]any{1.0, 2.0}, []any{"a"}}},
205		{"deeply nested", map[string]any{"users": []any{
206			map[string]any{"name": "a", "age": 30.0},
207			map[string]any{"name": "b", "age": 25.0},
208		}}},
209	}
210
211	for _, tt := range tests {
212		t.Run(tt.name, func(t *testing.T) {
213			star, err := jsonToStarlark(tt.goValue)
214			if err != nil {
215				t.Fatalf("jsonToStarlark() unexpected error: %v", err)
216			}
217
218			got, err := starlarkToJSON(star)
219			if err != nil {
220				t.Fatalf("starlarkToJSON() unexpected error: %v", err)
221			}
222
223			if !jsonEqual(got, tt.goValue) {
224				t.Errorf("round trip mismatch: got %#v, want %#v", got, tt.goValue)
225			}
226		})
227	}
228}
229
230func TestStarlarkToJSONErrors(t *testing.T) {
231	// Build a dict with a non-string key to trigger an error.
232	badDict := starlark.NewDict(1)
233	_ = badDict.SetKey(starlark.MakeInt(1), starlark.String("v"))
234
235	tests := []struct {
236		name  string
237		input starlark.Value
238	}{
239		{"unsupported type", starlark.NewSet(0)},
240		{"dict with non-string key", badDict},
241	}
242
243	for _, tt := range tests {
244		t.Run(tt.name, func(t *testing.T) {
245			_, err := starlarkToJSON(tt.input)
246			if err == nil {
247				t.Fatal("starlarkToJSON() expected error, got nil")
248			}
249		})
250	}
251}
252
253func TestJSONToStarlarkErrors(t *testing.T) {
254	tests := []struct {
255		name  string
256		input any
257	}{
258		{"unsupported int", 5},
259		{"unsupported struct", struct{ X int }{X: 1}},
260		{"unsupported byte slice", []byte("raw")},
261	}
262
263	for _, tt := range tests {
264		t.Run(tt.name, func(t *testing.T) {
265			_, err := jsonToStarlark(tt.input)
266			if err == nil {
267				t.Fatal("jsonToStarlark() expected error, got nil")
268			}
269		})
270	}
271}