ecda06748e
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
464 lines
19 KiB
Go
464 lines
19 KiB
Go
package gitlab
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/kandev/kandev/internal/events"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
)
|
|
|
|
func newMRAutomationControllerFixture(t *testing.T) (*gin.Engine, *Service) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
store := newTestStore(t)
|
|
seedWorkspace(t, store, "ws-1")
|
|
seedTask(t, store, "task-1", "ws-1")
|
|
// The switches are per-MR, so a PATCH only has somewhere to land once the
|
|
// task has at least one linked MR.
|
|
if err := store.UpsertTaskMR(context.Background(), newTestMR("task-1", "", "group/a", 1)); err != nil {
|
|
t.Fatalf("seed linked MR: %v", err)
|
|
}
|
|
if err := store.SaveConfigForWorkspace(context.Background(), "ws-1", &GitLabConfig{
|
|
Host: "https://gitlab.example.com", AuthMethod: AuthMethodPAT,
|
|
}); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
secrets := &configTestSecrets{values: map[string]string{SecretKeyForWorkspace("ws-1"): "token"}}
|
|
svc := newWorkspaceConfigService(t, store, secrets)
|
|
mock := NewMockClient("https://gitlab.example.com")
|
|
mock.SetUser("alice")
|
|
svc.workspaceClientFn = func(_ context.Context, _ *GitLabConfig, _ string) (Client, error) {
|
|
return mock, nil
|
|
}
|
|
router := gin.New()
|
|
controller := NewController(svc, newTestLogger(t))
|
|
controller.RegisterMRAutomationHTTPRoutes(router.Group("/api/v1/gitlab"))
|
|
return router, svc
|
|
}
|
|
|
|
func TestControllerGetTaskMRAutomation_ImplicitDefault(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/gitlab/tasks/task-1/mr-automation", nil)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("GET status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var got TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got.TaskID != "task-1" || got.PromptOnReviewRequested || got.PromptOnMerged || got.PromptOnClosed ||
|
|
got.ReviewReviewerUsername != "" || got.MRStates == nil {
|
|
t.Fatalf("unexpected implicit default response: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_SetsSingleSwitch(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
body := `{"prompt_on_merged":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("PATCH status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var got TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !got.PromptOnMerged || got.PromptOnReviewRequested || got.PromptOnClosed {
|
|
t.Fatalf("unexpected response: %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_AutoFixEnabled covers AC2: PATCH with
|
|
// auto_fix_enabled returns 200 with it true, and a subsequent GET agrees.
|
|
func TestControllerPatchTaskMRAutomation_AutoFixEnabled(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
body := `{"auto_fix_enabled":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("PATCH status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var patched TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &patched); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !patched.AutoFixEnabled {
|
|
t.Fatalf("auto_fix_enabled = false after PATCH, want true: %+v", patched)
|
|
}
|
|
|
|
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/gitlab/tasks/task-1/mr-automation", nil)
|
|
getResp := httptest.NewRecorder()
|
|
router.ServeHTTP(getResp, getReq)
|
|
var got TaskMRAutomationResponse
|
|
if err := json.Unmarshal(getResp.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode GET: %v", err)
|
|
}
|
|
if !got.AutoFixEnabled {
|
|
t.Fatalf("GET auto_fix_enabled = false, want true: %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_RejectsUnknownAutomationField and
|
|
// TestControllerPatchTaskMRAutomation_RejectsNullAutoFixSwitch extend AC3's
|
|
// controller contract (inherited from #2125) to the new fields.
|
|
func TestControllerPatchTaskMRAutomation_RejectsUnknownAutomationField(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(`{"auto_fx_enabled":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "unknown MR automation field") {
|
|
t.Fatalf("unexpected error body: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsNullAutoFixSwitch(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(`{"auto_fix_enabled":null}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestControllerGetTaskMRAutomation_DefaultPromptFields covers AC4: the
|
|
// response always carries auto_fix_max_rounds, a non-empty
|
|
// effective_auto_fix_prompt, and using_default_prompt=true with no override.
|
|
func TestControllerGetTaskMRAutomation_DefaultPromptFields(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/gitlab/tasks/task-1/mr-automation", nil)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
var got TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got.AutoFixMaxRounds != TaskMRAutoFixMaxRounds {
|
|
t.Errorf("AutoFixMaxRounds = %d, want %d", got.AutoFixMaxRounds, TaskMRAutoFixMaxRounds)
|
|
}
|
|
if got.EffectiveAutoFixPrompt == "" {
|
|
t.Error("EffectiveAutoFixPrompt is empty, want the embedded default")
|
|
}
|
|
if !got.UsingDefaultPrompt {
|
|
t.Error("UsingDefaultPrompt = false, want true when no override is set")
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_PromptOverrideRoundTrip covers AC5.
|
|
func TestControllerPatchTaskMRAutomation_PromptOverrideRoundTrip(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
setOverride := `{"auto_fix_prompt_override":"custom prompt text"}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(setOverride))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
var withOverride TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &withOverride); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if withOverride.EffectiveAutoFixPrompt != "custom prompt text" {
|
|
t.Errorf("EffectiveAutoFixPrompt = %q, want the override", withOverride.EffectiveAutoFixPrompt)
|
|
}
|
|
if withOverride.UsingDefaultPrompt {
|
|
t.Error("UsingDefaultPrompt = true with an override set, want false")
|
|
}
|
|
|
|
clearOverride := `{"auto_fix_prompt_override":""}`
|
|
req2 := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(clearOverride))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
resp2 := httptest.NewRecorder()
|
|
router.ServeHTTP(resp2, req2)
|
|
var cleared TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp2.Body.Bytes(), &cleared); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !cleared.UsingDefaultPrompt {
|
|
t.Error("UsingDefaultPrompt = false after clearing the override, want true")
|
|
}
|
|
if cleared.EffectiveAutoFixPrompt == "custom prompt text" {
|
|
t.Error("EffectiveAutoFixPrompt still the override after clearing it")
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_EmptyBodyRejected(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(`{}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "at least one MR automation option is required") {
|
|
t.Fatalf("unexpected error body: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsTrailingContent(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
body := `{"prompt_on_merged":true}{"prompt_on_merged":false}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsUnknownField(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
body := `{"promt_on_merged":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsUnknownFieldAlongsideValidOne(t *testing.T) {
|
|
router, svc := newMRAutomationControllerFixture(t)
|
|
// A typo'd field must not be silently dropped while a valid field in the
|
|
// same body still applies — reject the whole request instead.
|
|
body := `{"promt_on_merged":true,"prompt_on_closed":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
got, err := svc.GetTaskMRAutomationResponse(context.Background(), "task-1")
|
|
if err != nil {
|
|
t.Fatalf("get options: %v", err)
|
|
}
|
|
if got.PromptOnClosed {
|
|
t.Fatalf("expected no field applied from a rejected patch, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsNullSwitch(t *testing.T) {
|
|
router, svc := newMRAutomationControllerFixture(t)
|
|
body := `{"prompt_on_merged":null,"prompt_on_closed":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
got, err := svc.GetTaskMRAutomationResponse(context.Background(), "task-1")
|
|
if err != nil {
|
|
t.Fatalf("get options: %v", err)
|
|
}
|
|
if got.PromptOnClosed {
|
|
t.Fatalf("expected no field applied from a rejected patch, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestControllerGetTaskMRAutomation_UnknownTaskNotFound(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/gitlab/tasks/does-not-exist/mr-automation", nil)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_UnknownTaskNotFound(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/does-not-exist/mr-automation",
|
|
strings.NewReader(`{"prompt_on_merged":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_RejectsLifecycleOverrides(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
for _, key := range []string{"review_prompt_override", "merged_prompt_override", "closed_prompt_override"} {
|
|
body := `{"` + key + `":"custom"}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("%s: status = %d, body = %s", key, resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "lifecycle prompt overrides are not supported") {
|
|
t.Fatalf("%s: unexpected error body: %s", key, resp.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_ResolvesAndClearsReviewer(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation",
|
|
strings.NewReader(`{"prompt_on_review_requested":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("enable status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var enabled TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &enabled); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if enabled.ReviewReviewerUsername != "alice" {
|
|
t.Fatalf("expected resolved reviewer, got %+v", enabled)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation",
|
|
strings.NewReader(`{"prompt_on_review_requested":false}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp = httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("disable status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var disabled TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &disabled); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if disabled.ReviewReviewerUsername != "" {
|
|
t.Fatalf("expected cleared reviewer, got %+v", disabled)
|
|
}
|
|
}
|
|
|
|
func TestControllerPatchTaskMRAutomation_PublishesEvent(t *testing.T) {
|
|
router, svc := newMRAutomationControllerFixture(t)
|
|
memBus := bus.NewMemoryEventBus(newTestLogger(t))
|
|
svc.SetEventBus(memBus)
|
|
|
|
received := make(chan *bus.Event, 1)
|
|
if _, err := memBus.Subscribe(events.GitLabTaskMROptionsUpdated, func(_ context.Context, e *bus.Event) error {
|
|
received <- e
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("subscribe: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation",
|
|
strings.NewReader(`{"prompt_on_closed":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
select {
|
|
case e := <-received:
|
|
payload, ok := e.Data.(*TaskMRAutomationResponse)
|
|
if !ok || payload.TaskID != "task-1" || !payload.PromptOnClosed {
|
|
t.Fatalf("unexpected event payload: %+v", e.Data)
|
|
}
|
|
default:
|
|
t.Fatal("expected GitLabTaskMROptionsUpdated event to be published")
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_ScopesToTheNamedMR covers the per-MR
|
|
// PATCH contract: a body carrying repository_id/project_path/mr_iid applies
|
|
// the switch to that MR alone.
|
|
func TestControllerPatchTaskMRAutomation_ScopesToTheNamedMR(t *testing.T) {
|
|
router, svc := newMRAutomationControllerFixture(t)
|
|
if err := svc.store.UpsertTaskMR(context.Background(), newTestMR("task-1", "", "group/b", 2)); err != nil {
|
|
t.Fatalf("seed second MR: %v", err)
|
|
}
|
|
|
|
body := `{"repository_id":"","project_path":"group/a","mr_iid":1,"auto_merge_enabled":true}`
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("PATCH status = %d, body = %s", resp.Code, resp.Body.String())
|
|
}
|
|
var got TaskMRAutomationResponse
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(got.MROptions) != 2 {
|
|
t.Fatalf("expected one mr_options entry per linked MR, got %+v", got.MROptions)
|
|
}
|
|
for _, opt := range got.MROptions {
|
|
wantEnabled := opt.ProjectPath == "group/a"
|
|
if opt.AutoMergeEnabled != wantEnabled {
|
|
t.Errorf("MR %s auto_merge_enabled = %v, want %v", opt.ProjectPath, opt.AutoMergeEnabled, wantEnabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_RejectsBadMRIdentity keeps a caller
|
|
// mistake a 400 rather than a 500 — and, for a partial identity, keeps it
|
|
// from being silently reinterpreted as "apply to every linked MR".
|
|
func TestControllerPatchTaskMRAutomation_RejectsBadMRIdentity(t *testing.T) {
|
|
router, _ := newMRAutomationControllerFixture(t)
|
|
for name, body := range map[string]string{
|
|
"unlinked MR": `{"repository_id":"","project_path":"group/nope","mr_iid":9,"auto_fix_enabled":true}`,
|
|
"partial identity": `{"project_path":"group/a","auto_fix_enabled":true}`,
|
|
"null identity": `{"repository_id":null,"project_path":null,"mr_iid":null,"auto_fix_enabled":true}`,
|
|
"identity only": `{"repository_id":"","project_path":"group/a","mr_iid":1}`,
|
|
} {
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-1/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Errorf("%s: status = %d, want 400 (body = %s)", name, resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestControllerPatchTaskMRAutomation_RejectsSwitchesWithNoLinkedMRs is the
|
|
// HTTP half of the zero-target rule: because the switches only exist per MR,
|
|
// a task with none has nowhere to store them, and answering 200 would report
|
|
// a write that never happened. The task-level prompt override stays accepted.
|
|
func TestControllerPatchTaskMRAutomation_RejectsSwitchesWithNoLinkedMRs(t *testing.T) {
|
|
router, svc := newMRAutomationControllerFixture(t)
|
|
seedTask(t, svc.store, "task-2", "ws-1")
|
|
|
|
patch := func(body string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/gitlab/tasks/task-2/mr-automation", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
return resp
|
|
}
|
|
|
|
if resp := patch(`{"auto_merge_enabled":true}`); resp.Code != http.StatusBadRequest {
|
|
t.Errorf("switch patch status = %d, want 400 (body = %s)", resp.Code, resp.Body.String())
|
|
}
|
|
if resp := patch(`{"auto_fix_prompt_override":"custom"}`); resp.Code != http.StatusOK {
|
|
t.Errorf("prompt override status = %d, want 200 (body = %s)", resp.Code, resp.Body.String())
|
|
}
|
|
}
|