github-mcp-server / pkg /github /discussions_test.go
Gemini
Initial commit
fce10de
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/google/go-github/v74/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
discussionsGeneral = []map[string]any{
{"number": 1, "title": "Discussion 1 title", "createdAt": "2023-01-01T00:00:00Z", "updatedAt": "2023-01-01T00:00:00Z", "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/1", "category": map[string]any{"name": "General"}},
{"number": 3, "title": "Discussion 3 title", "createdAt": "2023-03-01T00:00:00Z", "updatedAt": "2023-02-01T00:00:00Z", "author": map[string]any{"login": "user1"}, "url": "https://github.com/owner/repo/discussions/3", "category": map[string]any{"name": "General"}},
}
discussionsAll = []map[string]any{
{
"number": 1,
"title": "Discussion 1 title",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z",
"author": map[string]any{"login": "user1"},
"url": "https://github.com/owner/repo/discussions/1",
"category": map[string]any{"name": "General"},
},
{
"number": 2,
"title": "Discussion 2 title",
"createdAt": "2023-02-01T00:00:00Z",
"updatedAt": "2023-02-01T00:00:00Z",
"author": map[string]any{"login": "user2"},
"url": "https://github.com/owner/repo/discussions/2",
"category": map[string]any{"name": "Questions"},
},
{
"number": 3,
"title": "Discussion 3 title",
"createdAt": "2023-03-01T00:00:00Z",
"updatedAt": "2023-03-01T00:00:00Z",
"author": map[string]any{"login": "user3"},
"url": "https://github.com/owner/repo/discussions/3",
"category": map[string]any{"name": "General"},
},
}
discussionsOrgLevel = []map[string]any{
{
"number": 1,
"title": "Org Discussion 1 - Community Guidelines",
"createdAt": "2023-01-15T00:00:00Z",
"updatedAt": "2023-01-15T00:00:00Z",
"author": map[string]any{"login": "org-admin"},
"url": "https://github.com/owner/.github/discussions/1",
"category": map[string]any{"name": "Announcements"},
},
{
"number": 2,
"title": "Org Discussion 2 - Roadmap 2023",
"createdAt": "2023-02-20T00:00:00Z",
"updatedAt": "2023-02-20T00:00:00Z",
"author": map[string]any{"login": "org-admin"},
"url": "https://github.com/owner/.github/discussions/2",
"category": map[string]any{"name": "General"},
},
{
"number": 3,
"title": "Org Discussion 3 - Roadmap 2024",
"createdAt": "2023-02-20T00:00:00Z",
"updatedAt": "2023-02-20T00:00:00Z",
"author": map[string]any{"login": "org-admin"},
"url": "https://github.com/owner/.github/discussions/3",
"category": map[string]any{"name": "General"},
},
{
"number": 4,
"title": "Org Discussion 4 - Roadmap 2025",
"createdAt": "2023-02-20T00:00:00Z",
"updatedAt": "2023-02-20T00:00:00Z",
"author": map[string]any{"login": "org-admin"},
"url": "https://github.com/owner/.github/discussions/4",
"category": map[string]any{"name": "General"},
},
}
// Ordered mock responses
discussionsOrderedCreatedAsc = []map[string]any{
discussionsAll[0], // Discussion 1 (created 2023-01-01)
discussionsAll[1], // Discussion 2 (created 2023-02-01)
discussionsAll[2], // Discussion 3 (created 2023-03-01)
}
discussionsOrderedUpdatedDesc = []map[string]any{
discussionsAll[2], // Discussion 3 (updated 2023-03-01)
discussionsAll[1], // Discussion 2 (updated 2023-02-01)
discussionsAll[0], // Discussion 1 (updated 2023-01-01)
}
// only 'General' category discussions ordered by created date descending
discussionsGeneralOrderedDesc = []map[string]any{
discussionsGeneral[1], // Discussion 3 (created 2023-03-01)
discussionsGeneral[0], // Discussion 1 (created 2023-01-01)
}
mockResponseListAll = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsAll,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 3,
},
},
})
mockResponseListGeneral = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsGeneral,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 2,
},
},
})
mockResponseOrderedCreatedAsc = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsOrderedCreatedAsc,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 3,
},
},
})
mockResponseOrderedUpdatedDesc = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsOrderedUpdatedDesc,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 3,
},
},
})
mockResponseGeneralOrderedDesc = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsGeneralOrderedDesc,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 2,
},
},
})
mockResponseOrgLevel = githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussions": map[string]any{
"nodes": discussionsOrgLevel,
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 4,
},
},
})
mockErrorRepoNotFound = githubv4mock.ErrorResponse("repository not found")
)
func Test_ListDiscussions(t *testing.T) {
mockClient := githubv4.NewClient(nil)
toolDef, _ := ListDiscussions(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper)
assert.Equal(t, "list_discussions", toolDef.Name)
assert.NotEmpty(t, toolDef.Description)
assert.Contains(t, toolDef.InputSchema.Properties, "owner")
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
assert.Contains(t, toolDef.InputSchema.Properties, "orderBy")
assert.Contains(t, toolDef.InputSchema.Properties, "direction")
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner"})
// Variables matching what GraphQL receives after JSON marshaling/unmarshaling
varsListAll := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"first": float64(30),
"after": (*string)(nil),
}
varsRepoNotFound := map[string]interface{}{
"owner": "owner",
"repo": "nonexistent-repo",
"first": float64(30),
"after": (*string)(nil),
}
varsDiscussionsFiltered := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"categoryId": "DIC_kwDOABC123",
"first": float64(30),
"after": (*string)(nil),
}
varsOrderByCreatedAsc := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"orderByField": "CREATED_AT",
"orderByDirection": "ASC",
"first": float64(30),
"after": (*string)(nil),
}
varsOrderByUpdatedDesc := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"orderByField": "UPDATED_AT",
"orderByDirection": "DESC",
"first": float64(30),
"after": (*string)(nil),
}
varsCategoryWithOrder := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"categoryId": "DIC_kwDOABC123",
"orderByField": "CREATED_AT",
"orderByDirection": "DESC",
"first": float64(30),
"after": (*string)(nil),
}
varsOrgLevel := map[string]interface{}{
"owner": "owner",
"repo": ".github", // This is what gets set when repo is not provided
"first": float64(30),
"after": (*string)(nil),
}
tests := []struct {
name string
reqParams map[string]interface{}
expectError bool
errContains string
expectedCount int
verifyOrder func(t *testing.T, discussions []*github.Discussion)
}{
{
name: "list all discussions without category filter",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
expectError: false,
expectedCount: 3, // All discussions
},
{
name: "filter by category ID",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"category": "DIC_kwDOABC123",
},
expectError: false,
expectedCount: 2, // Only General discussions (matching the category ID)
},
{
name: "order by created at ascending",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"orderBy": "CREATED_AT",
"direction": "ASC",
},
expectError: false,
expectedCount: 3,
verifyOrder: func(t *testing.T, discussions []*github.Discussion) {
// Verify discussions are ordered by created date ascending
require.Len(t, discussions, 3)
assert.Equal(t, 1, *discussions[0].Number, "First should be discussion 1 (created 2023-01-01)")
assert.Equal(t, 2, *discussions[1].Number, "Second should be discussion 2 (created 2023-02-01)")
assert.Equal(t, 3, *discussions[2].Number, "Third should be discussion 3 (created 2023-03-01)")
},
},
{
name: "order by updated at descending",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"orderBy": "UPDATED_AT",
"direction": "DESC",
},
expectError: false,
expectedCount: 3,
verifyOrder: func(t *testing.T, discussions []*github.Discussion) {
// Verify discussions are ordered by updated date descending
require.Len(t, discussions, 3)
assert.Equal(t, 3, *discussions[0].Number, "First should be discussion 3 (updated 2023-03-01)")
assert.Equal(t, 2, *discussions[1].Number, "Second should be discussion 2 (updated 2023-02-01)")
assert.Equal(t, 1, *discussions[2].Number, "Third should be discussion 1 (updated 2023-01-01)")
},
},
{
name: "filter by category with order",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"category": "DIC_kwDOABC123",
"orderBy": "CREATED_AT",
"direction": "DESC",
},
expectError: false,
expectedCount: 2,
verifyOrder: func(t *testing.T, discussions []*github.Discussion) {
// Verify only General discussions, ordered by created date descending
require.Len(t, discussions, 2)
assert.Equal(t, 3, *discussions[0].Number, "First should be discussion 3 (created 2023-03-01)")
assert.Equal(t, 1, *discussions[1].Number, "Second should be discussion 1 (created 2023-01-01)")
},
},
{
name: "order by without direction (should not use ordering)",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"orderBy": "CREATED_AT",
},
expectError: false,
expectedCount: 3,
},
{
name: "direction without order by (should not use ordering)",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"direction": "DESC",
},
expectError: false,
expectedCount: 3,
},
{
name: "repository not found error",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "nonexistent-repo",
},
expectError: true,
errContains: "repository not found",
},
{
name: "list org-level discussions (no repo provided)",
reqParams: map[string]interface{}{
"owner": "owner",
// repo is not provided, it will default to ".github"
},
expectError: false,
expectedCount: 4,
},
}
// Define the actual query strings that match the implementation
qBasicNoOrder := "query($after:String$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
qWithCategoryNoOrder := "query($after:String$categoryId:ID!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
qBasicWithOrder := "query($after:String$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
qWithCategoryAndOrder := "query($after:String$categoryId:ID!$first:Int!$orderByDirection:OrderDirection!$orderByField:DiscussionOrderField!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussions(first: $first, after: $after, categoryId: $categoryId, orderBy: { field: $orderByField, direction: $orderByDirection }){nodes{number,title,createdAt,updatedAt,author{login},category{name},url},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var httpClient *http.Client
switch tc.name {
case "list all discussions without category filter":
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsListAll, mockResponseListAll)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "filter by category ID":
matcher := githubv4mock.NewQueryMatcher(qWithCategoryNoOrder, varsDiscussionsFiltered, mockResponseListGeneral)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "order by created at ascending":
matcher := githubv4mock.NewQueryMatcher(qBasicWithOrder, varsOrderByCreatedAsc, mockResponseOrderedCreatedAsc)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "order by updated at descending":
matcher := githubv4mock.NewQueryMatcher(qBasicWithOrder, varsOrderByUpdatedDesc, mockResponseOrderedUpdatedDesc)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "filter by category with order":
matcher := githubv4mock.NewQueryMatcher(qWithCategoryAndOrder, varsCategoryWithOrder, mockResponseGeneralOrderedDesc)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "order by without direction (should not use ordering)":
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsListAll, mockResponseListAll)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "direction without order by (should not use ordering)":
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsListAll, mockResponseListAll)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "repository not found error":
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsRepoNotFound, mockErrorRepoNotFound)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
case "list org-level discussions (no repo provided)":
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsOrgLevel, mockResponseOrgLevel)
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
}
gqlClient := githubv4.NewClient(httpClient)
_, handler := ListDiscussions(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
req := createMCPRequest(tc.reqParams)
res, err := handler(context.Background(), req)
text := getTextResult(t, res).Text
if tc.expectError {
require.True(t, res.IsError)
assert.Contains(t, text, tc.errContains)
return
}
require.NoError(t, err)
// Parse the structured response with pagination info
var response struct {
Discussions []*github.Discussion `json:"discussions"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor string `json:"startCursor"`
EndCursor string `json:"endCursor"`
} `json:"pageInfo"`
TotalCount int `json:"totalCount"`
}
err = json.Unmarshal([]byte(text), &response)
require.NoError(t, err)
assert.Len(t, response.Discussions, tc.expectedCount, "Expected %d discussions, got %d", tc.expectedCount, len(response.Discussions))
// Verify order if verifyOrder function is provided
if tc.verifyOrder != nil {
tc.verifyOrder(t, response.Discussions)
}
// Verify that all returned discussions have a category if filtered
if _, hasCategory := tc.reqParams["category"]; hasCategory {
for _, discussion := range response.Discussions {
require.NotNil(t, discussion.DiscussionCategory, "Discussion should have category")
assert.NotEmpty(t, *discussion.DiscussionCategory.Name, "Discussion should have category name")
}
}
})
}
}
func Test_GetDiscussion(t *testing.T) {
// Verify tool definition and schema
toolDef, _ := GetDiscussion(nil, translations.NullTranslationHelper)
assert.Equal(t, "get_discussion", toolDef.Name)
assert.NotEmpty(t, toolDef.Description)
assert.Contains(t, toolDef.InputSchema.Properties, "owner")
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
assert.Contains(t, toolDef.InputSchema.Properties, "discussionNumber")
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner", "repo", "discussionNumber"})
// Use exact string query that matches implementation output
qGetDiscussion := "query($discussionNumber:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){number,title,body,createdAt,url,category{name}}}}"
vars := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"discussionNumber": float64(1),
}
tests := []struct {
name string
response githubv4mock.GQLResponse
expectError bool
expected *github.Discussion
errContains string
}{
{
name: "successful retrieval",
response: githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{"discussion": map[string]any{
"number": 1,
"title": "Test Discussion Title",
"body": "This is a test discussion",
"url": "https://github.com/owner/repo/discussions/1",
"createdAt": "2025-04-25T12:00:00Z",
"category": map[string]any{"name": "General"},
}},
}),
expectError: false,
expected: &github.Discussion{
HTMLURL: github.Ptr("https://github.com/owner/repo/discussions/1"),
Number: github.Ptr(1),
Title: github.Ptr("Test Discussion Title"),
Body: github.Ptr("This is a test discussion"),
CreatedAt: &github.Timestamp{Time: time.Date(2025, 4, 25, 12, 0, 0, 0, time.UTC)},
DiscussionCategory: &github.DiscussionCategory{
Name: github.Ptr("General"),
},
},
},
{
name: "discussion not found",
response: githubv4mock.ErrorResponse("discussion not found"),
expectError: true,
errContains: "discussion not found",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
matcher := githubv4mock.NewQueryMatcher(qGetDiscussion, vars, tc.response)
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
gqlClient := githubv4.NewClient(httpClient)
_, handler := GetDiscussion(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
req := createMCPRequest(map[string]interface{}{"owner": "owner", "repo": "repo", "discussionNumber": int32(1)})
res, err := handler(context.Background(), req)
text := getTextResult(t, res).Text
if tc.expectError {
require.True(t, res.IsError)
assert.Contains(t, text, tc.errContains)
return
}
require.NoError(t, err)
var out github.Discussion
require.NoError(t, json.Unmarshal([]byte(text), &out))
assert.Equal(t, *tc.expected.HTMLURL, *out.HTMLURL)
assert.Equal(t, *tc.expected.Number, *out.Number)
assert.Equal(t, *tc.expected.Title, *out.Title)
assert.Equal(t, *tc.expected.Body, *out.Body)
// Check category label
assert.Equal(t, *tc.expected.DiscussionCategory.Name, *out.DiscussionCategory.Name)
})
}
}
func Test_GetDiscussionComments(t *testing.T) {
// Verify tool definition and schema
toolDef, _ := GetDiscussionComments(nil, translations.NullTranslationHelper)
assert.Equal(t, "get_discussion_comments", toolDef.Name)
assert.NotEmpty(t, toolDef.Description)
assert.Contains(t, toolDef.InputSchema.Properties, "owner")
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
assert.Contains(t, toolDef.InputSchema.Properties, "discussionNumber")
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner", "repo", "discussionNumber"})
// Use exact string query that matches implementation output
qGetComments := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{body},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
// Variables matching what GraphQL receives after JSON marshaling/unmarshaling
vars := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"discussionNumber": float64(1),
"first": float64(30),
"after": (*string)(nil),
}
mockResponse := githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussion": map[string]any{
"comments": map[string]any{
"nodes": []map[string]any{
{"body": "This is the first comment"},
{"body": "This is the second comment"},
},
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 2,
},
},
},
})
matcher := githubv4mock.NewQueryMatcher(qGetComments, vars, mockResponse)
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
gqlClient := githubv4.NewClient(httpClient)
_, handler := GetDiscussionComments(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
request := createMCPRequest(map[string]interface{}{
"owner": "owner",
"repo": "repo",
"discussionNumber": int32(1),
})
result, err := handler(context.Background(), request)
require.NoError(t, err)
textContent := getTextResult(t, result)
// (Lines removed)
var response struct {
Comments []*github.IssueComment `json:"comments"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor string `json:"startCursor"`
EndCursor string `json:"endCursor"`
} `json:"pageInfo"`
TotalCount int `json:"totalCount"`
}
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Len(t, response.Comments, 2)
expectedBodies := []string{"This is the first comment", "This is the second comment"}
for i, comment := range response.Comments {
assert.Equal(t, expectedBodies[i], *comment.Body)
}
}
func Test_ListDiscussionCategories(t *testing.T) {
mockClient := githubv4.NewClient(nil)
toolDef, _ := ListDiscussionCategories(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper)
assert.Equal(t, "list_discussion_categories", toolDef.Name)
assert.NotEmpty(t, toolDef.Description)
assert.Contains(t, toolDef.Description, "or organisation")
assert.Contains(t, toolDef.InputSchema.Properties, "owner")
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner"})
// Use exact string query that matches implementation output
qListCategories := "query($first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussionCategories(first: $first){nodes{id,name},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
// Variables for repository-level categories
varsRepo := map[string]interface{}{
"owner": "owner",
"repo": "repo",
"first": float64(25),
}
// Variables for organization-level categories (using .github repo)
varsOrg := map[string]interface{}{
"owner": "owner",
"repo": ".github",
"first": float64(25),
}
mockRespRepo := githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussionCategories": map[string]any{
"nodes": []map[string]any{
{"id": "123", "name": "CategoryOne"},
{"id": "456", "name": "CategoryTwo"},
},
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 2,
},
},
})
mockRespOrg := githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"discussionCategories": map[string]any{
"nodes": []map[string]any{
{"id": "789", "name": "Announcements"},
{"id": "101", "name": "General"},
{"id": "112", "name": "Ideas"},
},
"pageInfo": map[string]any{
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "",
"endCursor": "",
},
"totalCount": 3,
},
},
})
tests := []struct {
name string
reqParams map[string]interface{}
vars map[string]interface{}
mockResponse githubv4mock.GQLResponse
expectError bool
expectedCount int
expectedCategories []map[string]string
}{
{
name: "list repository-level discussion categories",
reqParams: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
vars: varsRepo,
mockResponse: mockRespRepo,
expectError: false,
expectedCount: 2,
expectedCategories: []map[string]string{
{"id": "123", "name": "CategoryOne"},
{"id": "456", "name": "CategoryTwo"},
},
},
{
name: "list org-level discussion categories (no repo provided)",
reqParams: map[string]interface{}{
"owner": "owner",
// repo is not provided, it will default to ".github"
},
vars: varsOrg,
mockResponse: mockRespOrg,
expectError: false,
expectedCount: 3,
expectedCategories: []map[string]string{
{"id": "789", "name": "Announcements"},
{"id": "101", "name": "General"},
{"id": "112", "name": "Ideas"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
matcher := githubv4mock.NewQueryMatcher(qListCategories, tc.vars, tc.mockResponse)
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
gqlClient := githubv4.NewClient(httpClient)
_, handler := ListDiscussionCategories(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
req := createMCPRequest(tc.reqParams)
res, err := handler(context.Background(), req)
text := getTextResult(t, res).Text
if tc.expectError {
require.True(t, res.IsError)
return
}
require.NoError(t, err)
var response struct {
Categories []map[string]string `json:"categories"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor string `json:"startCursor"`
EndCursor string `json:"endCursor"`
} `json:"pageInfo"`
TotalCount int `json:"totalCount"`
}
require.NoError(t, json.Unmarshal([]byte(text), &response))
assert.Equal(t, tc.expectedCategories, response.Categories)
})
}
}