package handler import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" db "github.com/multica-ai/multica/server/pkg/db/generated" ) func TestSubscriberAPI(t *testing.T) { ctx := context.Background() // Helper: create an issue for subscriber tests createIssue := func(t *testing.T) string { t.Helper() w := httptest.NewRecorder() req := newRequest("POST", "/api/issues?workspace_id="+testWorkspaceID, map[string]any{ "title": "Subscriber test issue", }) testHandler.CreateIssue(w, req) if w.Code != http.StatusCreated { t.Fatalf("CreateIssue: expected 201, got %d: %s", w.Code, w.Body.String()) } var issue IssueResponse json.NewDecoder(w.Body).Decode(&issue) return issue.ID } // Helper: delete an issue deleteIssue := func(t *testing.T, issueID string) { t.Helper() w := httptest.NewRecorder() req := newRequest("DELETE", "/api/issues/"+issueID, nil) req = withURLParam(req, "id", issueID) testHandler.DeleteIssue(w, req) } t.Run("Subscribe", func(t *testing.T) { issueID := createIssue(t) defer deleteIssue(t, issueID) w := httptest.NewRecorder() req := newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("SubscribeToIssue: expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]bool json.NewDecoder(w.Body).Decode(&resp) if !resp["subscribed"] { t.Fatal("SubscribeToIssue: expected subscribed=true") } // Verify in DB subscribed, err := testHandler.Queries.IsIssueSubscriber(ctx, db.IsIssueSubscriberParams{ IssueID: parseUUID(issueID), UserType: "member", UserID: parseUUID(testUserID), }) if err != nil { t.Fatalf("IsIssueSubscriber: %v", err) } if !subscribed { t.Fatal("expected user to be subscribed in DB") } }) t.Run("SubscribeIdempotent", func(t *testing.T) { issueID := createIssue(t) defer deleteIssue(t, issueID) // Subscribe first time w := httptest.NewRecorder() req := newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("SubscribeToIssue (1st): expected 200, got %d: %s", w.Code, w.Body.String()) } // Subscribe second time — should also succeed w = httptest.NewRecorder() req = newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("SubscribeToIssue (2nd): expected 200, got %d: %s", w.Code, w.Body.String()) } }) t.Run("ListSubscribers", func(t *testing.T) { issueID := createIssue(t) defer deleteIssue(t, issueID) // Subscribe first w := httptest.NewRecorder() req := newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("SubscribeToIssue: expected 200, got %d: %s", w.Code, w.Body.String()) } // List w = httptest.NewRecorder() req = newRequest("GET", "/api/issues/"+issueID+"/subscribers", nil) req = withURLParam(req, "id", issueID) testHandler.ListIssueSubscribers(w, req) if w.Code != http.StatusOK { t.Fatalf("ListIssueSubscribers: expected 200, got %d: %s", w.Code, w.Body.String()) } var subscribers []SubscriberResponse json.NewDecoder(w.Body).Decode(&subscribers) if len(subscribers) == 0 { t.Fatal("ListIssueSubscribers: expected at least 1 subscriber") } found := false for _, s := range subscribers { if s.UserID == testUserID && s.UserType == "member" && s.Reason == "manual" { found = true break } } if !found { t.Fatalf("ListIssueSubscribers: expected to find test user subscriber, got %+v", subscribers) } }) t.Run("Unsubscribe", func(t *testing.T) { issueID := createIssue(t) defer deleteIssue(t, issueID) // Subscribe first w := httptest.NewRecorder() req := newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("SubscribeToIssue: expected 200, got %d: %s", w.Code, w.Body.String()) } // Unsubscribe w = httptest.NewRecorder() req = newRequest("POST", "/api/issues/"+issueID+"/unsubscribe", nil) req = withURLParam(req, "id", issueID) testHandler.UnsubscribeFromIssue(w, req) if w.Code != http.StatusOK { t.Fatalf("UnsubscribeFromIssue: expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]bool json.NewDecoder(w.Body).Decode(&resp) if resp["subscribed"] { t.Fatal("UnsubscribeFromIssue: expected subscribed=false") } // Verify in DB subscribed, err := testHandler.Queries.IsIssueSubscriber(ctx, db.IsIssueSubscriberParams{ IssueID: parseUUID(issueID), UserType: "member", UserID: parseUUID(testUserID), }) if err != nil { t.Fatalf("IsIssueSubscriber: %v", err) } if subscribed { t.Fatal("expected user to NOT be subscribed in DB") } }) t.Run("ListAfterUnsubscribe", func(t *testing.T) { issueID := createIssue(t) defer deleteIssue(t, issueID) // Subscribe w := httptest.NewRecorder() req := newRequest("POST", "/api/issues/"+issueID+"/subscribe", nil) req = withURLParam(req, "id", issueID) testHandler.SubscribeToIssue(w, req) // Unsubscribe w = httptest.NewRecorder() req = newRequest("POST", "/api/issues/"+issueID+"/unsubscribe", nil) req = withURLParam(req, "id", issueID) testHandler.UnsubscribeFromIssue(w, req) // List should be empty w = httptest.NewRecorder() req = newRequest("GET", "/api/issues/"+issueID+"/subscribers", nil) req = withURLParam(req, "id", issueID) testHandler.ListIssueSubscribers(w, req) if w.Code != http.StatusOK { t.Fatalf("ListIssueSubscribers: expected 200, got %d: %s", w.Code, w.Body.String()) } var subscribers []SubscriberResponse json.NewDecoder(w.Body).Decode(&subscribers) if len(subscribers) != 0 { t.Fatalf("ListIssueSubscribers: expected 0 subscribers after unsubscribe, got %d", len(subscribers)) } }) }