package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/kindredsystems/silo/internal/auth" "github.com/kindredsystems/silo/internal/db" "github.com/kindredsystems/silo/internal/modules" "github.com/kindredsystems/silo/internal/schema" "github.com/kindredsystems/silo/internal/testutil" "github.com/rs/zerolog" ) // newAuthTestServer creates a Server with a real auth service (for token tests). func newAuthTestServer(t *testing.T) *Server { t.Helper() pool := testutil.MustConnectTestPool(t) database := db.NewFromPool(pool) users := db.NewUserRepository(database) tokens := db.NewTokenRepository(database) authSvc := auth.NewService(zerolog.Nop(), users, tokens) broker := NewBroker(zerolog.Nop()) state := NewServerState(zerolog.Nop(), nil, broker) return NewServer( zerolog.Nop(), database, map[string]*schema.Schema{}, "", // schemasDir nil, // storage authSvc, // authService nil, // sessionManager nil, // oidcBackend nil, // authConfig broker, state, nil, // jobDefs "", // jobDefsDir modules.NewRegistry(), // modules nil, // cfg nil, // workflows ) } // ensureTestUser creates a user in the DB and returns their ID. func ensureTestUser(t *testing.T, s *Server, username string) string { t.Helper() u := &db.User{ Username: username, DisplayName: "Test " + username, Email: username + "@test.local", AuthSource: "local", Role: "admin", } users := db.NewUserRepository(s.db) if err := users.Upsert(context.Background(), u); err != nil { t.Fatalf("upserting user: %v", err) } return u.ID } func newAuthRouter(s *Server) http.Handler { r := chi.NewRouter() r.Get("/api/auth/me", s.HandleGetCurrentUser) r.Post("/api/auth/tokens", s.HandleCreateToken) r.Get("/api/auth/tokens", s.HandleListTokens) r.Delete("/api/auth/tokens/{id}", s.HandleRevokeToken) r.Get("/api/auth/config", s.HandleAuthConfig) return r } func TestHandleGetCurrentUser(t *testing.T) { s := newTestServer(t) router := newAuthRouter(s) req := authRequest(httptest.NewRequest("GET", "/api/auth/me", nil)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decoding response: %v", err) } if resp["username"] != "testadmin" { t.Errorf("username: got %v, want %q", resp["username"], "testadmin") } if resp["role"] != "admin" { t.Errorf("role: got %v, want %q", resp["role"], "admin") } } func TestHandleGetCurrentUserUnauth(t *testing.T) { s := newTestServer(t) router := newAuthRouter(s) // No auth context req := httptest.NewRequest("GET", "/api/auth/me", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status: got %d, want %d", w.Code, http.StatusUnauthorized) } } func TestHandleAuthConfig(t *testing.T) { s := newTestServer(t) router := newAuthRouter(s) req := httptest.NewRequest("GET", "/api/auth/config", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decoding response: %v", err) } // With nil oidc and nil authConfig, both should be false if resp["oidc_enabled"] != false { t.Errorf("oidc_enabled: got %v, want false", resp["oidc_enabled"]) } } func TestHandleCreateAndListTokens(t *testing.T) { s := newAuthTestServer(t) router := newAuthRouter(s) // Create a user in the DB so token generation can associate userID := ensureTestUser(t, s, "tokenuser") // Inject user with the DB-assigned ID u := &auth.User{ ID: userID, Username: "tokenuser", DisplayName: "Test tokenuser", Role: auth.RoleAdmin, AuthSource: "local", } // Create token body := `{"name":"test-token"}` req := httptest.NewRequest("POST", "/api/auth/tokens", strings.NewReader(body)) req = req.WithContext(auth.ContextWithUser(req.Context(), u)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create token status: got %d, want %d; body: %s", w.Code, http.StatusCreated, w.Body.String()) } var createResp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &createResp); err != nil { t.Fatalf("decoding create response: %v", err) } if createResp["token"] == nil || createResp["token"] == "" { t.Error("expected token in response") } tokenID, _ := createResp["id"].(string) // List tokens req = httptest.NewRequest("GET", "/api/auth/tokens", nil) req = req.WithContext(auth.ContextWithUser(req.Context(), u)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list tokens status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var tokens []map[string]any if err := json.Unmarshal(w.Body.Bytes(), &tokens); err != nil { t.Fatalf("decoding list response: %v", err) } if len(tokens) != 1 { t.Errorf("expected 1 token, got %d", len(tokens)) } // Revoke token req = httptest.NewRequest("DELETE", "/api/auth/tokens/"+tokenID, nil) req = req.WithContext(auth.ContextWithUser(req.Context(), u)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Errorf("revoke token status: got %d, want %d; body: %s", w.Code, http.StatusNoContent, w.Body.String()) } // List again — should be empty req = httptest.NewRequest("GET", "/api/auth/tokens", nil) req = req.WithContext(auth.ContextWithUser(req.Context(), u)) w = httptest.NewRecorder() router.ServeHTTP(w, req) json.Unmarshal(w.Body.Bytes(), &tokens) if len(tokens) != 0 { t.Errorf("expected 0 tokens after revoke, got %d", len(tokens)) } }