chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/github/github-mcp-server/internal/githubv4mock"
|
||||
"github.com/github/github-mcp-server/pkg/translations"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ProjectsWrite_CreateProject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
toolDef := ProjectsWrite(translations.NullTranslationHelper)
|
||||
|
||||
t.Run("success user project", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockedClient := githubv4mock.NewMockedHTTPClient(
|
||||
githubv4mock.NewQueryMatcher(
|
||||
struct {
|
||||
User struct {
|
||||
ID string
|
||||
} `graphql:"user(login: $login)"`
|
||||
}{},
|
||||
map[string]any{
|
||||
"login": githubv4.String("octocat"),
|
||||
},
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "U_octocat",
|
||||
},
|
||||
}),
|
||||
),
|
||||
githubv4mock.NewMutationMatcher(
|
||||
struct {
|
||||
CreateProjectV2 struct {
|
||||
ProjectV2 struct {
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
}
|
||||
} `graphql:"createProjectV2(input: $input)"`
|
||||
}{},
|
||||
githubv4.CreateProjectV2Input{
|
||||
OwnerID: githubv4.ID("U_octocat"),
|
||||
Title: githubv4.String("New Project"),
|
||||
},
|
||||
nil,
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"createProjectV2": map[string]any{
|
||||
"projectV2": map[string]any{
|
||||
"id": "PVT_project123",
|
||||
"number": 1,
|
||||
"title": "New Project",
|
||||
"url": "https://github.com/users/octocat/projects/1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
deps := BaseDeps{
|
||||
GQLClient: githubv4.NewClient(mockedClient),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_project",
|
||||
"owner": "octocat",
|
||||
"owner_type": "user",
|
||||
"title": "New Project",
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PVT_project123", response["id"])
|
||||
assert.Equal(t, float64(1), response["number"])
|
||||
assert.Equal(t, "New Project", response["title"])
|
||||
assert.Equal(t, "https://github.com/users/octocat/projects/1", response["url"])
|
||||
})
|
||||
|
||||
t.Run("missing owner_type returns error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := BaseDeps{
|
||||
GQLClient: githubv4.NewClient(githubv4mock.NewMockedHTTPClient()),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_project",
|
||||
"owner": "octocat",
|
||||
"title": "New Project",
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
assert.Contains(t, textContent.Text, "owner_type is required")
|
||||
})
|
||||
|
||||
t.Run("invalid owner_type returns error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := BaseDeps{
|
||||
GQLClient: githubv4.NewClient(githubv4mock.NewMockedHTTPClient()),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_project",
|
||||
"owner": "octocat",
|
||||
"owner_type": "invalid",
|
||||
"title": "New Project",
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
assert.Contains(t, textContent.Text, "invalid owner_type")
|
||||
assert.Contains(t, textContent.Text, "must be")
|
||||
})
|
||||
}
|
||||
|
||||
// resolveProjectNodeIDOrgMatcher returns a GraphQL query matcher for resolving
|
||||
// an org project node ID via resolveProjectNodeID.
|
||||
func resolveProjectNodeIDOrgMatcher(owner string, projectNumber int, nodeID string) githubv4mock.Matcher {
|
||||
return githubv4mock.NewQueryMatcher(
|
||||
struct {
|
||||
Organization struct {
|
||||
ProjectV2 struct {
|
||||
ID githubv4.ID
|
||||
} `graphql:"projectV2(number: $projectNumber)"`
|
||||
} `graphql:"organization(login: $owner)"`
|
||||
}{},
|
||||
map[string]any{
|
||||
"owner": githubv4.String(owner),
|
||||
"projectNumber": githubv4.Int(int32(projectNumber)), //nolint:gosec // test constant
|
||||
},
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"organization": map[string]any{
|
||||
"projectV2": map[string]any{
|
||||
"id": nodeID,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func createFieldMatcher() githubv4mock.Matcher {
|
||||
return githubv4mock.NewMutationMatcher(
|
||||
struct {
|
||||
CreateProjectV2Field struct {
|
||||
ProjectV2Field struct {
|
||||
ProjectV2IterationField struct {
|
||||
ID string
|
||||
Name string
|
||||
} `graphql:"... on ProjectV2IterationField"`
|
||||
} `graphql:"projectV2Field"`
|
||||
} `graphql:"createProjectV2Field(input: $input)"`
|
||||
}{},
|
||||
githubv4.CreateProjectV2FieldInput{
|
||||
ProjectID: githubv4.ID("PVT_project1"),
|
||||
DataType: githubv4.ProjectV2CustomFieldType("ITERATION"),
|
||||
Name: githubv4.String("Sprint"),
|
||||
},
|
||||
nil,
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"createProjectV2Field": map[string]any{
|
||||
"projectV2Field": map[string]any{
|
||||
"id": "PVTIF_field1",
|
||||
"name": "Sprint",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func updateFieldIterationResponse() githubv4mock.GQLResponse {
|
||||
return githubv4mock.DataResponse(map[string]any{
|
||||
"updateProjectV2Field": map[string]any{
|
||||
"projectV2Field": map[string]any{
|
||||
"id": "PVTIF_field1",
|
||||
"name": "Sprint",
|
||||
"configuration": map[string]any{
|
||||
"iterations": []any{
|
||||
map[string]any{
|
||||
"id": "PVTI_iter1",
|
||||
"title": "Sprint 1",
|
||||
"startDate": "2025-01-20",
|
||||
"duration": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ProjectsWrite_CreateIterationField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
toolDef := ProjectsWrite(translations.NullTranslationHelper)
|
||||
|
||||
t.Run("success with iterations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockGQLClient := githubv4mock.NewMockedHTTPClient(
|
||||
resolveProjectNodeIDOrgMatcher("octo-org", 1, "PVT_project1"),
|
||||
createFieldMatcher(),
|
||||
githubv4mock.NewMutationMatcher(
|
||||
struct {
|
||||
UpdateProjectV2Field struct {
|
||||
ProjectV2Field struct {
|
||||
ProjectV2IterationField struct {
|
||||
ID string
|
||||
Name string
|
||||
Configuration struct {
|
||||
Iterations []struct {
|
||||
ID string
|
||||
Title string
|
||||
StartDate string
|
||||
Duration int
|
||||
}
|
||||
}
|
||||
} `graphql:"... on ProjectV2IterationField"`
|
||||
} `graphql:"projectV2Field"`
|
||||
} `graphql:"updateProjectV2Field(input: $input)"`
|
||||
}{},
|
||||
UpdateProjectV2FieldInput{
|
||||
FieldID: githubv4.ID("PVTIF_field1"),
|
||||
IterationConfiguration: &ProjectV2IterationFieldConfigurationInput{
|
||||
Duration: githubv4.Int(7),
|
||||
StartDate: githubv4.Date{Time: time.Date(2025, 1, 20, 0, 0, 0, 0, time.UTC)},
|
||||
Iterations: []ProjectV2IterationFieldIterationInput{
|
||||
{
|
||||
Title: githubv4.String("Sprint 1"),
|
||||
StartDate: githubv4.Date{Time: time.Date(2025, 1, 20, 0, 0, 0, 0, time.UTC)},
|
||||
Duration: githubv4.Int(7),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
updateFieldIterationResponse(),
|
||||
),
|
||||
)
|
||||
|
||||
deps := BaseDeps{
|
||||
GQLClient: githubv4.NewClient(mockGQLClient),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_iteration_field",
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"field_name": "Sprint",
|
||||
"iteration_duration": float64(7),
|
||||
"start_date": "2025-01-20",
|
||||
"iterations": []any{
|
||||
map[string]any{
|
||||
"title": "Sprint 1",
|
||||
"start_date": "2025-01-20",
|
||||
"duration": float64(7),
|
||||
},
|
||||
},
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PVTIF_field1", response["id"])
|
||||
})
|
||||
|
||||
t.Run("success without iterations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockGQLClient := githubv4mock.NewMockedHTTPClient(
|
||||
resolveProjectNodeIDOrgMatcher("octo-org", 1, "PVT_project1"),
|
||||
createFieldMatcher(),
|
||||
githubv4mock.NewMutationMatcher(
|
||||
struct {
|
||||
UpdateProjectV2Field struct {
|
||||
ProjectV2Field struct {
|
||||
ProjectV2IterationField struct {
|
||||
ID string
|
||||
Name string
|
||||
Configuration struct {
|
||||
Iterations []struct {
|
||||
ID string
|
||||
Title string
|
||||
StartDate string
|
||||
Duration int
|
||||
}
|
||||
}
|
||||
} `graphql:"... on ProjectV2IterationField"`
|
||||
} `graphql:"projectV2Field"`
|
||||
} `graphql:"updateProjectV2Field(input: $input)"`
|
||||
}{},
|
||||
UpdateProjectV2FieldInput{
|
||||
FieldID: githubv4.ID("PVTIF_field1"),
|
||||
IterationConfiguration: &ProjectV2IterationFieldConfigurationInput{
|
||||
Duration: githubv4.Int(7),
|
||||
StartDate: githubv4.Date{Time: time.Date(2025, 1, 20, 0, 0, 0, 0, time.UTC)},
|
||||
Iterations: []ProjectV2IterationFieldIterationInput{},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"updateProjectV2Field": map[string]any{
|
||||
"projectV2Field": map[string]any{
|
||||
"id": "PVTIF_field1",
|
||||
"name": "Sprint",
|
||||
"configuration": map[string]any{
|
||||
"iterations": []any{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
deps := BaseDeps{
|
||||
GQLClient: githubv4.NewClient(mockGQLClient),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_iteration_field",
|
||||
"owner": "octo-org",
|
||||
"owner_type": "org",
|
||||
"project_number": float64(1),
|
||||
"field_name": "Sprint",
|
||||
"iteration_duration": float64(7),
|
||||
"start_date": "2025-01-20",
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PVTIF_field1", response["id"])
|
||||
})
|
||||
|
||||
t.Run("success with auto-detected owner_type", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// detectOwnerType uses REST to probe user first, then org
|
||||
mockRESTClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
||||
GetUsersProjectsV2ByUsernameByProject: mockResponse(t, http.StatusNotFound, nil),
|
||||
GetOrgsProjectsV2ByProject: mockResponse(t, http.StatusOK, map[string]any{
|
||||
"id": 1,
|
||||
"node_id": "PVT_project1",
|
||||
"title": "Org Project",
|
||||
}),
|
||||
})
|
||||
|
||||
mockGQLClient := githubv4mock.NewMockedHTTPClient(
|
||||
resolveProjectNodeIDOrgMatcher("octo-org", 1, "PVT_project1"),
|
||||
createFieldMatcher(),
|
||||
githubv4mock.NewMutationMatcher(
|
||||
struct {
|
||||
UpdateProjectV2Field struct {
|
||||
ProjectV2Field struct {
|
||||
ProjectV2IterationField struct {
|
||||
ID string
|
||||
Name string
|
||||
Configuration struct {
|
||||
Iterations []struct {
|
||||
ID string
|
||||
Title string
|
||||
StartDate string
|
||||
Duration int
|
||||
}
|
||||
}
|
||||
} `graphql:"... on ProjectV2IterationField"`
|
||||
} `graphql:"projectV2Field"`
|
||||
} `graphql:"updateProjectV2Field(input: $input)"`
|
||||
}{},
|
||||
UpdateProjectV2FieldInput{
|
||||
FieldID: githubv4.ID("PVTIF_field1"),
|
||||
IterationConfiguration: &ProjectV2IterationFieldConfigurationInput{
|
||||
Duration: githubv4.Int(14),
|
||||
StartDate: githubv4.Date{Time: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC)},
|
||||
Iterations: []ProjectV2IterationFieldIterationInput{},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
githubv4mock.DataResponse(map[string]any{
|
||||
"updateProjectV2Field": map[string]any{
|
||||
"projectV2Field": map[string]any{
|
||||
"id": "PVTIF_field1",
|
||||
"name": "Sprint",
|
||||
"configuration": map[string]any{
|
||||
"iterations": []any{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
deps := BaseDeps{
|
||||
Client: mustNewGHClient(t, mockRESTClient),
|
||||
GQLClient: githubv4.NewClient(mockGQLClient),
|
||||
Obsv: stubExporters(),
|
||||
}
|
||||
handler := toolDef.Handler(deps)
|
||||
request := createMCPRequest(map[string]any{
|
||||
"method": "create_iteration_field",
|
||||
"owner": "octo-org",
|
||||
"project_number": float64(1),
|
||||
"field_name": "Sprint",
|
||||
"iteration_duration": float64(14),
|
||||
"start_date": "2025-02-01",
|
||||
})
|
||||
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
|
||||
textContent := getTextResult(t, result)
|
||||
var response map[string]any
|
||||
err = json.Unmarshal([]byte(textContent.Text), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PVTIF_field1", response["id"])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user