mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
1d9a20bd60
* detect remote stack from current branch * clean up phrasing
202 lines
6.1 KiB
Go
202 lines
6.1 KiB
Go
package github
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/cli/go-gh/v2/pkg/api"
|
|
graphql "github.com/cli/shurcooL-graphql"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPRURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
owner string
|
|
repo string
|
|
number int
|
|
want string
|
|
}{
|
|
{"github.com", "github.com", "owner", "repo", 42, "https://github.com/owner/repo/pull/42"},
|
|
{"GHES host", "ghes.example.com", "myorg", "myrepo", 99, "https://ghes.example.com/myorg/myrepo/pull/99"},
|
|
{"empty host defaults to github.com", "", "owner", "repo", 1, "https://github.com/owner/repo/pull/1"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := PRURL(tt.host, tt.owner, tt.repo, tt.number)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPullRequest_IsQueued(t *testing.T) {
|
|
t.Run("not queued when MergeQueueEntry is nil", func(t *testing.T) {
|
|
pr := &PullRequest{Number: 1}
|
|
assert.False(t, pr.IsQueued())
|
|
})
|
|
|
|
t.Run("queued when MergeQueueEntry has ID", func(t *testing.T) {
|
|
pr := &PullRequest{
|
|
Number: 1,
|
|
MergeQueueEntry: &MergeQueueEntry{ID: "MQE_123"},
|
|
}
|
|
assert.True(t, pr.IsQueued())
|
|
})
|
|
|
|
t.Run("nil receiver is safe", func(t *testing.T) {
|
|
var pr *PullRequest
|
|
assert.False(t, pr.IsQueued())
|
|
})
|
|
}
|
|
|
|
func TestPullRequest_IsAutoMergeEnabled(t *testing.T) {
|
|
t.Run("not enabled when AutoMergeRequest is nil", func(t *testing.T) {
|
|
pr := &PullRequest{Number: 1}
|
|
assert.False(t, pr.IsAutoMergeEnabled())
|
|
})
|
|
|
|
t.Run("enabled when AutoMergeRequest is present", func(t *testing.T) {
|
|
pr := &PullRequest{
|
|
Number: 1,
|
|
AutoMergeRequest: &AutoMergeRequest{EnabledAt: "2024-01-01T00:00:00Z"},
|
|
}
|
|
assert.True(t, pr.IsAutoMergeEnabled())
|
|
})
|
|
|
|
t.Run("nil receiver is safe", func(t *testing.T) {
|
|
var pr *PullRequest
|
|
assert.False(t, pr.IsAutoMergeEnabled())
|
|
})
|
|
}
|
|
|
|
func TestToGraphQLInt(t *testing.T) {
|
|
t.Run("in range", func(t *testing.T) {
|
|
got, err := toGraphQLInt(123)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, graphql.Int(123), got)
|
|
})
|
|
|
|
t.Run("out of range", func(t *testing.T) {
|
|
_, err := toGraphQLInt(1 << 40)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestRemoteStack_UnmarshalJSON verifies the custom decoder that flattens the
|
|
// Stacks REST API's pull_requests object array into ordered PullRequests
|
|
// numbers while preserving the full entries in PRDetails. This is the only path
|
|
// that populates those fields from the wire, so a shape regression here would
|
|
// silently break every stack endpoint.
|
|
func TestRemoteStack_UnmarshalJSON(t *testing.T) {
|
|
payload := `{
|
|
"id": 6154,
|
|
"number": 360,
|
|
"node_id": "S_kwABCD",
|
|
"url": "https://api.github.com/repos/o/r/stacks/360",
|
|
"base": {"ref": "main", "sha": "basesha"},
|
|
"open": true,
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"pull_requests": [
|
|
{"number": 12, "state": "open", "draft": true, "merged_at": null, "head": {"ref": "feat-1", "sha": "sha1"}},
|
|
{"number": 15, "state": "closed", "draft": false, "merged_at": "2026-01-02T00:00:00Z", "head": {"ref": "feat-2", "sha": "sha2"}}
|
|
]
|
|
}`
|
|
|
|
var s RemoteStack
|
|
require.NoError(t, json.Unmarshal([]byte(payload), &s))
|
|
|
|
// Top-level metadata.
|
|
assert.Equal(t, 6154, s.ID)
|
|
assert.Equal(t, 360, s.Number)
|
|
assert.Equal(t, "S_kwABCD", s.NodeID)
|
|
assert.Equal(t, "https://api.github.com/repos/o/r/stacks/360", s.URL)
|
|
assert.Equal(t, "main", s.Base.Ref)
|
|
assert.Equal(t, "basesha", s.Base.Sha)
|
|
assert.True(t, s.Open)
|
|
assert.Equal(t, "2026-01-01T00:00:00Z", s.CreatedAt)
|
|
|
|
// Ordered PR numbers (bottom to top) derived from the object array.
|
|
assert.Equal(t, []int{12, 15}, s.PullRequests)
|
|
assert.Equal(t, []int{12, 15}, s.PRNumbers())
|
|
|
|
// Full PR entries preserved in PRDetails, including nullable merged_at.
|
|
require.Len(t, s.PRDetails, 2)
|
|
assert.Equal(t, 12, s.PRDetails[0].Number)
|
|
assert.Equal(t, "open", s.PRDetails[0].State)
|
|
assert.True(t, s.PRDetails[0].Draft)
|
|
assert.Nil(t, s.PRDetails[0].MergedAt)
|
|
assert.False(t, s.PRDetails[0].IsMerged())
|
|
assert.Equal(t, "feat-1", s.PRDetails[0].Head.Ref)
|
|
assert.Equal(t, "sha1", s.PRDetails[0].Head.Sha)
|
|
|
|
assert.Equal(t, 15, s.PRDetails[1].Number)
|
|
assert.Equal(t, "closed", s.PRDetails[1].State)
|
|
require.NotNil(t, s.PRDetails[1].MergedAt)
|
|
assert.Equal(t, "2026-01-02T00:00:00Z", *s.PRDetails[1].MergedAt)
|
|
assert.True(t, s.PRDetails[1].IsMerged())
|
|
assert.Equal(t, "feat-2", s.PRDetails[1].Head.Ref)
|
|
}
|
|
|
|
// TestRemoteStack_UnmarshalJSON_EmptyPRs ensures a stack with no pull_requests
|
|
// decodes to empty (non-nil) slices rather than panicking.
|
|
func TestRemoteStack_UnmarshalJSON_EmptyPRs(t *testing.T) {
|
|
var s RemoteStack
|
|
require.NoError(t, json.Unmarshal([]byte(`{"id": 1, "number": 2, "pull_requests": []}`), &s))
|
|
assert.Equal(t, 1, s.ID)
|
|
assert.Equal(t, 2, s.Number)
|
|
assert.Empty(t, s.PullRequests)
|
|
assert.Empty(t, s.PRDetails)
|
|
}
|
|
|
|
func TestListStacks_Paginates(t *testing.T) {
|
|
firstPage := make([]RemoteStack, 100)
|
|
for i := range firstPage {
|
|
firstPage[i] = RemoteStack{ID: i + 1, Number: 101 - i}
|
|
}
|
|
secondPage := []RemoteStack{{ID: 101, Number: 1}}
|
|
firstBody, err := json.Marshal(firstPage)
|
|
require.NoError(t, err)
|
|
secondBody, err := json.Marshal(secondPage)
|
|
require.NoError(t, err)
|
|
|
|
var requestedPages []int
|
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
assert.Equal(t, "100", r.URL.Query().Get("per_page"))
|
|
page, parseErr := strconv.Atoi(r.URL.Query().Get("page"))
|
|
require.NoError(t, parseErr)
|
|
requestedPages = append(requestedPages, page)
|
|
|
|
body := firstBody
|
|
if page == 2 {
|
|
body = secondBody
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(body)),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
Request: r,
|
|
}, nil
|
|
})
|
|
rest, err := api.NewRESTClient(api.ClientOptions{
|
|
Host: "github.com",
|
|
AuthToken: "x",
|
|
Transport: transport,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
client := &Client{rest: rest, owner: "o", repo: "r"}
|
|
stacks, err := client.ListStacks()
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, stacks, 101)
|
|
assert.Equal(t, []int{1, 2}, requestedPages)
|
|
assert.Equal(t, 101, stacks[0].Number)
|
|
assert.Equal(t, 1, stacks[100].Number)
|
|
}
|