Files
kandev/apps/backend/internal/gitlab/client_helpers_test.go
T
Alassane 262ecdf270 feat(mcp): handle task change links (#3506)
Co-authored-by: Carlos Florêncio <carlosmflorencio@gmail.com>
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
2026-09-14 15:00:35 +01:00

169 lines
6.4 KiB
Go

package gitlab
import (
"encoding/json"
"net/url"
"testing"
)
func TestConvertReviewersPreservesNumericUserID(t *testing.T) {
var user rawUser
if err := json.Unmarshal([]byte(`{"id":42,"username":"alice","name":"Alice"}`), &user); err != nil {
t.Fatalf("unmarshal reviewer: %v", err)
}
reviewers := convertReviewers([]rawUser{user})
encoded, err := json.Marshal(reviewers)
if err != nil {
t.Fatalf("marshal reviewers: %v", err)
}
if got, want := string(encoded), `[{"id":42,"username":"alice","name":"Alice","type":"user"}]`; got != want {
t.Fatalf("reviewers JSON = %s, want %s", got, want)
}
}
func TestConvertRawMRHydratesLabels(t *testing.T) {
mr := convertRawMR(&rawMR{Labels: []string{"bug", "backend"}})
if got, want := mr.Labels, []string{"bug", "backend"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("labels = %#v, want %#v", got, want)
}
}
func TestConvertRawMRHydratesCommitSHAs(t *testing.T) {
raw := &rawMR{SHA: "legacy-head-sha"}
raw.DiffRefs.BaseSHA = "base-sha"
raw.DiffRefs.HeadSHA = "head-sha"
mr := convertRawMR(raw)
if got, want := mr.HeadSHA, "head-sha"; got != want {
t.Fatalf("HeadSHA = %q, want %q", got, want)
}
if got, want := mr.BaseSHA, "base-sha"; got != want {
t.Fatalf("BaseSHA = %q, want %q", got, want)
}
}
func TestConvertRawMR_DetailedMergeStatus(t *testing.T) {
// Present (GitLab 15.6+).
mr := convertRawMR(&rawMR{DetailedMergeStatus: "mergeable", BlockingDiscussionsResolved: true})
if mr.DetailedMergeStatus != "mergeable" {
t.Errorf("DetailedMergeStatus = %q, want mergeable", mr.DetailedMergeStatus)
}
if !mr.BlockingDiscussionsResolved {
t.Errorf("BlockingDiscussionsResolved = false, want true")
}
// Absent (pre-15.6 self-managed host) — must not fabricate a value.
old := convertRawMR(&rawMR{MergeStatus: "can_be_merged"})
if old.DetailedMergeStatus != "" {
t.Errorf("DetailedMergeStatus = %q, want empty when the API omits it", old.DetailedMergeStatus)
}
}
func TestCountUnapprovedReviewers(t *testing.T) {
reviewers := []MRReviewer{{Username: "alice"}, {Username: "bob"}, {Username: "carol"}}
approvals := []MRApproval{{Username: "bob"}}
if got := countUnapprovedReviewers(reviewers, approvals); got != 2 {
t.Errorf("countUnapprovedReviewers() = %d, want 2 (alice, carol)", got)
}
if got := countUnapprovedReviewers(nil, approvals); got != 0 {
t.Errorf("countUnapprovedReviewers(no reviewers) = %d, want 0", got)
}
allApproved := []MRApproval{{Username: "alice"}, {Username: "bob"}, {Username: "carol"}}
if got := countUnapprovedReviewers(reviewers, allApproved); got != 0 {
t.Errorf("countUnapprovedReviewers(all approved) = %d, want 0", got)
}
}
// User-supplied filter keys must override defaults seeded by the caller.
// The previous implementation used values.Add(), which kept the default
// alongside the user value (e.g. state=opened&state=closed) and GitLab
// silently honoured the first one.
func TestAppendFilter_UserKeysOverrideDefaults(t *testing.T) {
values := url.Values{}
values.Set("state", "opened")
values.Set("scope", "all")
appendFilter(values, "state=closed&labels=bug")
if got := values.Get("state"); got != "closed" {
t.Errorf("state = %q, want closed (user filter must override default)", got)
}
if got := values["state"]; len(got) != 1 {
t.Errorf("state values = %v, want length 1 (defaults must be cleared before re-adding)", got)
}
if got := values.Get("labels"); got != "bug" {
t.Errorf("labels = %q, want bug", got)
}
if got := values.Get("scope"); got != "all" {
t.Errorf("scope = %q, want all (untouched keys keep their default)", got)
}
}
func TestAppendFilter_MultiValueOverride(t *testing.T) {
values := url.Values{}
values.Set("labels", "default")
appendFilter(values, "labels=a&labels=b")
got := values["labels"]
if len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Errorf("labels = %v, want [a b] (default cleared then both user values appended)", got)
}
}
func TestAppendFilter_UnparseableIgnored(t *testing.T) {
values := url.Values{}
values.Set("state", "opened")
appendFilter(values, "%bad%")
if got := values.Get("state"); got != "opened" {
t.Errorf("state = %q, want opened (unparseable filter must not nuke defaults)", got)
}
}
// translateUserSearchFilter is the seam that turns the /gitlab page's tab
// values (assigned_to_me / created_by_me / review_requested) into proper
// GitLab API filter strings. Before this helper existed, the controller
// passed the bare tab tokens straight to appendFilter, which parsed them as
// keys with empty values — the filter contributed nothing and the page
// served the global, unscoped listing.
func TestTranslateUserSearchFilter(t *testing.T) {
cases := []struct {
name string
token string
username string
want string
}{
{"assigned_to_me", "assigned_to_me", "", "scope=assigned_to_me"},
{"created_by_me", "created_by_me", "", "scope=created_by_me"},
{"review_requested_with_user", "review_requested", "alice", "reviewer_username=alice&scope=all"},
// Empty username means the controller failed to resolve /user; the
// translator must refuse so the caller can surface the error instead
// of silently falling back to the global listing.
{"review_requested_without_user", "review_requested", "", ""},
// Already in key=value form — power-user passthrough must not be
// rewritten. Verified end-to-end by TestAppendFilter_*.
{"raw_key_value_passthrough", "labels=bug", "", ""},
{"raw_key_value_with_amp", "labels=bug&state=closed", "", ""},
// Unknown token — leave it to appendFilter to ignore.
{"unknown_token", "mystery", "", ""},
{"empty", "", "", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := translateUserSearchFilter(tc.token, tc.username)
if got != tc.want {
t.Errorf("translateUserSearchFilter(%q, %q) = %q, want %q",
tc.token, tc.username, got, tc.want)
}
})
}
}
// Usernames with characters that need URL escaping (rare on GitLab but
// possible on self-managed instances configured to allow them) must not
// land on the wire unescaped — the resulting query string would be
// ambiguous and could either break parsing or worse, accidentally inject
// extra params.
func TestTranslateUserSearchFilter_EscapesUsername(t *testing.T) {
got := translateUserSearchFilter("review_requested", "a&b=c")
want := "reviewer_username=a%26b%3Dc&scope=all"
if got != want {
t.Errorf("got = %q, want %q (username must be URL-escaped)", got, want)
}
}