client_test.go

  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}