// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package task import ( "io" "net/http" "net/http/httptest" "testing" "gitea.dev/tea/modules/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestListPullStacks(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks", r.URL.Path) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`[{"id":1,"repo_id":2,"owner_id":3,"title":"feat: base","base_branch":"main","state":"open","auto_restack":true,"entries":[{"pull_id":10,"parent_pull_id":0,"position":0,"state":"open","last_restacked_sha":"abc"}]}]`)) })) defer server.Close() stacks, err := ListPullStacks(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo") require.NoError(t, err) require.Len(t, stacks, 1) assert.Equal(t, int64(1), stacks[0].ID) assert.Equal(t, "feat: base", stacks[0].Title) assert.Equal(t, "main", stacks[0].BaseBranch) assert.True(t, stacks[0].AutoRestack) require.Len(t, stacks[0].Entries, 1) assert.Equal(t, int64(10), stacks[0].Entries[0].PullID) } func TestGetPullStack(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks/7", r.URL.Path) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":7,"repo_id":2,"owner_id":3,"title":"feat: base","base_branch":"main","state":"open","auto_restack":false,"entries":[]}`)) })) defer server.Close() stack, err := GetPullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 7) require.NoError(t, err) assert.Equal(t, int64(7), stack.ID) assert.Equal(t, "open", stack.State) } func TestGetPullStackNotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message":"stack does not exist"}`)) })) defer server.Close() _, err := GetPullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 7) require.Error(t, err) assert.Equal(t, "stack does not exist", err.Error()) } func TestCreatePullStack(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks", r.URL.Path) body, err := io.ReadAll(r.Body) require.NoError(t, err) assert.JSONEq(t, `{"parent_index":1,"child_index":2}`, string(body)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(`{"id":9,"repo_id":2,"owner_id":3,"title":"feat: base","base_branch":"main","state":"open","auto_restack":true,"entries":[]}`)) })) defer server.Close() stack, err := CreatePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 1, 2) require.NoError(t, err) assert.Equal(t, int64(9), stack.ID) } func TestCreatePullStackConflict(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message":"parent_index and child_index are required"}`)) })) defer server.Close() _, err := CreatePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 0, 2) require.Error(t, err) assert.Equal(t, "parent_index and child_index are required", err.Error()) } func TestAddPullStackEntry(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks/5/entries", r.URL.Path) body, err := io.ReadAll(r.Body) require.NoError(t, err) assert.JSONEq(t, `{"parent_index":2,"child_index":3}`, string(body)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(`{"id":5,"repo_id":2,"owner_id":3,"title":"feat: base","base_branch":"main","state":"open","auto_restack":true,"entries":[]}`)) })) defer server.Close() stack, err := AddPullStackEntry(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 5, 2, 3) require.NoError(t, err) assert.Equal(t, int64(5), stack.ID) } func TestRestackPullStack(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks/4/restack", r.URL.Path) w.WriteHeader(http.StatusAccepted) })) defer server.Close() err := RestackPullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 4) require.NoError(t, err) } func TestRestackPullStackConflict(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusConflict) _, _ = w.Write([]byte(`{"message":"stack is not open"}`)) })) defer server.Close() err := RestackPullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 4) require.Error(t, err) assert.Equal(t, "stack is not open", err.Error()) } func TestMergePullStack(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "/api/v1/repos/owner/repo/stacks/6/merge", r.URL.Path) body, err := io.ReadAll(r.Body) require.NoError(t, err) assert.JSONEq(t, `{"style":"squash","wait_for_ci":false}`, string(body)) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"merged":3}`)) })) defer server.Close() result, queued, err := MergePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 6, "squash", false) require.NoError(t, err) assert.False(t, queued) require.NotNil(t, result) assert.Equal(t, int64(3), result.Merged) } func TestMergePullStackQueued(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) require.NoError(t, err) assert.JSONEq(t, `{"style":"merge","wait_for_ci":true}`, string(body)) w.WriteHeader(http.StatusAccepted) })) defer server.Close() result, queued, err := MergePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 6, "merge", true) require.NoError(t, err) assert.True(t, queued) assert.Nil(t, result) } func TestMergePullStackPartialFailure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) _, _ = w.Write([]byte(`{"merged":1,"error":"PR #4 is not mergeable"}`)) })) defer server.Close() result, queued, err := MergePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 6, "merge", false) require.Error(t, err) assert.Equal(t, "PR #4 is not mergeable", err.Error()) assert.False(t, queued) require.NotNil(t, result) assert.Equal(t, int64(1), result.Merged) } func TestMergePullStackPermissionDenied(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"message":"merge not permitted"}`)) })) defer server.Close() result, queued, err := MergePullStack(t.Context(), &config.Login{ Name: "test", URL: server.URL, Token: "secret-token", }, "owner", "repo", 6, "merge", false) require.ErrorContains(t, err, "merge not permitted") assert.False(t, queued) assert.Nil(t, result) }