orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12)
13
14func testStore(t *testing.T) *Store {
15 t.Helper()
16 s, err := OpenStore(filepath.Join(t.TempDir(), "llm.db"))
17 if err != nil {
18 t.Fatal(err)
19 }
20 t.Cleanup(func() { _ = s.Close() })
21 return s
22}
23
24func TestAKeyIsUsableOnceAndNeverRecoverable(t *testing.T) {
25 s := testStore(t)
26 secret, k, err := s.NewKey("chat")
27 if err != nil {
28 t.Fatal(err)
29 }
30 if !strings.HasPrefix(secret, keyPrefix) {
31 t.Errorf("secret = %q, want the %s prefix", secret, keyPrefix)
32 }
33 got, ok := s.Authenticate(secret)
34 if !ok {
35 t.Fatal("a fresh key did not authenticate")
36 }
37 if got.Name != "chat" || got.ID != k.ID {
38 t.Errorf("resolved to %+v", got)
39 }
40 // The plaintext must not be anywhere the listing can reach.
41 keys, err := s.Keys()
42 if err != nil {
43 t.Fatal(err)
44 }
45 for _, listed := range keys {
46 if strings.Contains(secret, listed.Prefix) && len(listed.Prefix) >= len(secret) {
47 t.Error("the whole secret is in the listing")
48 }
49 }
50}
51
52func TestAWrongKeyIsRefused(t *testing.T) {
53 s := testStore(t)
54 if _, _, err := s.NewKey("chat"); err != nil {
55 t.Fatal(err)
56 }
57 for _, bad := range []string{"", "nonsense", keyPrefix + "wrong", "Bearer something"} {
58 if _, ok := s.Authenticate(bad); ok {
59 t.Errorf("%q authenticated", bad)
60 }
61 }
62}
63
64// Revoking has to take effect on the next request, which is the whole reason
65// the key is checked every time rather than cached.
66func TestRevokingStopsAKeyImmediately(t *testing.T) {
67 s := testStore(t)
68 secret, k, _ := s.NewKey("search")
69 if _, ok := s.Authenticate(secret); !ok {
70 t.Fatal("not usable before revoking")
71 }
72 if err := s.Revoke(k.ID); err != nil {
73 t.Fatal(err)
74 }
75 if _, ok := s.Authenticate(secret); ok {
76 t.Error("a revoked key still authenticates")
77 }
78}
79
80func TestAKeyNeedsAName(t *testing.T) {
81 s := testStore(t)
82 if _, _, err := s.NewKey(" "); err == nil {
83 t.Error("an unnamed key was accepted")
84 }
85}
86
87func TestCallsAreLoggedAndRolledUp(t *testing.T) {
88 s := testStore(t)
89 _, k, _ := s.NewKey("chat")
90 s.LogCall(Call{KeyID: k.ID, Caller: "chat", Model: "local",
91 Messages: `[{"role":"user","content":"hello"}]`, Completion: "hi",
92 PromptTok: 10, OutputTok: 4, DecodeTPS: 60, Status: 200})
93 s.LogCall(Call{KeyID: k.ID, Caller: "chat", Status: 500, Err: "upstream refused"})
94
95 calls, err := s.Calls("chat", 10)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if len(calls) != 2 {
100 t.Fatalf("got %d calls", len(calls))
101 }
102 // The prompt and the completion are the point of the log, so assert they
103 // survived rather than only that a row exists.
104 if !strings.Contains(calls[1].Messages, "hello") || calls[1].Completion != "hi" {
105 t.Errorf("the text was not kept: %+v", calls[1])
106 }
107
108 usage, err := s.Usage(time.Now().Add(-time.Hour))
109 if err != nil {
110 t.Fatal(err)
111 }
112 if len(usage) != 1 {
113 t.Fatalf("got %d callers", len(usage))
114 }
115 u := usage[0]
116 if u.Calls != 2 || u.PromptTok != 10 || u.OutputTok != 4 || u.Errors != 1 {
117 t.Errorf("usage = %+v", u)
118 }
119}
120
121func TestPruneDropsOldCallsOnly(t *testing.T) {
122 s := testStore(t)
123 s.LogCall(Call{Caller: "chat", Completion: "recent"})
124 if _, err := s.db.Exec(`INSERT INTO calls(caller, completion, at) VALUES(?,?,?)`,
125 "chat", "ancient", time.Now().Add(-200*24*time.Hour).Unix()); err != nil {
126 t.Fatal(err)
127 }
128 n, err := s.Prune(90 * 24 * time.Hour)
129 if err != nil {
130 t.Fatal(err)
131 }
132 if n != 1 {
133 t.Errorf("pruned %d rows, want 1", n)
134 }
135 calls, _ := s.Calls("", 10)
136 if len(calls) != 1 || calls[0].Completion != "recent" {
137 t.Errorf("wrong row survived: %+v", calls)
138 }
139}
140
141// The gateway is the security boundary, so an unkeyed request must not reach
142// upstream at all rather than being refused after it.
143func TestAnUnkeyedRequestNeverReachesUpstream(t *testing.T) {
144 reached := false
145 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 reached = true
147 }))
148 defer up.Close()
149
150 s := &site{store: testStore(t), upstream: up.URL, client: up.Client()}
151 rec := httptest.NewRecorder()
152 req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"local"}`))
153 s.requireKey(s.completions)(rec, req)
154
155 if rec.Code != http.StatusUnauthorized {
156 t.Errorf("status = %d, want 401", rec.Code)
157 }
158 if reached {
159 t.Error("an unkeyed request was forwarded upstream")
160 }
161}
162
163func TestAKeyedCallIsForwardedAndWrittenDown(t *testing.T) {
164 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165 w.Header().Set("Content-Type", "application/json")
166 fmt.Fprint(w, `{"choices":[{"message":{"content":"the answer"}}],
167 "usage":{"prompt_tokens":31,"completion_tokens":7},
168 "timings":{"predicted_per_second":58.5}}`)
169 }))
170 defer up.Close()
171
172 store := testStore(t)
173 secret, _, _ := store.NewKey("chat")
174 s := &site{store: store, upstream: up.URL, client: up.Client()}
175
176 rec := httptest.NewRecorder()
177 req := httptest.NewRequest("POST", "/v1/chat/completions",
178 strings.NewReader(`{"model":"local","messages":[{"role":"user","content":"ask"}]}`))
179 req.Header.Set("Authorization", "Bearer "+secret)
180 s.requireKey(s.completions)(rec, req)
181
182 if rec.Code != 200 {
183 t.Fatalf("status = %d", rec.Code)
184 }
185 if !strings.Contains(rec.Body.String(), "the answer") {
186 t.Errorf("the caller did not get the upstream body: %s", rec.Body.String())
187 }
188
189 calls, _ := store.Calls("chat", 5)
190 if len(calls) != 1 {
191 t.Fatalf("logged %d calls", len(calls))
192 }
193 c := calls[0]
194 if c.Completion != "the answer" {
195 t.Errorf("completion = %q", c.Completion)
196 }
197 if c.PromptTok != 31 || c.OutputTok != 7 {
198 t.Errorf("tokens = %d/%d, want 31/7", c.PromptTok, c.OutputTok)
199 }
200 if !strings.Contains(c.Messages, "ask") {
201 t.Errorf("the prompt was not kept: %q", c.Messages)
202 }
203 if c.Status != 200 {
204 t.Errorf("status = %d", c.Status)
205 }
206}
207
208// Incognito is the one case where a call runs and leaves nothing behind, so the
209// test is that the answer still arrives and the log is still empty.
210func TestAnIncognitoCallIsForwardedAndNotWrittenDown(t *testing.T) {
211 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212 w.Header().Set("Content-Type", "application/json")
213 fmt.Fprint(w, `{"choices":[{"message":{"content":"the answer"}}]}`)
214 }))
215 defer up.Close()
216
217 store := testStore(t)
218 secret, _, _ := store.NewKey("chat")
219 s := &site{store: store, upstream: up.URL, client: up.Client()}
220
221 rec := httptest.NewRecorder()
222 req := httptest.NewRequest("POST", "/v1/chat/completions",
223 strings.NewReader(`{"model":"local","messages":[{"role":"user","content":"a secret"}]}`))
224 req.Header.Set("Authorization", "Bearer "+secret)
225 req.Header.Set(incognitoHeader, "1")
226 s.requireKey(s.completions)(rec, req)
227
228 if rec.Code != 200 {
229 t.Fatalf("status = %d", rec.Code)
230 }
231 if !strings.Contains(rec.Body.String(), "the answer") {
232 t.Errorf("the caller did not get the upstream body: %s", rec.Body.String())
233 }
234 calls, _ := store.Calls("", 5)
235 if len(calls) != 0 {
236 t.Fatalf("an incognito call was logged: %+v", calls)
237 }
238}
239
240// A streamed answer is the one worth recording and the one easiest to lose,
241// since the text only exists as deltas passing through.
242func TestAStreamedCallIsReassembledForTheLog(t *testing.T) {
243 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
244 w.Header().Set("Content-Type", "text/event-stream")
245 for _, chunk := range []string{
246 `{"choices":[{"delta":{"content":"one "}}]}`,
247 `{"choices":[{"delta":{"content":"two"}}]}`,
248 `{"choices":[],"usage":{"prompt_tokens":9,"completion_tokens":2},"timings":{"predicted_per_second":61.0}}`,
249 } {
250 fmt.Fprintf(w, "data: %s\n\n", chunk)
251 }
252 fmt.Fprint(w, "data: [DONE]\n\n")
253 }))
254 defer up.Close()
255
256 store := testStore(t)
257 secret, _, _ := store.NewKey("search")
258 s := &site{store: store, upstream: up.URL, client: up.Client()}
259
260 rec := httptest.NewRecorder()
261 req := httptest.NewRequest("POST", "/v1/chat/completions",
262 strings.NewReader(`{"model":"local","stream":true,"messages":[{"role":"user","content":"go"}]}`))
263 req.Header.Set("Authorization", "Bearer "+secret)
264 s.requireKey(s.completions)(rec, req)
265
266 // The caller gets the events unchanged.
267 if !strings.Contains(rec.Body.String(), "data: [DONE]") {
268 t.Errorf("the stream did not pass through:\n%s", rec.Body.String())
269 }
270 calls, _ := store.Calls("search", 5)
271 if len(calls) != 1 {
272 t.Fatalf("logged %d calls", len(calls))
273 }
274 if calls[0].Completion != "one two" {
275 t.Errorf("completion = %q, want %q", calls[0].Completion, "one two")
276 }
277 if calls[0].OutputTok != 2 || calls[0].DecodeTPS != 61.0 {
278 t.Errorf("stats lost: %d tokens, %.1f tok/s", calls[0].OutputTok, calls[0].DecodeTPS)
279 }
280}
281
282// A caller whose key is refused upstream still gets a row, since a log with the
283// failures missing is the one that cannot explain an outage.
284func TestAnUpstreamFailureIsStillLogged(t *testing.T) {
285 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
286 http.Error(w, "no model loaded", http.StatusServiceUnavailable)
287 }))
288 defer up.Close()
289
290 store := testStore(t)
291 secret, _, _ := store.NewKey("chat")
292 s := &site{store: store, upstream: up.URL, client: up.Client()}
293
294 rec := httptest.NewRecorder()
295 req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"local"}`))
296 req.Header.Set("Authorization", "Bearer "+secret)
297 s.requireKey(s.completions)(rec, req)
298
299 calls, _ := store.Calls("chat", 5)
300 if len(calls) != 1 {
301 t.Fatalf("logged %d calls", len(calls))
302 }
303 if calls[0].Status != http.StatusServiceUnavailable {
304 t.Errorf("status = %d, want 503", calls[0].Status)
305 }
306}
307
308func TestTheApiKeyHeaderIsAcceptedToo(t *testing.T) {
309 store := testStore(t)
310 secret, _, _ := store.NewKey("aiagent")
311 s := &site{store: store}
312 r := httptest.NewRequest("POST", "/v1/chat/completions", nil)
313 r.Header.Set("X-Api-Key", secret)
314 if _, ok := s.authenticate(r); !ok {
315 t.Error("a key sent as X-Api-Key was refused")
316 }
317}
318
319func TestShortRendersMessagesReadably(t *testing.T) {
320 msgs, _ := json.Marshal([]map[string]string{
321 {"role": "system", "content": "be brief"},
322 {"role": "user", "content": "what is the time"},
323 })
324 got := short(string(msgs), 200)
325 if !strings.Contains(got, "system: be brief") || !strings.Contains(got, "user: what is the time") {
326 t.Errorf("got %q", got)
327 }
328 if len(short(string(msgs), 10)) > 13 {
329 t.Errorf("not truncated: %q", short(string(msgs), 10))
330 }
331}