1package silverbullet
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10)
11
12func newTestServer(mux *http.ServeMux) *httptest.Server {
13 return httptest.NewServer(mux)
14}
15
16func testClient(url string) *Client {
17 return New(url, Auth{})
18}
19
20func testClientWithBasicAuth(url string) *Client {
21 return New(url, Auth{User: "testuser", Pass: "testpass"})
22}
23
24func setupAuthServer(mux *http.ServeMux) {
25 mux.HandleFunc("/.auth", func(w http.ResponseWriter, r *http.Request) {
26 if r.Method != http.MethodPost {
27 w.WriteHeader(http.StatusMethodNotAllowed)
28 return
29 }
30 username := r.FormValue("username")
31 password := r.FormValue("password")
32 if username != "testuser" || password != "testpass" {
33 w.WriteHeader(http.StatusUnauthorized)
34 return
35 }
36 http.SetCookie(w, &http.Cookie{
37 Name: "auth_session",
38 Value: "mock-jwt-token",
39 })
40 w.WriteHeader(http.StatusOK)
41 })
42}
43
44func testClientWithBearerToken(url string) *Client {
45 return New(url, Auth{Token: "testtoken123"})
46}
47
48func TestExecuteLua(t *testing.T) {
49 mux := http.NewServeMux()
50 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
51 if r.Method != http.MethodPost {
52 t.Errorf("expected POST, got %s", r.Method)
53 }
54 if r.Header.Get("Content-Type") != "text/plain" {
55 t.Errorf("expected Content-Type text/plain, got %s", r.Header.Get("Content-Type"))
56 }
57
58 result := LuaResult{Result: json.RawMessage(`2`)}
59 json.NewEncoder(w).Encode(result)
60 })
61
62 srv := newTestServer(mux)
63 defer srv.Close()
64
65 client := testClient(srv.URL)
66 result, err := client.ExecuteLua(context.Background(), "1 + 1", 30)
67 if err != nil {
68 t.Fatalf("ExecuteLua failed: %v", err)
69 }
70
71 if result.Result == nil {
72 t.Fatal("expected result, got nil")
73 }
74}
75
76func TestExecuteLuaError(t *testing.T) {
77 mux := http.NewServeMux()
78 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
79 w.Header().Set("Content-Type", "application/json")
80 w.WriteHeader(http.StatusInternalServerError)
81 fmt.Fprintf(w, `{"error":"attempt to call nil value"}`)
82 })
83
84 srv := newTestServer(mux)
85 defer srv.Close()
86
87 client := testClient(srv.URL)
88 result, err := client.ExecuteLua(context.Background(), "bad()", 30)
89 if err != nil {
90 t.Fatalf("ExecuteLua returned unexpected error: %v", err)
91 }
92
93 if result.Error == "" {
94 t.Fatal("expected error in result, got empty string")
95 }
96}
97
98func TestExecuteLuaTimeoutHeader(t *testing.T) {
99 mux := http.NewServeMux()
100 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
101 timeout := r.Header.Get("X-Timeout")
102 if timeout != "60" {
103 t.Errorf("expected X-Timeout 60, got %s", timeout)
104 }
105 result := LuaResult{Result: json.RawMessage(`"ok"`)}
106 json.NewEncoder(w).Encode(result)
107 })
108
109 srv := newTestServer(mux)
110 defer srv.Close()
111
112 client := testClient(srv.URL)
113 _, err := client.ExecuteLua(context.Background(), "return 'ok'", 60)
114 if err != nil {
115 t.Fatalf("ExecuteLua failed: %v", err)
116 }
117}
118
119func TestExecuteLuaTimeoutClamp(t *testing.T) {
120 mux := http.NewServeMux()
121 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
122 // Timeout should be clamped to 21600 even if caller passes 50000
123 timeout := r.Header.Get("X-Timeout")
124 if timeout != "21600" {
125 t.Errorf("expected X-Timeout 21600, got %s", timeout)
126 }
127 result := LuaResult{Result: json.RawMessage(`"ok"`)}
128 json.NewEncoder(w).Encode(result)
129 })
130
131 srv := newTestServer(mux)
132 defer srv.Close()
133
134 client := testClient(srv.URL)
135 _, err := client.ExecuteLua(context.Background(), "return 'ok'", 50000)
136 if err != nil {
137 t.Fatalf("ExecuteLua failed: %v", err)
138 }
139}
140
141func TestScreenshot(t *testing.T) {
142 mux := http.NewServeMux()
143 mux.HandleFunc("/.runtime/screenshot", func(w http.ResponseWriter, r *http.Request) {
144 if r.Method != http.MethodGet {
145 t.Errorf("expected GET, got %s", r.Method)
146 }
147 w.Header().Set("Content-Type", "image/png")
148 w.Write([]byte("fake-png-data"))
149 })
150
151 srv := newTestServer(mux)
152 defer srv.Close()
153
154 client := testClient(srv.URL)
155 data, err := client.Screenshot(context.Background())
156 if err != nil {
157 t.Fatalf("Screenshot failed: %v", err)
158 }
159
160 if string(data) != "fake-png-data" {
161 t.Errorf("expected fake-png-data, got %s", string(data))
162 }
163}
164
165func TestConsoleLogs(t *testing.T) {
166 mux := http.NewServeMux()
167 mux.HandleFunc("/.runtime/logs", func(w http.ResponseWriter, r *http.Request) {
168 if r.Method != http.MethodGet {
169 t.Errorf("expected GET, got %s", r.Method)
170 }
171 limit := r.URL.Query().Get("limit")
172 if limit != "5" {
173 t.Errorf("expected limit=5, got %s", limit)
174 }
175 since := r.URL.Query().Get("since")
176 if since != "1000" {
177 t.Errorf("expected since=1000, got %s", since)
178 }
179
180 logs := LogsResult{
181 Logs: []LogEntry{
182 {Level: "log", Text: "Booting", Timestamp: 1710000000000},
183 {Level: "info", Text: "Ready", Timestamp: 1710000000050},
184 },
185 }
186 json.NewEncoder(w).Encode(logs)
187 })
188
189 srv := newTestServer(mux)
190 defer srv.Close()
191
192 client := testClient(srv.URL)
193 result, err := client.ConsoleLogs(context.Background(), 5, 1000)
194 if err != nil {
195 t.Fatalf("ConsoleLogs failed: %v", err)
196 }
197
198 if len(result.Logs) != 2 {
199 t.Fatalf("expected 2 log entries, got %d", len(result.Logs))
200 }
201
202 if result.Logs[0].Text != "Booting" {
203 t.Errorf("expected first log text 'Booting', got %s", result.Logs[0].Text)
204 }
205}
206
207func TestConsoleLogsDefaultLimit(t *testing.T) {
208 mux := http.NewServeMux()
209 mux.HandleFunc("/.runtime/logs", func(w http.ResponseWriter, r *http.Request) {
210 limit := r.URL.Query().Get("limit")
211 if limit != "100" {
212 t.Errorf("expected default limit=100, got %s", limit)
213 }
214 logs := LogsResult{Logs: []LogEntry{}}
215 json.NewEncoder(w).Encode(logs)
216 })
217
218 srv := newTestServer(mux)
219 defer srv.Close()
220
221 client := testClient(srv.URL)
222 _, err := client.ConsoleLogs(context.Background(), 0, 0)
223 if err != nil {
224 t.Fatalf("ConsoleLogs failed: %v", err)
225 }
226}
227
228func TestBasicAuth(t *testing.T) {
229 mux := http.NewServeMux()
230 setupAuthServer(mux)
231 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
232 // Verify session cookie is present
233 cookie, err := r.Cookie("auth_session")
234 if err != nil {
235 t.Error("expected auth_session cookie, got none")
236 }
237 if cookie != nil && cookie.Value != "mock-jwt-token" {
238 t.Errorf("expected cookie value 'mock-jwt-token', got %s", cookie.Value)
239 }
240
241 if r.Header.Get("X-Timeout") == "" {
242 t.Error("expected X-Timeout header to be set")
243 }
244
245 result := LuaResult{Result: json.RawMessage(`"ok"`)}
246 json.NewEncoder(w).Encode(result)
247 })
248
249 srv := newTestServer(mux)
250 defer srv.Close()
251
252 client := testClientWithBasicAuth(srv.URL)
253 _, err := client.ExecuteLua(context.Background(), "return 'ok'", 30)
254 if err != nil {
255 t.Fatalf("ExecuteLua with basic auth failed: %v", err)
256 }
257}
258
259func TestBearerToken(t *testing.T) {
260 mux := http.NewServeMux()
261 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
262 auth := r.Header.Get("Authorization")
263 if auth != "Bearer testtoken123" {
264 t.Errorf("expected Bearer token, got %s", auth)
265 }
266 result := LuaResult{Result: json.RawMessage(`"ok"`)}
267 json.NewEncoder(w).Encode(result)
268 })
269
270 srv := newTestServer(mux)
271 defer srv.Close()
272
273 client := testClientWithBearerToken(srv.URL)
274 _, err := client.ExecuteLua(context.Background(), "return 'ok'", 30)
275 if err != nil {
276 t.Fatalf("ExecuteLua with bearer token failed: %v", err)
277 }
278}
279
280func TestBothAuth(t *testing.T) {
281 mux := http.NewServeMux()
282 setupAuthServer(mux)
283 mux.HandleFunc("/.runtime/lua_script", func(w http.ResponseWriter, r *http.Request) {
284 // Bearer token on Authorization header
285 auth := r.Header.Get("Authorization")
286 if auth != "Bearer testtoken123" {
287 t.Errorf("expected Bearer token on Authorization, got %s", auth)
288 }
289 // Session cookie from /.auth login
290 cookie, err := r.Cookie("auth_session")
291 if err != nil {
292 t.Error("expected auth_session cookie, got none")
293 }
294 if cookie != nil && cookie.Value != "mock-jwt-token" {
295 t.Errorf("expected cookie value 'mock-jwt-token', got %s", cookie.Value)
296 }
297 result := LuaResult{Result: json.RawMessage(`"ok"`)}
298 json.NewEncoder(w).Encode(result)
299 })
300
301 srv := newTestServer(mux)
302 defer srv.Close()
303
304 client := New(srv.URL, Auth{User: "testuser", Pass: "testpass", Token: "testtoken123"})
305 _, err := client.ExecuteLua(context.Background(), "return 'ok'", 30)
306 if err != nil {
307 t.Fatalf("ExecuteLua with both auth types failed: %v", err)
308 }
309}
310
311func TestTrailingSlashURL(t *testing.T) {
312 // Verify that trailing slashes in the base URL are handled correctly
313 client := New("http://localhost:3000/", Auth{})
314 if client.baseURL != "http://localhost:3000" {
315 t.Errorf("expected trailing slash stripped, got %s", client.baseURL)
316 }
317
318 endpoint, err := client.resolveURL("/.runtime/lua_script")
319 if err != nil {
320 t.Fatalf("resolveURL failed: %v", err)
321 }
322 if endpoint != "http://localhost:3000/.runtime/lua_script" {
323 t.Errorf("expected no double slash, got %s", endpoint)
324 }
325}