client_test.go

  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}