package api import ( "context" "errors" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/google/uuid" "project/internal/auth" "project/internal/models" ) type analysisServiceStub struct { append func(ctx context.Context, analysisID uuid.UUID, userID, role, content string, messageID uuid.UUID) (*models.ChatMessage, error) } func (s analysisServiceStub) CreateAnalysis(ctx context.Context, title, objective, userID string, businessQuestions models.BusinessQuestionList, dataBind models.DataBindList) (*models.Analysis, error) { return nil, nil } func (s analysisServiceStub) GetAnalysis(ctx context.Context, id uuid.UUID, userID string) (*models.Analysis, error) { return nil, nil } func (s analysisServiceStub) ListAnalyses(ctx context.Context, userID, status string, limit, offset int) ([]*models.Analysis, error) { return nil, nil } func (s analysisServiceStub) UpdateAnalysis(ctx context.Context, id uuid.UUID, userID, title, objective, status string) (*models.Analysis, error) { return nil, nil } func (s analysisServiceStub) DeleteAnalysis(ctx context.Context, id uuid.UUID, userID string) error { return nil } func (s analysisServiceStub) UpdateDataBind(ctx context.Context, id uuid.UUID, userID string, expectedVersion int, dataBind models.DataBindList) (*models.Analysis, error) { return nil, nil } func (s analysisServiceStub) ListMessages(ctx context.Context, analysisID uuid.UUID, userID string, limit, offset int) ([]*models.ChatMessage, error) { return nil, nil } func (s analysisServiceStub) AppendMessage(ctx context.Context, analysisID uuid.UUID, userID, role, content string, messageID uuid.UUID) (*models.ChatMessage, error) { if s.append != nil { return s.append(ctx, analysisID, userID, role, content, messageID) } return models.NewChatMessage(analysisID, userID, role, content, messageID), nil } func (s analysisServiceStub) GenerateReport(ctx context.Context, analysisID uuid.UUID, userID string) (*models.Report, error) { return nil, nil } func (s analysisServiceStub) ListReports(ctx context.Context, analysisID uuid.UUID, userID string, limit, offset int) ([]*models.Report, error) { return nil, nil } func TestSendMessageRequiresAuthenticatedUser(t *testing.T) { h := NewAnalysisHandler(analysisServiceStub{}, slog.New(slog.NewTextHandler(io.Discard, nil))) req := httptest.NewRequest(http.MethodPost, "/api/v1/analyses/"+uuid.NewString()+"/messages", strings.NewReader(`{"role":"user","content":"hello"}`)) w := httptest.NewRecorder() h.SendMessage(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want %d; body=%s", w.Code, http.StatusUnauthorized, w.Body.String()) } } func TestSendMessageRejectsInvalidRole(t *testing.T) { analysisID := uuid.New() h := NewAnalysisHandler(analysisServiceStub{append: func(ctx context.Context, analysisID uuid.UUID, userID, role, content string, messageID uuid.UUID) (*models.ChatMessage, error) { return nil, models.ErrMessageRoleInvalid }}, slog.New(slog.NewTextHandler(io.Discard, nil))) req := httptest.NewRequest(http.MethodPost, "/api/v1/analyses/"+analysisID.String()+"/messages", strings.NewReader(`{"role":"agent","content":"hello"}`)) req.SetPathValue("id", analysisID.String()) req = req.WithContext(auth.WithUser(req.Context(), auth.ContextUser{ID: "user-1"})) w := httptest.NewRecorder() h.SendMessage(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d; body=%s", w.Code, http.StatusBadRequest, w.Body.String()) } if !strings.Contains(w.Body.String(), "role must be") { t.Fatalf("body missing role validation message: %s", w.Body.String()) } } func TestSendMessageReturnsSingleMessageEnvelope(t *testing.T) { analysisID := uuid.New() var capturedUserID string h := NewAnalysisHandler(analysisServiceStub{append: func(ctx context.Context, id uuid.UUID, userID, role, content string, messageID uuid.UUID) (*models.ChatMessage, error) { if id != analysisID { return nil, errors.New("unexpected analysis id") } capturedUserID = userID return models.NewChatMessage(id, userID, role, content, messageID), nil }}, slog.New(slog.NewTextHandler(io.Discard, nil))) req := httptest.NewRequest(http.MethodPost, "/api/v1/analyses/"+analysisID.String()+"/messages", strings.NewReader(`{"role":"ai","content":"answer","message_id":"`+uuid.NewString()+`"}`)) req.SetPathValue("id", analysisID.String()) req = req.WithContext(auth.WithUser(req.Context(), auth.ContextUser{ID: "user-1"})) w := httptest.NewRecorder() h.SendMessage(w, req) if w.Code != http.StatusCreated { t.Fatalf("status = %d, want %d; body=%s", w.Code, http.StatusCreated, w.Body.String()) } if capturedUserID != "user-1" { t.Fatalf("captured user id = %q, want token user", capturedUserID) } body := w.Body.String() if !strings.Contains(body, `"message"`) || strings.Contains(body, `"messages"`) { t.Fatalf("response should contain singular data.message only: %s", body) } if !strings.Contains(body, `"role":"ai"`) || !strings.Contains(body, `"content":"answer"`) { t.Fatalf("response missing message role/content: %s", body) } }