mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 10:10:49 +00:00
28fab9507a
- Backend: depth-limited WalkDir (max 3 levels default) with on-demand subtree loading
- Backend: new GET /v1/storage/size SSE endpoint with 60min in-memory cache
- Backend: raw binary file serving (?raw=true) with MIME detection and download support
- Frontend: lazy tree expansion with loading spinners for deep folders
- Frontend: streaming size display with cache info tooltip
- Frontend: image viewer (blob URL), unsupported file UI, download button, colored size badges
- Frontend: file-type icons for 13 categories (md, json, yaml, images, video, etc.)
- Fix sidebar connection status text overflow on collapse
- Apply go fix modernization (interface{} → any) across http handlers
241 lines
5.8 KiB
Go
241 lines
5.8 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// --- mock stores for tests ---
|
|
|
|
type mockProviderStore struct {
|
|
providers map[string]*store.LLMProviderData
|
|
}
|
|
|
|
func newMockProviderStore() *mockProviderStore {
|
|
return &mockProviderStore{providers: make(map[string]*store.LLMProviderData)}
|
|
}
|
|
|
|
func (m *mockProviderStore) CreateProvider(_ context.Context, p *store.LLMProviderData) error {
|
|
if p.ID == uuid.Nil {
|
|
p.ID = uuid.New()
|
|
}
|
|
m.providers[p.Name] = p
|
|
return nil
|
|
}
|
|
|
|
func (m *mockProviderStore) GetProvider(_ context.Context, id uuid.UUID) (*store.LLMProviderData, error) {
|
|
for _, p := range m.providers {
|
|
if p.ID == id {
|
|
return p, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("not found")
|
|
}
|
|
|
|
func (m *mockProviderStore) GetProviderByName(_ context.Context, name string) (*store.LLMProviderData, error) {
|
|
if p, ok := m.providers[name]; ok {
|
|
return p, nil
|
|
}
|
|
return nil, fmt.Errorf("not found")
|
|
}
|
|
|
|
func (m *mockProviderStore) ListProviders(_ context.Context) ([]store.LLMProviderData, error) {
|
|
var out []store.LLMProviderData
|
|
for _, p := range m.providers {
|
|
out = append(out, *p)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *mockProviderStore) UpdateProvider(_ context.Context, id uuid.UUID, updates map[string]any) error {
|
|
for _, p := range m.providers {
|
|
if p.ID == id {
|
|
if v, ok := updates["api_key"]; ok {
|
|
p.APIKey = v.(string)
|
|
}
|
|
if v, ok := updates["settings"]; ok {
|
|
p.Settings = v.(json.RawMessage)
|
|
}
|
|
if v, ok := updates["enabled"]; ok {
|
|
p.Enabled = v.(bool)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("not found")
|
|
}
|
|
|
|
func (m *mockProviderStore) DeleteProvider(_ context.Context, id uuid.UUID) error {
|
|
for name, p := range m.providers {
|
|
if p.ID == id {
|
|
delete(m.providers, name)
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("not found")
|
|
}
|
|
|
|
type mockSecretsStore struct {
|
|
data map[string]string
|
|
}
|
|
|
|
func newMockSecretsStore() *mockSecretsStore {
|
|
return &mockSecretsStore{data: make(map[string]string)}
|
|
}
|
|
|
|
func (m *mockSecretsStore) Get(_ context.Context, key string) (string, error) {
|
|
if v, ok := m.data[key]; ok {
|
|
return v, nil
|
|
}
|
|
return "", fmt.Errorf("not found: %s", key)
|
|
}
|
|
|
|
func (m *mockSecretsStore) Set(_ context.Context, key, value string) error {
|
|
m.data[key] = value
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSecretsStore) Delete(_ context.Context, key string) error {
|
|
delete(m.data, key)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSecretsStore) GetAll(_ context.Context) (map[string]string, error) {
|
|
return m.data, nil
|
|
}
|
|
|
|
// --- helper ---
|
|
|
|
func newTestOAuthHandler(token string) *OAuthHandler {
|
|
return NewOAuthHandler(token, newMockProviderStore(), newMockSecretsStore(), nil, nil)
|
|
}
|
|
|
|
// --- tests ---
|
|
|
|
func TestOAuthHandlerStatusNoToken(t *testing.T) {
|
|
h := newTestOAuthHandler("")
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
req := httptest.NewRequest("GET", "/v1/auth/openai/status", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status code = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var result map[string]any
|
|
json.NewDecoder(w.Body).Decode(&result)
|
|
|
|
if result["authenticated"] != false {
|
|
t.Errorf("authenticated = %v, want false", result["authenticated"])
|
|
}
|
|
}
|
|
|
|
func TestOAuthHandlerAuth(t *testing.T) {
|
|
h := newTestOAuthHandler("secret-token")
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
// Without token - should be unauthorized
|
|
req := httptest.NewRequest("GET", "/v1/auth/openai/status", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status code without token = %d, want %d", w.Code, http.StatusUnauthorized)
|
|
}
|
|
|
|
// With correct token - should work
|
|
req2 := httptest.NewRequest("GET", "/v1/auth/openai/status", nil)
|
|
req2.Header.Set("Authorization", "Bearer secret-token")
|
|
w2 := httptest.NewRecorder()
|
|
mux.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("status code with token = %d, want %d", w2.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestOAuthHandlerLogoutNoProvider(t *testing.T) {
|
|
h := newTestOAuthHandler("")
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
req := httptest.NewRequest("POST", "/v1/auth/openai/logout", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status code = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var result map[string]string
|
|
json.NewDecoder(w.Body).Decode(&result)
|
|
|
|
if result["status"] != "logged out" {
|
|
t.Errorf("status = %q, want 'logged out'", result["status"])
|
|
}
|
|
}
|
|
|
|
func TestOAuthHandlerRouteRegistration(t *testing.T) {
|
|
h := newTestOAuthHandler("")
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{"GET", "/v1/auth/openai/status"},
|
|
{"POST", "/v1/auth/openai/logout"},
|
|
{"POST", "/v1/auth/openai/start"},
|
|
}
|
|
|
|
for _, r := range routes {
|
|
req := httptest.NewRequest(r.method, r.path, nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
if w.Code == http.StatusNotFound {
|
|
t.Errorf("%s %s returned 404", r.method, r.path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOAuthHandlerStartReturnsAuthURL(t *testing.T) {
|
|
h := newTestOAuthHandler("")
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
req := httptest.NewRequest("POST", "/v1/auth/openai/start", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Skip if port 1455 is already in use (environment issue, not code bug)
|
|
if w.Code == http.StatusInternalServerError {
|
|
t.Skip("port 1455 unavailable, skipping")
|
|
}
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status code = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
|
|
var result map[string]any
|
|
json.NewDecoder(w.Body).Decode(&result)
|
|
|
|
_, hasURL := result["auth_url"]
|
|
_, hasStatus := result["status"]
|
|
|
|
if !hasURL && !hasStatus {
|
|
t.Fatal("response has neither auth_url nor status")
|
|
}
|
|
}
|