| 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) |
| } |
| } |
|
|