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