Pulse/internal/api/profile_suggestions_handlers_test.go
rcourtman fd108faa7d feat(profiles): de-emphasize AI suggestions and fix multi-tenant support
UI/UX Improvements for AI-skeptical users:
- Only show 'Ideas' button if AI is enabled AND configured
- Renamed 'Suggest Profile' to 'Ideas' with lightbulb icon
- Moved 'New Profile' button to primary position
- Changed AI button styling from prominent purple to subtle gray
- Updated modal title to 'Profile Ideas' with neutral language

Multi-tenant bug fix:
- ProfileSuggestionHandler now uses MultiTenantPersistence
- Properly resolves tenant-specific persistence from request context
- Fixes potential nil pointer panic in multi-tenant deployments
- Existing profiles are now correctly loaded per-tenant for AI context

Tests updated to use MultiTenantPersistence with org context injection.
2026-02-04 11:39:50 +00:00

127 lines
4.6 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/chat"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/stretchr/testify/mock"
)
// withOrgContext adds the default org ID to the request context
func withOrgContext(req *http.Request) *http.Request {
ctx := context.WithValue(req.Context(), OrgIDContextKey, "default")
return req.WithContext(ctx)
}
func TestProfileSuggestionHandler_MethodNotAllowed(t *testing.T) {
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), &AIHandler{})
req := withOrgContext(httptest.NewRequest(http.MethodGet, "/api/admin/profiles/suggestions", nil))
rr := httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rr.Code)
}
}
func TestProfileSuggestionHandler_ServiceUnavailable(t *testing.T) {
mockSvc := new(MockAIService)
mockSvc.On("IsRunning").Return(false)
aiHandler := &AIHandler{legacyService: mockSvc}
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), aiHandler)
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"test"}`))))
rr := httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rr.Code)
}
}
func TestProfileSuggestionHandler_InvalidRequest(t *testing.T) {
mockSvc := new(MockAIService)
mockSvc.On("IsRunning").Return(true)
aiHandler := &AIHandler{legacyService: mockSvc}
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), aiHandler)
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte("{bad"))))
rr := httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code)
}
req = withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":" "}`))))
rr = httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code)
}
}
func TestProfileSuggestionHandler_SuccessAndParseFailure(t *testing.T) {
mtPersistence := config.NewMultiTenantPersistence(t.TempDir())
// Get the default tenant's persistence to save test data
persistence, err := mtPersistence.GetPersistence("default")
if err != nil {
t.Fatalf("get persistence: %v", err)
}
if err := persistence.SaveAgentProfiles([]models.AgentProfile{{Name: "Existing"}}); err != nil {
t.Fatalf("save profiles: %v", err)
}
mockSvc := new(MockAIService)
mockSvc.On("IsRunning").Return(true)
mockSvc.On("Execute", mock.Anything, mock.Anything).Return(map[string]interface{}{
"content": `{"name":"Suggested","description":"desc","config":{"interval":"30s"},"rationale":["reason"]}`,
}, nil).Once()
aiHandler := &AIHandler{legacyService: mockSvc}
handler := NewProfileSuggestionHandler(mtPersistence, aiHandler)
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"build a profile"}`))))
rr := httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
}
var resp ProfileSuggestion
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Name != "Suggested" {
t.Fatalf("unexpected name: %q", resp.Name)
}
if resp.Config["interval"] == "" {
t.Fatalf("expected config in response")
}
mockSvc.On("Execute", mock.Anything, mock.Anything).Return(map[string]interface{}{
"content": "not json",
}, nil).Once()
req = withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"bad response"}`))))
rr = httptest.NewRecorder()
handler.HandleSuggestProfile(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Fatalf("expected status %d, got %d", http.StatusInternalServerError, rr.Code)
}
}
// Ensure MockAIService implements the interface methods used by the handler.
var _ interface {
IsRunning() bool
Execute(ctx context.Context, req chat.ExecuteRequest) (map[string]interface{}, error)
} = (*MockAIService)(nil)