package github
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
gh "github.com/google/go-github/v82/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Tests for consolidated project tools
func Test_ProjectsList(t *testing.T) {
// Verify tool definition once
toolDef := ProjectsList(translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))
assert.Equal(t, "projects_list", toolDef.Tool.Name)
assert.NotEmpty(t, toolDef.Tool.Description)
inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema)
assert.Contains(t, inputSchema.Properties, "method")
assert.Contains(t, inputSchema.Properties, "owner")
assert.Contains(t, inputSchema.Properties, "owner_type")
assert.Contains(t, inputSchema.Properties, "project_number")
assert.Contains(t, inputSchema.Properties, "query")
assert.Contains(t, inputSchema.Properties, "fields")
assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner"})
}
func Test_ProjectsList_ListProjects(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)
orgProjects := []map[string]any{{"id": 1, "node_id": "NODE1", "title": "Org Project"}}
userProjects := []map[string]any{{"id": 2, "node_id": "NODE2", "title": "User Project"}}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedLength int
}{
{
name: "success organization",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2: mockResponse(t, http.StatusOK, orgProjects),
}),
requestArgs: map[string]any{
"method": "list_projects",
"owner": "octo-org",
"owner_type": "org",
},
expectError: false,
expectedLength: 1,
},
{
name: "success user",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetUsersProjectsV2ByUsername: mockResponse(t, http.StatusOK, userProjects),
}),
requestArgs: map[string]any{
"method": "list_projects",
"owner": "octocat",
"owner_type": "user",
},
expectError: false,
expectedLength: 1,
},
{
name: "missing required parameter method",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
requestArgs: map[string]any{
"owner": "octo-org",
"owner_type": "org",
},
expectError: true,
expectedErrMsg: "missing required parameter: method",
},
{
name: "unknown method",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
requestArgs: map[string]any{
"method": "unknown_method",
"owner": "octo-org",
"owner_type": "org",
},
expectError: true,
expectedErrMsg: "unknown method: unknown_method",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := gh.NewClient(tc.mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
textContent := getTextResult(t, result)
if tc.expectError {
if tc.expectedErrMsg != "" {
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
}
return
}
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
projects, ok := response["projects"].([]any)
require.True(t, ok)
assert.Equal(t, tc.expectedLength, len(projects))
})
}
}
func Test_ProjectsList_ListProjectFields(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)
fields := []map[string]any{{"id": 101, "name": "Status", "data_type": "single_select"}}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2FieldsByProject: mockResponse(t, http.StatusOK, fields),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "list_project_fields",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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)
fieldsList, ok := response["fields"].([]any)
require.True(t, ok)
assert.Equal(t, 1, len(fieldsList))
})
t.Run("missing project_number", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "list_project_fields",
"owner": "octo-org",
"owner_type": "org",
})
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, "missing required parameter: project_number")
})
}
func Test_ProjectsList_ListProjectItems(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)
items := []map[string]any{{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2ItemsByProject: mockResponse(t, http.StatusOK, items),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "list_project_items",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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)
itemsList, ok := response["items"].([]any)
require.True(t, ok)
assert.Equal(t, 1, len(itemsList))
})
}
func Test_ProjectsGet(t *testing.T) {
// Verify tool definition once
toolDef := ProjectsGet(translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))
assert.Equal(t, "projects_get", toolDef.Tool.Name)
assert.NotEmpty(t, toolDef.Tool.Description)
inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema)
assert.Contains(t, inputSchema.Properties, "method")
assert.Contains(t, inputSchema.Properties, "owner")
assert.Contains(t, inputSchema.Properties, "owner_type")
assert.Contains(t, inputSchema.Properties, "project_number")
assert.Contains(t, inputSchema.Properties, "field_id")
assert.Contains(t, inputSchema.Properties, "item_id")
assert.ElementsMatch(t, inputSchema.Required, []string{"method"})
}
func Test_ProjectsGet_GetProject(t *testing.T) {
toolDef := ProjectsGet(translations.NullTranslationHelper)
project := map[string]any{"id": 123, "title": "Project Title"}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2ByProject: mockResponse(t, http.StatusOK, project),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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.NotNil(t, response["id"])
})
t.Run("unknown method", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "unknown_method",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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, "unknown method: unknown_method")
})
}
func Test_ProjectsGet_GetProjectField(t *testing.T) {
toolDef := ProjectsGet(translations.NullTranslationHelper)
field := map[string]any{"id": 101, "name": "Status", "data_type": "single_select"}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2FieldsByProjectByFieldID: mockResponse(t, http.StatusOK, field),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project_field",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"field_id": float64(101),
})
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.NotNil(t, response["id"])
})
t.Run("missing field_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project_field",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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, "missing required parameter: field_id")
})
}
func Test_ProjectsGet_GetProjectItem(t *testing.T) {
toolDef := ProjectsGet(translations.NullTranslationHelper)
item := map[string]any{"id": 1001, "archived_at": nil, "content": map[string]any{"title": "Issue 1"}}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusOK, item),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_id": float64(1001),
})
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.NotNil(t, response["id"])
})
t.Run("missing item_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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, "missing required parameter: item_id")
})
}
func Test_ProjectsWrite(t *testing.T) {
// Verify tool definition once
toolDef := ProjectsWrite(translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))
assert.Equal(t, "projects_write", toolDef.Tool.Name)
assert.NotEmpty(t, toolDef.Tool.Description)
inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema)
assert.Contains(t, inputSchema.Properties, "method")
assert.Contains(t, inputSchema.Properties, "owner")
assert.Contains(t, inputSchema.Properties, "owner_type")
assert.Contains(t, inputSchema.Properties, "project_number")
assert.Contains(t, inputSchema.Properties, "item_id")
assert.Contains(t, inputSchema.Properties, "item_type")
assert.Contains(t, inputSchema.Properties, "item_owner")
assert.Contains(t, inputSchema.Properties, "item_repo")
assert.Contains(t, inputSchema.Properties, "issue_number")
assert.Contains(t, inputSchema.Properties, "pull_request_number")
assert.Contains(t, inputSchema.Properties, "updated_field")
assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "project_number"})
// Verify DestructiveHint is set
assert.NotNil(t, toolDef.Tool.Annotations)
assert.NotNil(t, toolDef.Tool.Annotations.DestructiveHint)
assert.True(t, *toolDef.Tool.Annotations.DestructiveHint)
}
func Test_ProjectsWrite_AddProjectItem(t *testing.T) {
toolDef := ProjectsWrite(translations.NullTranslationHelper)
t.Run("success organization with issue", func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
// Mock resolveIssueNodeID query
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Issue struct {
ID githubv4.ID
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("item-owner"),
"repo": githubv4.String("item-repo"),
"issueNumber": githubv4.Int(123),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"issue": map[string]any{
"id": "I_issue123",
},
},
}),
),
// Mock project ID query for org
githubv4mock.NewQueryMatcher(
struct {
Organization struct {
ProjectV2 struct {
ID githubv4.ID
} `graphql:"projectV2(number: $projectNumber)"`
} `graphql:"organization(login: $owner)"`
}{},
map[string]any{
"owner": githubv4.String("octo-org"),
"projectNumber": githubv4.Int(1),
},
githubv4mock.DataResponse(map[string]any{
"organization": map[string]any{
"projectV2": map[string]any{
"id": "PVT_project1",
},
},
}),
),
// Mock addProjectV2ItemById mutation
githubv4mock.NewMutationMatcher(
struct {
AddProjectV2ItemByID struct {
Item struct {
ID githubv4.ID
}
} `graphql:"addProjectV2ItemById(input: $input)"`
}{},
githubv4.AddProjectV2ItemByIdInput{
ProjectID: githubv4.ID("PVT_project1"),
ContentID: githubv4.ID("I_issue123"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"addProjectV2ItemById": map[string]any{
"item": map[string]any{
"id": "PVTI_item1",
},
},
}),
),
)
client := githubv4.NewClient(mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "add_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_owner": "item-owner",
"item_repo": "item-repo",
"issue_number": float64(123),
"item_type": "issue",
})
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.NotNil(t, response["id"])
assert.Contains(t, response["message"], "Successfully added")
})
t.Run("success user with pull request", func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
// Mock resolvePullRequestNodeID query
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
PullRequest struct {
ID githubv4.ID
} `graphql:"pullRequest(number: $prNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("item-owner"),
"repo": githubv4.String("item-repo"),
"prNumber": githubv4.Int(456),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"pullRequest": map[string]any{
"id": "PR_pr456",
},
},
}),
),
// Mock project ID query for user
githubv4mock.NewQueryMatcher(
struct {
User struct {
ProjectV2 struct {
ID githubv4.ID
} `graphql:"projectV2(number: $projectNumber)"`
} `graphql:"user(login: $owner)"`
}{},
map[string]any{
"owner": githubv4.String("octo-user"),
"projectNumber": githubv4.Int(2),
},
githubv4mock.DataResponse(map[string]any{
"user": map[string]any{
"projectV2": map[string]any{
"id": "PVT_project2",
},
},
}),
),
// Mock addProjectV2ItemById mutation
githubv4mock.NewMutationMatcher(
struct {
AddProjectV2ItemByID struct {
Item struct {
ID githubv4.ID
}
} `graphql:"addProjectV2ItemById(input: $input)"`
}{},
githubv4.AddProjectV2ItemByIdInput{
ProjectID: githubv4.ID("PVT_project2"),
ContentID: githubv4.ID("PR_pr456"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"addProjectV2ItemById": map[string]any{
"item": map[string]any{
"id": "PVTI_item2",
},
},
}),
),
)
client := githubv4.NewClient(mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "add_project_item",
"owner": "octo-user",
"owner_type": "user",
"project_number": float64(2),
"item_owner": "item-owner",
"item_repo": "item-repo",
"pull_request_number": float64(456),
"item_type": "pull_request",
})
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.NotNil(t, response["id"])
assert.Contains(t, response["message"], "Successfully added")
})
t.Run("missing item_type", func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient()
client := githubv4.NewClient(mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "add_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_owner": "item-owner",
"item_repo": "item-repo",
"issue_number": float64(123),
})
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, "missing required parameter: item_type")
})
t.Run("invalid item_type", func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient()
client := githubv4.NewClient(mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "add_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_owner": "item-owner",
"item_repo": "item-repo",
"issue_number": float64(123),
"item_type": "invalid_type",
})
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, "item_type must be either 'issue' or 'pull_request'")
})
t.Run("unknown method", func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient()
client := githubv4.NewClient(mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "unknown_method",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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, "unknown method: unknown_method")
})
}
func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
toolDef := ProjectsWrite(translations.NullTranslationHelper)
updatedItem := map[string]any{"id": 1001, "archived_at": nil}
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusOK, updatedItem),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "update_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_id": float64(1001),
"updated_field": map[string]any{
"id": float64(101),
"value": "In Progress",
},
})
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.NotNil(t, response["id"])
})
t.Run("missing updated_field", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "update_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_id": float64(1001),
})
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, "missing required parameter: updated_field")
})
}
func Test_ProjectsWrite_DeleteProjectItem(t *testing.T) {
toolDef := ProjectsWrite(translations.NullTranslationHelper)
t.Run("success organization", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
DeleteOrgsProjectsV2ItemsByProjectByItemID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "delete_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
"item_id": float64(1001),
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "project item successfully deleted")
})
t.Run("missing item_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
client := gh.NewClient(mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "delete_project_item",
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(1),
})
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, "missing required parameter: item_id")
})
}
func Test_ProjectsList_ListProjectStatusUpdates(t *testing.T) {
toolDef := ProjectsList(translations.NullTranslationHelper)
t.Run("success via consolidated tool", func(t *testing.T) {
// REST mock for detectOwnerType (when owner_type is omitted)
restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetUsersProjectsV2ByUsernameByProject: mockResponse(t, http.StatusOK, map[string]any{"id": 1}),
})
// GQL mock for listProjectStatusUpdates
gqlMockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
statusUpdatesUserQuery{},
map[string]any{
"owner": githubv4.String("octocat"),
"projectNumber": githubv4.Int(1),
"first": githubv4.Int(50),
"after": (*githubv4.String)(nil),
},
githubv4mock.DataResponse(map[string]any{
"user": map[string]any{
"projectV2": map[string]any{
"statusUpdates": map[string]any{
"nodes": []map[string]any{
{
"id": "SU_1",
"body": "On track",
"status": "ON_TRACK",
"createdAt": "2026-01-15T10:00:00Z",
"startDate": "2026-01-01",
"targetDate": "2026-03-01",
"creator": map[string]any{"login": "octocat"},
},
},
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
},
},
},
}),
),
)
gqlClient := githubv4.NewClient(gqlMockedClient)
deps := BaseDeps{
Client: gh.NewClient(restClient),
GQLClient: gqlClient,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "list_project_status_updates",
"owner": "octocat",
"project_number": float64(1),
})
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)
updates, ok := response["statusUpdates"].([]any)
require.True(t, ok)
assert.Len(t, updates, 1)
})
}
func Test_ProjectsGet_GetProjectStatusUpdate(t *testing.T) {
toolDef := ProjectsGet(translations.NullTranslationHelper)
t.Run("success via consolidated tool", func(t *testing.T) {
gqlMockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
statusUpdateNodeQuery{},
map[string]any{
"id": githubv4.ID("SU_abc123"),
},
githubv4mock.DataResponse(map[string]any{
"node": map[string]any{
"id": "SU_abc123",
"body": "On track",
"status": "ON_TRACK",
"createdAt": "2026-01-15T10:00:00Z",
"startDate": "2026-01-01",
"targetDate": "2026-03-01",
"creator": map[string]any{"login": "octocat"},
},
}),
),
)
gqlClient := githubv4.NewClient(gqlMockedClient)
deps := BaseDeps{
GQLClient: gqlClient,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "get_project_status_update",
"owner": "octocat",
"project_number": float64(1),
"status_update_id": "SU_abc123",
})
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, "SU_abc123", response["id"])
assert.Equal(t, "On track", response["body"])
})
}
func Test_ProjectsWrite_CreateProjectStatusUpdate(t *testing.T) {
toolDef := ProjectsWrite(translations.NullTranslationHelper)
t.Run("success via consolidated tool", func(t *testing.T) {
bodyStr := githubv4.String("Consolidated test")
statusStr := githubv4.String("AT_RISK")
gqlMockedClient := githubv4mock.NewMockedHTTPClient(
// Mock project ID query for user
githubv4mock.NewQueryMatcher(
struct {
User struct {
ProjectV2 struct {
ID githubv4.ID
} `graphql:"projectV2(number: $projectNumber)"`
} `graphql:"user(login: $owner)"`
}{},
map[string]any{
"owner": githubv4.String("octocat"),
"projectNumber": githubv4.Int(3),
},
githubv4mock.DataResponse(map[string]any{
"user": map[string]any{
"projectV2": map[string]any{
"id": "PVT_project3",
},
},
}),
),
// Mock createProjectV2StatusUpdate mutation
githubv4mock.NewMutationMatcher(
struct {
CreateProjectV2StatusUpdate struct {
StatusUpdate statusUpdateNode
} `graphql:"createProjectV2StatusUpdate(input: $input)"`
}{},
CreateProjectV2StatusUpdateInput{
ProjectID: githubv4.ID("PVT_project3"),
Body: &bodyStr,
Status: &statusStr,
},
nil,
githubv4mock.DataResponse(map[string]any{
"createProjectV2StatusUpdate": map[string]any{
"statusUpdate": map[string]any{
"id": "PVTSU_su003",
"body": "Consolidated test",
"status": "AT_RISK",
"createdAt": "2026-02-09T12:00:00Z",
"creator": map[string]any{"login": "octocat"},
},
},
}),
),
)
gqlClient := githubv4.NewClient(gqlMockedClient)
deps := BaseDeps{
GQLClient: gqlClient,
}
handler := toolDef.Handler(deps)
request := createMCPRequest(map[string]any{
"method": "create_project_status_update",
"owner": "octocat",
"owner_type": "user",
"project_number": float64(3),
"body": "Consolidated test",
"status": "AT_RISK",
})
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, "PVTSU_su003", response["id"])
assert.Equal(t, "Consolidated test", response["body"])
assert.Equal(t, "AT_RISK", response["status"])
})
}