package github
import (
"context"
"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"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetLabel(t *testing.T) {
t.Parallel()
// Verify tool definition
serverTool := GetLabel(translations.NullTranslationHelper)
tool := serverTool.Tool
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "get_label", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.True(t, tool.Annotations.ReadOnlyHint, "get_label tool should be read-only")
tests := []struct {
name string
requestArgs map[string]any
mockedClient *http.Client
expectToolError bool
expectedToolErrMsg string
}{
{
name: "successful label retrieval",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"name": "bug",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Label struct {
ID githubv4.ID
Name githubv4.String
Color githubv4.String
Description githubv4.String
} `graphql:"label(name: $name)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"name": githubv4.String("bug"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"label": map[string]any{
"id": githubv4.ID("test-label-id"),
"name": githubv4.String("bug"),
"color": githubv4.String("d73a4a"),
"description": githubv4.String("Something isn't working"),
},
},
}),
),
),
expectToolError: false,
},
{
name: "label not found",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"name": "nonexistent",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Label struct {
ID githubv4.ID
Name githubv4.String
Color githubv4.String
Description githubv4.String
} `graphql:"label(name: $name)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"name": githubv4.String("nonexistent"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"label": map[string]any{
"id": githubv4.ID(""),
"name": githubv4.String(""),
"color": githubv4.String(""),
"description": githubv4.String(""),
},
},
}),
),
),
expectToolError: true,
expectedToolErrMsg: "label 'nonexistent' not found in owner/repo",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := githubv4.NewClient(tc.mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.NotNil(t, result)
if tc.expectToolError {
assert.True(t, result.IsError)
if tc.expectedToolErrMsg != "" {
textContent := getErrorResult(t, result)
assert.Contains(t, textContent.Text, tc.expectedToolErrMsg)
}
} else {
assert.False(t, result.IsError)
}
})
}
}
func TestListLabels(t *testing.T) {
t.Parallel()
// Verify tool definition
serverTool := ListLabels(translations.NullTranslationHelper)
tool := serverTool.Tool
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "list_label", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.True(t, tool.Annotations.ReadOnlyHint, "list_label tool should be read-only")
tests := []struct {
name string
requestArgs map[string]any
mockedClient *http.Client
expectToolError bool
expectedToolErrMsg string
}{
{
name: "successful repository labels listing",
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Labels struct {
Nodes []struct {
ID githubv4.ID
Name githubv4.String
Color githubv4.String
Description githubv4.String
}
TotalCount githubv4.Int
} `graphql:"labels(first: 100)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"labels": map[string]any{
"nodes": []any{
map[string]any{
"id": githubv4.ID("label-1"),
"name": githubv4.String("bug"),
"color": githubv4.String("d73a4a"),
"description": githubv4.String("Something isn't working"),
},
map[string]any{
"id": githubv4.ID("label-2"),
"name": githubv4.String("enhancement"),
"color": githubv4.String("a2eeef"),
"description": githubv4.String("New feature or request"),
},
},
"totalCount": githubv4.Int(2),
},
},
}),
),
),
expectToolError: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := githubv4.NewClient(tc.mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.NotNil(t, result)
if tc.expectToolError {
assert.True(t, result.IsError)
if tc.expectedToolErrMsg != "" {
textContent := getErrorResult(t, result)
assert.Contains(t, textContent.Text, tc.expectedToolErrMsg)
}
} else {
assert.False(t, result.IsError)
}
})
}
}
func TestWriteLabel(t *testing.T) {
t.Parallel()
// Verify tool definition
serverTool := LabelWrite(translations.NullTranslationHelper)
tool := serverTool.Tool
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "label_write", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.False(t, tool.Annotations.ReadOnlyHint, "label_write tool should not be read-only")
tests := []struct {
name string
requestArgs map[string]any
mockedClient *http.Client
expectToolError bool
expectedToolErrMsg string
}{
{
name: "successful label creation",
requestArgs: map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"name": "new-label",
"color": "f29513",
"description": "A new test label",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
ID githubv4.ID
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"id": githubv4.ID("test-repo-id"),
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
CreateLabel struct {
Label struct {
Name githubv4.String
ID githubv4.ID
}
} `graphql:"createLabel(input: $input)"`
}{},
githubv4.CreateLabelInput{
RepositoryID: githubv4.ID("test-repo-id"),
Name: githubv4.String("new-label"),
Color: githubv4.String("f29513"),
Description: func() *githubv4.String { s := githubv4.String("A new test label"); return &s }(),
},
nil,
githubv4mock.DataResponse(map[string]any{
"createLabel": map[string]any{
"label": map[string]any{
"id": githubv4.ID("new-label-id"),
"name": githubv4.String("new-label"),
},
},
}),
),
),
expectToolError: false,
},
{
name: "create label without color",
requestArgs: map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"name": "new-label",
},
mockedClient: githubv4mock.NewMockedHTTPClient(),
expectToolError: true,
expectedToolErrMsg: "color is required for create",
},
{
name: "successful label update",
requestArgs: map[string]any{
"method": "update",
"owner": "owner",
"repo": "repo",
"name": "bug",
"new_name": "defect",
"color": "ff0000",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Label struct {
ID githubv4.ID
Name githubv4.String
} `graphql:"label(name: $name)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"name": githubv4.String("bug"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"label": map[string]any{
"id": githubv4.ID("bug-label-id"),
"name": githubv4.String("bug"),
},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
UpdateLabel struct {
Label struct {
Name githubv4.String
ID githubv4.ID
}
} `graphql:"updateLabel(input: $input)"`
}{},
githubv4.UpdateLabelInput{
ID: githubv4.ID("bug-label-id"),
Name: func() *githubv4.String { s := githubv4.String("defect"); return &s }(),
Color: func() *githubv4.String { s := githubv4.String("ff0000"); return &s }(),
},
nil,
githubv4mock.DataResponse(map[string]any{
"updateLabel": map[string]any{
"label": map[string]any{
"id": githubv4.ID("bug-label-id"),
"name": githubv4.String("defect"),
},
},
}),
),
),
expectToolError: false,
},
{
name: "update label without any changes",
requestArgs: map[string]any{
"method": "update",
"owner": "owner",
"repo": "repo",
"name": "bug",
},
mockedClient: githubv4mock.NewMockedHTTPClient(),
expectToolError: true,
expectedToolErrMsg: "at least one of new_name, color, or description must be provided for update",
},
{
name: "successful label deletion",
requestArgs: map[string]any{
"method": "delete",
"owner": "owner",
"repo": "repo",
"name": "bug",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Label struct {
ID githubv4.ID
Name githubv4.String
} `graphql:"label(name: $name)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"name": githubv4.String("bug"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"label": map[string]any{
"id": githubv4.ID("bug-label-id"),
"name": githubv4.String("bug"),
},
},
}),
),
githubv4mock.NewMutationMatcher(
struct {
DeleteLabel struct {
ClientMutationID githubv4.String
} `graphql:"deleteLabel(input: $input)"`
}{},
githubv4.DeleteLabelInput{
ID: githubv4.ID("bug-label-id"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"deleteLabel": map[string]any{
"clientMutationId": githubv4.String("test-mutation-id"),
},
}),
),
),
expectToolError: false,
},
{
name: "invalid method",
requestArgs: map[string]any{
"method": "invalid",
"owner": "owner",
"repo": "repo",
"name": "bug",
},
mockedClient: githubv4mock.NewMockedHTTPClient(),
expectToolError: true,
expectedToolErrMsg: "unknown method: invalid",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := githubv4.NewClient(tc.mockedClient)
deps := BaseDeps{
GQLClient: client,
}
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.NotNil(t, result)
if tc.expectToolError {
assert.True(t, result.IsError)
if tc.expectedToolErrMsg != "" {
textContent := getErrorResult(t, result)
assert.Contains(t, textContent.Text, tc.expectedToolErrMsg)
}
} else {
assert.False(t, result.IsError)
}
})
}
}