github-mcp-server / pkg /github /notifications_test.go
Gemini
Initial commit
fce10de
package github
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v74/github"
"github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_ListNotifications(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := ListNotifications(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "list_notifications", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "filter")
assert.Contains(t, tool.InputSchema.Properties, "since")
assert.Contains(t, tool.InputSchema.Properties, "before")
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "page")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
// All fields are optional, so Required should be empty
assert.Empty(t, tool.InputSchema.Required)
mockNotification := &github.Notification{
ID: github.Ptr("123"),
Reason: github.Ptr("mention"),
}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedResult []*github.Notification
expectedErrMsg string
}{
{
name: "success default filter (no params)",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetNotifications,
[]*github.Notification{mockNotification},
),
),
requestArgs: map[string]interface{}{},
expectError: false,
expectedResult: []*github.Notification{mockNotification},
},
{
name: "success with filter=include_read_notifications",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetNotifications,
[]*github.Notification{mockNotification},
),
),
requestArgs: map[string]interface{}{
"filter": "include_read_notifications",
},
expectError: false,
expectedResult: []*github.Notification{mockNotification},
},
{
name: "success with filter=only_participating",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetNotifications,
[]*github.Notification{mockNotification},
),
),
requestArgs: map[string]interface{}{
"filter": "only_participating",
},
expectError: false,
expectedResult: []*github.Notification{mockNotification},
},
{
name: "success for repo notifications",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetReposNotificationsByOwnerByRepo,
[]*github.Notification{mockNotification},
),
),
requestArgs: map[string]interface{}{
"filter": "default",
"since": "2024-01-01T00:00:00Z",
"before": "2024-01-02T00:00:00Z",
"owner": "octocat",
"repo": "hello-world",
"page": float64(2),
"perPage": float64(10),
},
expectError: false,
expectedResult: []*github.Notification{mockNotification},
},
{
name: "error",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetNotifications,
mockResponse(t, http.StatusInternalServerError, `{"message": "error"}`),
),
),
requestArgs: map[string]interface{}{},
expectError: true,
expectedErrMsg: "error",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := ListNotifications(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
}
return
}
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
t.Logf("textContent: %s", textContent.Text)
var returned []*github.Notification
err = json.Unmarshal([]byte(textContent.Text), &returned)
require.NoError(t, err)
require.NotEmpty(t, returned)
assert.Equal(t, *tc.expectedResult[0].ID, *returned[0].ID)
})
}
}
func Test_ManageNotificationSubscription(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := ManageNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "manage_notification_subscription", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "notificationID")
assert.Contains(t, tool.InputSchema.Properties, "action")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"notificationID", "action"})
mockSub := &github.Subscription{Ignored: github.Ptr(true)}
mockSubWatch := &github.Subscription{Ignored: github.Ptr(false), Subscribed: github.Ptr(true)}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectIgnored *bool
expectDeleted bool
expectInvalid bool
expectedErrMsg string
}{
{
name: "ignore subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutNotificationsThreadsSubscriptionByThreadId,
mockSub,
),
),
requestArgs: map[string]interface{}{
"notificationID": "123",
"action": "ignore",
},
expectError: false,
expectIgnored: github.Ptr(true),
},
{
name: "watch subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutNotificationsThreadsSubscriptionByThreadId,
mockSubWatch,
),
),
requestArgs: map[string]interface{}{
"notificationID": "123",
"action": "watch",
},
expectError: false,
expectIgnored: github.Ptr(false),
},
{
name: "delete subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.DeleteNotificationsThreadsSubscriptionByThreadId,
nil,
),
),
requestArgs: map[string]interface{}{
"notificationID": "123",
"action": "delete",
},
expectError: false,
expectDeleted: true,
},
{
name: "invalid action",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"notificationID": "123",
"action": "invalid",
},
expectError: false,
expectInvalid: true,
},
{
name: "missing required notificationID",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"action": "ignore",
},
expectError: true,
},
{
name: "missing required action",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"notificationID": "123",
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := ManageNotificationSubscription(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
require.NoError(t, err)
require.NotNil(t, result)
text := getTextResult(t, result).Text
switch {
case tc.requestArgs["notificationID"] == nil:
assert.Contains(t, text, "missing required parameter: notificationID")
case tc.requestArgs["action"] == nil:
assert.Contains(t, text, "missing required parameter: action")
default:
assert.Contains(t, text, "error")
}
return
}
require.NoError(t, err)
textContent := getTextResult(t, result)
if tc.expectIgnored != nil {
var returned github.Subscription
err = json.Unmarshal([]byte(textContent.Text), &returned)
require.NoError(t, err)
assert.Equal(t, *tc.expectIgnored, *returned.Ignored)
}
if tc.expectDeleted {
assert.Contains(t, textContent.Text, "deleted")
}
if tc.expectInvalid {
assert.Contains(t, textContent.Text, "Invalid action")
}
})
}
}
func Test_ManageRepositoryNotificationSubscription(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := ManageRepositoryNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "manage_repository_notification_subscription", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "action")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "action"})
mockSub := &github.Subscription{Ignored: github.Ptr(true)}
mockWatchSub := &github.Subscription{Ignored: github.Ptr(false), Subscribed: github.Ptr(true)}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectIgnored *bool
expectSubscribed *bool
expectDeleted bool
expectInvalid bool
expectedErrMsg string
}{
{
name: "ignore subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutReposSubscriptionByOwnerByRepo,
mockSub,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"action": "ignore",
},
expectError: false,
expectIgnored: github.Ptr(true),
},
{
name: "watch subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutReposSubscriptionByOwnerByRepo,
mockWatchSub,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"action": "watch",
},
expectError: false,
expectIgnored: github.Ptr(false),
expectSubscribed: github.Ptr(true),
},
{
name: "delete subscription",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.DeleteReposSubscriptionByOwnerByRepo,
nil,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"action": "delete",
},
expectError: false,
expectDeleted: true,
},
{
name: "invalid action",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"action": "invalid",
},
expectError: false,
expectInvalid: true,
},
{
name: "missing required owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"repo": "repo",
"action": "ignore",
},
expectError: true,
},
{
name: "missing required repo",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
"action": "ignore",
},
expectError: true,
},
{
name: "missing required action",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := ManageRepositoryNotificationSubscription(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
require.NoError(t, err)
require.NotNil(t, result)
text := getTextResult(t, result).Text
switch {
case tc.requestArgs["owner"] == nil:
assert.Contains(t, text, "missing required parameter: owner")
case tc.requestArgs["repo"] == nil:
assert.Contains(t, text, "missing required parameter: repo")
case tc.requestArgs["action"] == nil:
assert.Contains(t, text, "missing required parameter: action")
default:
assert.Contains(t, text, "error")
}
return
}
require.NoError(t, err)
textContent := getTextResult(t, result)
if tc.expectIgnored != nil || tc.expectSubscribed != nil {
var returned github.Subscription
err = json.Unmarshal([]byte(textContent.Text), &returned)
require.NoError(t, err)
if tc.expectIgnored != nil {
assert.Equal(t, *tc.expectIgnored, *returned.Ignored)
}
if tc.expectSubscribed != nil {
assert.Equal(t, *tc.expectSubscribed, *returned.Subscribed)
}
}
if tc.expectDeleted {
assert.Contains(t, textContent.Text, "deleted")
}
if tc.expectInvalid {
assert.Contains(t, textContent.Text, "Invalid action")
}
})
}
}
func Test_DismissNotification(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := DismissNotification(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "dismiss_notification", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "threadID")
assert.Contains(t, tool.InputSchema.Properties, "state")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"threadID"})
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectRead bool
expectDone bool
expectInvalid bool
expectedErrMsg string
}{
{
name: "mark as read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PatchNotificationsThreadsByThreadId,
nil,
),
),
requestArgs: map[string]interface{}{
"threadID": "123",
"state": "read",
},
expectError: false,
expectRead: true,
},
{
name: "mark as done",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.DeleteNotificationsThreadsByThreadId,
nil,
),
),
requestArgs: map[string]interface{}{
"threadID": "123",
"state": "done",
},
expectError: false,
expectDone: true,
},
{
name: "invalid threadID format",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"threadID": "notanumber",
"state": "done",
},
expectError: false,
expectInvalid: true,
},
{
name: "missing required threadID",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"state": "read",
},
expectError: true,
},
{
name: "missing required state",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"threadID": "123",
},
expectError: true,
},
{
name: "invalid state value",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"threadID": "123",
"state": "invalid",
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := DismissNotification(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
// The tool returns a ToolResultError with a specific message
require.NoError(t, err)
require.NotNil(t, result)
text := getTextResult(t, result).Text
switch {
case tc.requestArgs["threadID"] == nil:
assert.Contains(t, text, "missing required parameter: threadID")
case tc.requestArgs["state"] == nil:
assert.Contains(t, text, "missing required parameter: state")
case tc.name == "invalid threadID format":
assert.Contains(t, text, "invalid threadID format")
case tc.name == "invalid state value":
assert.Contains(t, text, "Invalid state. Must be one of: read, done.")
default:
// fallback for other errors
assert.Contains(t, text, "error")
}
return
}
require.NoError(t, err)
textContent := getTextResult(t, result)
if tc.expectRead {
assert.Contains(t, textContent.Text, "Notification marked as read")
}
if tc.expectDone {
assert.Contains(t, textContent.Text, "Notification marked as done")
}
if tc.expectInvalid {
assert.Contains(t, textContent.Text, "invalid threadID format")
}
})
}
}
func Test_MarkAllNotificationsRead(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := MarkAllNotificationsRead(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "mark_all_notifications_read", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "lastReadAt")
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Empty(t, tool.InputSchema.Required)
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectMarked bool
expectedErrMsg string
}{
{
name: "success (no params)",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutNotifications,
nil,
),
),
requestArgs: map[string]interface{}{},
expectError: false,
expectMarked: true,
},
{
name: "success with lastReadAt param",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutNotifications,
nil,
),
),
requestArgs: map[string]interface{}{
"lastReadAt": "2024-01-01T00:00:00Z",
},
expectError: false,
expectMarked: true,
},
{
name: "success with owner and repo",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.PutReposNotificationsByOwnerByRepo,
nil,
),
),
requestArgs: map[string]interface{}{
"owner": "octocat",
"repo": "hello-world",
},
expectError: false,
expectMarked: true,
},
{
name: "API error",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PutNotifications,
mockResponse(t, http.StatusInternalServerError, `{"message": "error"}`),
),
),
requestArgs: map[string]interface{}{},
expectError: true,
expectedErrMsg: "error",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := MarkAllNotificationsRead(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
}
return
}
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
if tc.expectMarked {
assert.Contains(t, textContent.Text, "All notifications marked as read")
}
})
}
}
func Test_GetNotificationDetails(t *testing.T) {
// Verify tool definition and schema
mockClient := github.NewClient(nil)
tool, _ := GetNotificationDetails(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "get_notification_details", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "notificationID")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"notificationID"})
mockThread := &github.Notification{ID: github.Ptr("123"), Reason: github.Ptr("mention")}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectResult *github.Notification
expectedErrMsg string
}{
{
name: "success",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetNotificationsThreadsByThreadId,
mockThread,
),
),
requestArgs: map[string]interface{}{
"notificationID": "123",
},
expectError: false,
expectResult: mockThread,
},
{
name: "not found",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetNotificationsThreadsByThreadId,
mockResponse(t, http.StatusNotFound, `{"message": "not found"}`),
),
),
requestArgs: map[string]interface{}{
"notificationID": "123",
},
expectError: true,
expectedErrMsg: "not found",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := GetNotificationDetails(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
result, err := handler(context.Background(), request)
if tc.expectError {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
}
return
}
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
var returned github.Notification
err = json.Unmarshal([]byte(textContent.Text), &returned)
require.NoError(t, err)
assert.Equal(t, *tc.expectResult.ID, *returned.ID)
})
}
}