Add API infrastructure for issue types, sub-issues, and issue relationships (blocked-by/blocking): - New types: IssueType, LinkedIssue, SubIssues, SubIssuesSummary, LinkedIssueConnection - New Issue struct fields for all Issues 2.0 data - GraphQL query builder cases for new fields - ExportData cases for JSON output - Mutation functions: UpdateIssueIssueType, AddSubIssue, RemoveSubIssue, AddBlockedBy, RemoveBlockedBy - Helper functions: RepoIssueTypes, IssueNodeID - Feature detection: IssueRelationshipsSupported for GHES 3.19+ (issue types and sub-issues are GA on GHES 3.17+, no detection needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
853 lines
22 KiB
Go
853 lines
22 KiB
Go
package featuredetection
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/pkg/httpmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIssueFeatures(t *testing.T) {
|
|
issueFieldsWithRelationships := `{"data":{"Issue":{"fields":[{"name":"title"},{"name":"body"},{"name":"blockedBy"}]}}}`
|
|
issueFieldsWithoutRelationships := `{"data":{"Issue":{"fields":[{"name":"title"},{"name":"body"}]}}}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
queryResponse map[string]string
|
|
wantFeatures IssueFeatures
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "github.com",
|
|
hostname: "github.com",
|
|
wantFeatures: IssueFeatures{
|
|
ApiActorsSupported: true,
|
|
IssueRelationshipsSupported: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com)",
|
|
hostname: "stampname.ghe.com",
|
|
wantFeatures: IssueFeatures{
|
|
ApiActorsSupported: true,
|
|
IssueRelationshipsSupported: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE with relationship support",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Issue_fields`: issueFieldsWithRelationships,
|
|
},
|
|
wantFeatures: IssueFeatures{
|
|
ApiActorsSupported: false,
|
|
IssueRelationshipsSupported: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE without relationship support",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Issue_fields`: issueFieldsWithoutRelationships,
|
|
},
|
|
wantFeatures: IssueFeatures{
|
|
ApiActorsSupported: false,
|
|
IssueRelationshipsSupported: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
defer reg.Verify(t)
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
for query, resp := range tt.queryResponse {
|
|
reg.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp))
|
|
}
|
|
detector := detector{host: tt.hostname, httpClient: httpClient}
|
|
gotFeatures, err := detector.IssueFeatures()
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantFeatures, gotFeatures)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPullRequestFeatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
queryResponse map[string]string
|
|
wantFeatures PullRequestFeatures
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "github.com with all features",
|
|
hostname: "github.com",
|
|
queryResponse: map[string]string{
|
|
`query PullRequest_fields\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"PullRequest": {
|
|
"fields": [
|
|
{"name": "isInMergeQueue"},
|
|
{"name": "isMergeQueueEnabled"}
|
|
]
|
|
},
|
|
"StatusCheckRollupContextConnection": {
|
|
"fields": [
|
|
{"name": "checkRunCount"},
|
|
{"name": "checkRunCountsByState"},
|
|
{"name": "statusContextCount"},
|
|
{"name": "statusContextCountsByState"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
`query PullRequest_fields2\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"WorkflowRun": {
|
|
"fields": [
|
|
{"name": "event"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
},
|
|
wantFeatures: PullRequestFeatures{
|
|
MergeQueue: true,
|
|
CheckRunAndStatusContextCounts: true,
|
|
CheckRunEvent: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "github.com with no merge queue",
|
|
hostname: "github.com",
|
|
queryResponse: map[string]string{
|
|
`query PullRequest_fields\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"PullRequest": {
|
|
"fields": []
|
|
},
|
|
"StatusCheckRollupContextConnection": {
|
|
"fields": [
|
|
{"name": "checkRunCount"},
|
|
{"name": "checkRunCountsByState"},
|
|
{"name": "statusContextCount"},
|
|
{"name": "statusContextCountsByState"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
`query PullRequest_fields2\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"WorkflowRun": {
|
|
"fields": [
|
|
{"name": "event"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
},
|
|
wantFeatures: PullRequestFeatures{
|
|
MergeQueue: false,
|
|
CheckRunAndStatusContextCounts: true,
|
|
CheckRunEvent: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE with all features",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query PullRequest_fields\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"PullRequest": {
|
|
"fields": [
|
|
{"name": "isInMergeQueue"},
|
|
{"name": "isMergeQueueEnabled"}
|
|
]
|
|
},
|
|
"StatusCheckRollupContextConnection": {
|
|
"fields": [
|
|
{"name": "checkRunCount"},
|
|
{"name": "checkRunCountsByState"},
|
|
{"name": "statusContextCount"},
|
|
{"name": "statusContextCountsByState"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
`query PullRequest_fields2\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"WorkflowRun": {
|
|
"fields": [
|
|
{"name": "event"}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
},
|
|
wantFeatures: PullRequestFeatures{
|
|
MergeQueue: true,
|
|
CheckRunAndStatusContextCounts: true,
|
|
CheckRunEvent: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE with no features",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query PullRequest_fields\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"PullRequest": {
|
|
"fields": []
|
|
},
|
|
"StatusCheckRollupContextConnection": {
|
|
"fields": []
|
|
}
|
|
}
|
|
}`),
|
|
`query PullRequest_fields2\b`: heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"WorkflowRun": {
|
|
"fields": []
|
|
}
|
|
}
|
|
}`),
|
|
},
|
|
wantFeatures: PullRequestFeatures{
|
|
MergeQueue: false,
|
|
CheckRunAndStatusContextCounts: false,
|
|
CheckRunEvent: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
for query, resp := range tt.queryResponse {
|
|
reg.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp))
|
|
}
|
|
detector := detector{host: tt.hostname, httpClient: httpClient}
|
|
gotFeatures, err := detector.PullRequestFeatures()
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantFeatures, gotFeatures)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepositoryFeatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
queryResponse map[string]string
|
|
wantFeatures RepositoryFeatures
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "github.com",
|
|
hostname: "github.com",
|
|
wantFeatures: RepositoryFeatures{
|
|
PullRequestTemplateQuery: true,
|
|
VisibilityField: true,
|
|
AutoMerge: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com)",
|
|
hostname: "stampname.ghe.com",
|
|
wantFeatures: RepositoryFeatures{
|
|
PullRequestTemplateQuery: true,
|
|
VisibilityField: true,
|
|
AutoMerge: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE empty response",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Repository_fields\b`: `{"data": {}}`,
|
|
},
|
|
wantFeatures: RepositoryFeatures{
|
|
PullRequestTemplateQuery: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE has pull request template query",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Repository_fields\b`: heredoc.Doc(`
|
|
{ "data": { "Repository": { "fields": [
|
|
{"name": "pullRequestTemplates"}
|
|
] } } }
|
|
`),
|
|
},
|
|
wantFeatures: RepositoryFeatures{
|
|
PullRequestTemplateQuery: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE has visibility field",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Repository_fields\b`: heredoc.Doc(`
|
|
{ "data": { "Repository": { "fields": [
|
|
{"name": "visibility"}
|
|
] } } }
|
|
`),
|
|
},
|
|
wantFeatures: RepositoryFeatures{
|
|
VisibilityField: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "GHE has automerge field",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query Repository_fields\b`: heredoc.Doc(`
|
|
{ "data": { "Repository": { "fields": [
|
|
{"name": "autoMergeAllowed"}
|
|
] } } }
|
|
`),
|
|
},
|
|
wantFeatures: RepositoryFeatures{
|
|
AutoMerge: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
for query, resp := range tt.queryResponse {
|
|
reg.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp))
|
|
}
|
|
detector := detector{host: tt.hostname, httpClient: httpClient}
|
|
gotFeatures, err := detector.RepositoryFeatures()
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantFeatures, gotFeatures)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProjectV1Support(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
httpStubs func(*httpmock.Registry)
|
|
wantFeatures gh.ProjectsV1Support
|
|
}{
|
|
{
|
|
name: "github.com",
|
|
hostname: "github.com",
|
|
wantFeatures: gh.ProjectsV1Unsupported,
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com)",
|
|
hostname: "stampname.ghe.com",
|
|
wantFeatures: gh.ProjectsV1Unsupported,
|
|
},
|
|
{
|
|
name: "GHE 3.16.0",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.16.0"}`),
|
|
)
|
|
},
|
|
wantFeatures: gh.ProjectsV1Supported,
|
|
},
|
|
{
|
|
name: "GHE 3.16.1",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.16.1"}`),
|
|
)
|
|
},
|
|
wantFeatures: gh.ProjectsV1Supported,
|
|
},
|
|
{
|
|
name: "GHE 3.17",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.17.0"}`),
|
|
)
|
|
},
|
|
wantFeatures: gh.ProjectsV1Unsupported,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
reg := &httpmock.Registry{}
|
|
if tt.httpStubs != nil {
|
|
tt.httpStubs(reg)
|
|
}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
|
|
detector := NewDetector(httpClient, tt.hostname)
|
|
require.Equal(t, tt.wantFeatures, detector.ProjectsV1())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvancedIssueSearchSupport(t *testing.T) {
|
|
withIssueAdvanced := `{"data":{"SearchType":{"enumValues":[{"name":"ISSUE"},{"name":"ISSUE_ADVANCED"},{"name":"REPOSITORY"},{"name":"USER"},{"name":"DISCUSSION"}]}}}`
|
|
withoutIssueAdvanced := `{"data":{"SearchType":{"enumValues":[{"name":"ISSUE"},{"name":"REPOSITORY"},{"name":"USER"},{"name":"DISCUSSION"}]}}}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
httpStubs func(*httpmock.Registry)
|
|
wantFeatures SearchFeatures
|
|
}{
|
|
{
|
|
name: "github.com, before ISSUE_ADVANCED cleanup",
|
|
hostname: "github.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOptIn,
|
|
},
|
|
{
|
|
name: "github.com, after ISSUE_ADVANCED cleanup",
|
|
hostname: "github.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withoutIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com), before ISSUE_ADVANCED cleanup",
|
|
hostname: "stampname.ghe.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOptIn,
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com), after ISSUE_ADVANCED cleanup",
|
|
hostname: "stampname.ghe.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withoutIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
|
|
},
|
|
{
|
|
name: "GHE 3.18, before ISSUE_ADVANCED cleanup",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.18.0"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOptIn,
|
|
},
|
|
{
|
|
name: "GHE 3.18, after ISSUE_ADVANCED cleanup",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.18.0"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withoutIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
|
|
},
|
|
{
|
|
name: "GHE >3.18, before ISSUE_ADVANCED cleanup",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.18.1"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOptIn,
|
|
},
|
|
{
|
|
name: "GHE >3.18, after ISSUE_ADVANCED cleanup",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.18.1"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query SearchType_enumValues\b`),
|
|
httpmock.StringResponse(withoutIssueAdvanced),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
|
|
},
|
|
{
|
|
name: "GHE <3.18 (no advanced issue search support)",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.17.999"}`),
|
|
)
|
|
},
|
|
wantFeatures: advancedIssueSearchNotSupported,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
reg := &httpmock.Registry{}
|
|
if tt.httpStubs != nil {
|
|
tt.httpStubs(reg)
|
|
}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
|
|
detector := NewDetector(httpClient, tt.hostname)
|
|
|
|
features, err := detector.SearchFeatures()
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.wantFeatures, features)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProjectFeatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
queryResponse map[string]string
|
|
wantFeatures ProjectFeatures
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "github.com",
|
|
hostname: "github.com",
|
|
wantFeatures: ProjectFeatures{
|
|
ProjectItemQuery: true,
|
|
},
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com)",
|
|
hostname: "stampname.ghe.com",
|
|
wantFeatures: ProjectFeatures{
|
|
ProjectItemQuery: true,
|
|
},
|
|
},
|
|
{
|
|
name: "GHE empty response",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query ProjectV2_fields\b`: `{"data": {}}`,
|
|
},
|
|
wantFeatures: ProjectFeatures{},
|
|
},
|
|
{
|
|
name: "GHE items field without query arg",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query ProjectV2_fields\b`: heredoc.Doc(`
|
|
{ "data": { "ProjectV2": { "fields": [
|
|
{"name": "items", "args": [
|
|
{"name": "after"},
|
|
{"name": "first"}
|
|
]}
|
|
] } } }
|
|
`),
|
|
},
|
|
wantFeatures: ProjectFeatures{},
|
|
},
|
|
{
|
|
name: "GHE items field with query arg",
|
|
hostname: "git.my.org",
|
|
queryResponse: map[string]string{
|
|
`query ProjectV2_fields\b`: heredoc.Doc(`
|
|
{ "data": { "ProjectV2": { "fields": [
|
|
{"name": "items", "args": [
|
|
{"name": "after"},
|
|
{"name": "first"},
|
|
{"name": "query"}
|
|
]}
|
|
] } } }
|
|
`),
|
|
},
|
|
wantFeatures: ProjectFeatures{
|
|
ProjectItemQuery: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
defer reg.Verify(t)
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
for query, resp := range tt.queryResponse {
|
|
reg.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp))
|
|
}
|
|
detector := detector{host: tt.hostname, httpClient: httpClient}
|
|
gotFeatures, err := detector.ProjectFeatures()
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantFeatures, gotFeatures)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReleaseFeatures(t *testing.T) {
|
|
withImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"},{"name":"immutable"}]}}}`
|
|
withoutImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"}]}}}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
httpStubs func(*httpmock.Registry)
|
|
wantFeatures ReleaseFeatures
|
|
}{
|
|
{
|
|
// This is not a real case as `github.com` supports immutable releases.
|
|
name: "github.com, immutable releases unsupported",
|
|
hostname: "github.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withoutImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: false,
|
|
},
|
|
},
|
|
{
|
|
name: "github.com, immutable releases supported",
|
|
hostname: "github.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: true,
|
|
},
|
|
},
|
|
{
|
|
// This is not a real case as `github.com` supports immutable releases.
|
|
name: "ghec data residency (ghe.com), immutable releases unsupported",
|
|
hostname: "stampname.ghe.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withoutImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: false,
|
|
},
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com), immutable releases supported",
|
|
hostname: "stampname.ghe.com",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: true,
|
|
},
|
|
},
|
|
{
|
|
name: "GHE, immutable releases unsupported",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withoutImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: false,
|
|
},
|
|
},
|
|
{
|
|
name: "GHE, immutable releases supported",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query Release_fields\b`),
|
|
httpmock.StringResponse(withImmutableReleaseSupport),
|
|
)
|
|
},
|
|
wantFeatures: ReleaseFeatures{
|
|
ImmutableReleases: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
reg := &httpmock.Registry{}
|
|
if tt.httpStubs != nil {
|
|
tt.httpStubs(reg)
|
|
}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
|
|
detector := NewDetector(httpClient, tt.hostname)
|
|
|
|
features, err := detector.ReleaseFeatures()
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.wantFeatures, features)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActionsFeatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
httpStubs func(*httpmock.Registry)
|
|
wantFeatures ActionsFeatures
|
|
}{
|
|
{
|
|
name: "github.com, workflow dispatch run details supported",
|
|
hostname: "github.com",
|
|
wantFeatures: ActionsFeatures{
|
|
DispatchRunDetails: true,
|
|
},
|
|
},
|
|
{
|
|
name: "ghec data residency (ghe.com), workflow dispatch run details supported",
|
|
hostname: "stampname.ghe.com",
|
|
wantFeatures: ActionsFeatures{
|
|
DispatchRunDetails: true,
|
|
},
|
|
},
|
|
{
|
|
name: "GHE 3.20, workflow dispatch run details not supported",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.20.999"}`),
|
|
)
|
|
},
|
|
wantFeatures: ActionsFeatures{
|
|
DispatchRunDetails: false,
|
|
},
|
|
},
|
|
{
|
|
name: "GHE 3.21, workflow dispatch run details supported",
|
|
hostname: "git.my.org",
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "api/v3/meta"),
|
|
httpmock.StringResponse(`{"installed_version":"3.21.0"}`),
|
|
)
|
|
},
|
|
wantFeatures: ActionsFeatures{
|
|
DispatchRunDetails: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
reg := &httpmock.Registry{}
|
|
if tt.httpStubs != nil {
|
|
tt.httpStubs(reg)
|
|
}
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
|
|
detector := NewDetector(httpClient, tt.hostname)
|
|
|
|
features, err := detector.ActionsFeatures()
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.wantFeatures, features)
|
|
})
|
|
}
|
|
}
|