mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
5e028a847c
A standalone web-based tool for managing picoclaw configuration, OAuth authentication providers, and gateway process lifecycle. Features include a sidebar layout with i18n (en/zh) and theme support, real-time gateway log streaming, startup prerequisites checks, and Windows icon embedding. Co-authored-by: wj-xiao <meetwenjie@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
248 lines
6.9 KiB
Go
248 lines
6.9 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
// ── Config API tests ─────────────────────────────────────────────
|
|
|
|
func setupConfigMux(t *testing.T, cfg *config.Config) (*http.ServeMux, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("marshal config: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterConfigAPI(mux, path)
|
|
RegisterAuthAPI(mux, path)
|
|
return mux, path
|
|
}
|
|
|
|
func TestGetConfig(t *testing.T) {
|
|
cfg := &config.Config{
|
|
ModelList: []config.ModelConfig{
|
|
{ModelName: "gpt-4o", Model: "openai/gpt-4o"},
|
|
},
|
|
}
|
|
mux, path := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("GET", "/api/config", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET /api/config: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Config config.Config `json:"config"`
|
|
Path string `json:"path"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
|
|
if resp.Path != path {
|
|
t.Errorf("expected path %q, got %q", path, resp.Path)
|
|
}
|
|
if len(resp.Config.ModelList) != 1 {
|
|
t.Errorf("expected 1 model, got %d", len(resp.Config.ModelList))
|
|
}
|
|
}
|
|
|
|
func TestGetConfig_MissingFile_ReturnsDefault(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterConfigAPI(mux, "/tmp/nonexistent-picoclaw-launcher-test/config.json")
|
|
|
|
req := httptest.NewRequest("GET", "/api/config", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// LoadConfig returns a default empty config when file is missing
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200 for missing file (default config), got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPutConfig(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, path := setupConfigMux(t, cfg)
|
|
|
|
newCfg := config.Config{
|
|
ModelList: []config.ModelConfig{
|
|
{ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(newCfg)
|
|
|
|
req := httptest.NewRequest("PUT", "/api/config", strings.NewReader(string(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("PUT /api/config: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
saved, err := config.LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("load saved config: %v", err)
|
|
}
|
|
if len(saved.ModelList) != 1 {
|
|
t.Fatalf("expected 1 model saved, got %d", len(saved.ModelList))
|
|
}
|
|
if saved.ModelList[0].Model != "anthropic/claude-sonnet-4.6" {
|
|
t.Errorf("expected model anthropic/claude-sonnet-4.6, got %q", saved.ModelList[0].Model)
|
|
}
|
|
}
|
|
|
|
func TestPutConfig_InvalidJSON(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("PUT", "/api/config", strings.NewReader("{invalid"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for invalid JSON, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── Auth API tests ───────────────────────────────────────────────
|
|
|
|
func TestAuthStatus(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("GET", "/api/auth/status", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET /api/auth/status: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Providers []providerStatus `json:"providers"`
|
|
PendingDevice map[string]any `json:"pending_device"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
|
|
// providers should be a non-nil list (could be empty)
|
|
if resp.Providers == nil {
|
|
t.Error("providers should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestAuthLogin_UnsupportedProvider(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
body := `{"provider": "unsupported"}`
|
|
req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for unsupported provider, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthLogin_AnthropicNoToken(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
body := `{"provider": "anthropic"}`
|
|
req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for anthropic without token, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthLogin_InvalidBody(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader("{bad"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for invalid JSON body, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthLogout_InvalidBody(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/logout", strings.NewReader("{bad"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for invalid body, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestOAuthCallback_InvalidState(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
mux, _ := setupConfigMux(t, cfg)
|
|
|
|
req := httptest.NewRequest("GET", "/auth/callback?state=invalid&code=test", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for invalid state, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── Utility tests ────────────────────────────────────────────────
|
|
|
|
func TestDefaultConfigPath(t *testing.T) {
|
|
path := DefaultConfigPath()
|
|
if path == "" {
|
|
t.Error("defaultConfigPath should not return empty")
|
|
}
|
|
if !strings.HasSuffix(path, filepath.Join(".picoclaw", "config.json")) {
|
|
t.Errorf("expected path ending with .picoclaw/config.json, got %q", path)
|
|
}
|
|
}
|
|
|
|
func TestGetLocalIP(t *testing.T) {
|
|
// Just ensure it doesn't panic; IP may or may not be available
|
|
ip := GetLocalIP()
|
|
if ip != "" {
|
|
// If returned, should look like an IP
|
|
if !strings.Contains(ip, ".") {
|
|
t.Errorf("getLocalIP returned non-IPv4 looking string: %q", ip)
|
|
}
|
|
}
|
|
}
|