Files
github--github-mcp-server/pkg/github/projects_v2_test.go
T
2026-07-13 12:37:57 +08:00

458 lines
13 KiB
Go

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"])
})
}