package github import ( "context" "encoding/json" "net/http" "testing" "time" "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_ListGists(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) tool, _ := ListGists(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "list_gists", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "username") assert.Contains(t, tool.InputSchema.Properties, "since") assert.Contains(t, tool.InputSchema.Properties, "page") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Empty(t, tool.InputSchema.Required) // Setup mock gists for success case mockGists := []*github.Gist{ { ID: github.Ptr("gist1"), Description: github.Ptr("First Gist"), HTMLURL: github.Ptr("https://gist.github.com/user/gist1"), Public: github.Ptr(true), CreatedAt: &github.Timestamp{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)}, Owner: &github.User{Login: github.Ptr("user")}, Files: map[github.GistFilename]github.GistFile{ "file1.txt": { Filename: github.Ptr("file1.txt"), Content: github.Ptr("content of file 1"), }, }, }, { ID: github.Ptr("gist2"), Description: github.Ptr("Second Gist"), HTMLURL: github.Ptr("https://gist.github.com/testuser/gist2"), Public: github.Ptr(false), CreatedAt: &github.Timestamp{Time: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)}, Owner: &github.User{Login: github.Ptr("testuser")}, Files: map[github.GistFilename]github.GistFile{ "file2.js": { Filename: github.Ptr("file2.js"), Content: github.Ptr("console.log('hello');"), }, }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedGists []*github.Gist expectedErrMsg string }{ { name: "list authenticated user's gists", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetGists, mockGists, ), ), requestArgs: map[string]interface{}{}, expectError: false, expectedGists: mockGists, }, { name: "list specific user's gists", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetUsersGistsByUsername, mockResponse(t, http.StatusOK, mockGists), ), ), requestArgs: map[string]interface{}{ "username": "testuser", }, expectError: false, expectedGists: mockGists, }, { name: "list gists with pagination and since parameter", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetGists, expectQueryParams(t, map[string]string{ "since": "2023-01-01T00:00:00Z", "page": "2", "per_page": "5", }).andThen( mockResponse(t, http.StatusOK, mockGists), ), ), ), requestArgs: map[string]interface{}{ "since": "2023-01-01T00:00:00Z", "page": float64(2), "perPage": float64(5), }, expectError: false, expectedGists: mockGists, }, { name: "invalid since parameter", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetGists, mockGists, ), ), requestArgs: map[string]interface{}{ "since": "invalid-date", }, expectError: true, expectedErrMsg: "invalid since timestamp", }, { name: "list gists fails with error", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetGists, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message": "Requires authentication"}`)) }), ), ), requestArgs: map[string]interface{}{}, expectError: true, expectedErrMsg: "failed to list gists", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := ListGists(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { if err != nil { assert.Contains(t, err.Error(), tc.expectedErrMsg) } else { // For errors returned as part of the result, not as an error assert.NotNil(t, result) textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedGists []*github.Gist err = json.Unmarshal([]byte(textContent.Text), &returnedGists) require.NoError(t, err) assert.Len(t, returnedGists, len(tc.expectedGists)) for i, gist := range returnedGists { assert.Equal(t, *tc.expectedGists[i].ID, *gist.ID) assert.Equal(t, *tc.expectedGists[i].Description, *gist.Description) assert.Equal(t, *tc.expectedGists[i].HTMLURL, *gist.HTMLURL) assert.Equal(t, *tc.expectedGists[i].Public, *gist.Public) } }) } } func Test_CreateGist(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) tool, _ := CreateGist(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_gist", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "description") assert.Contains(t, tool.InputSchema.Properties, "filename") assert.Contains(t, tool.InputSchema.Properties, "content") assert.Contains(t, tool.InputSchema.Properties, "public") // Verify required parameters assert.Contains(t, tool.InputSchema.Required, "filename") assert.Contains(t, tool.InputSchema.Required, "content") // Setup mock data for test cases createdGist := &github.Gist{ ID: github.Ptr("new-gist-id"), Description: github.Ptr("Test Gist"), HTMLURL: github.Ptr("https://gist.github.com/user/new-gist-id"), Public: github.Ptr(false), CreatedAt: &github.Timestamp{Time: time.Now()}, Owner: &github.User{Login: github.Ptr("user")}, Files: map[github.GistFilename]github.GistFile{ "test.go": { Filename: github.Ptr("test.go"), Content: github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedErrMsg string expectedGist *github.Gist }{ { name: "create gist successfully", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostGists, mockResponse(t, http.StatusCreated, createdGist), ), ), requestArgs: map[string]interface{}{ "filename": "test.go", "content": "package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}", "description": "Test Gist", "public": false, }, expectError: false, expectedGist: createdGist, }, { name: "missing required filename", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "content": "test content", "description": "Test Gist", }, expectError: true, expectedErrMsg: "missing required parameter: filename", }, { name: "missing required content", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "filename": "test.go", "description": "Test Gist", }, expectError: true, expectedErrMsg: "missing required parameter: content", }, { name: "api returns error", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostGists, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message": "Requires authentication"}`)) }), ), ), requestArgs: map[string]interface{}{ "filename": "test.go", "content": "package main", "description": "Test Gist", }, expectError: true, expectedErrMsg: "failed to create gist", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := CreateGist(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { if err != nil { assert.Contains(t, err.Error(), tc.expectedErrMsg) } else { // For errors returned as part of the result, not as an error assert.NotNil(t, result) textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) assert.NotNil(t, result) // Parse the result and get the text content textContent := getTextResult(t, result) // Unmarshal and verify the result var gist *github.Gist err = json.Unmarshal([]byte(textContent.Text), &gist) require.NoError(t, err) assert.Equal(t, *tc.expectedGist.ID, *gist.ID) assert.Equal(t, *tc.expectedGist.Description, *gist.Description) assert.Equal(t, *tc.expectedGist.HTMLURL, *gist.HTMLURL) assert.Equal(t, *tc.expectedGist.Public, *gist.Public) // Verify file content for filename, expectedFile := range tc.expectedGist.Files { actualFile, exists := gist.Files[filename] assert.True(t, exists) assert.Equal(t, *expectedFile.Filename, *actualFile.Filename) assert.Equal(t, *expectedFile.Content, *actualFile.Content) } }) } } func Test_UpdateGist(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) tool, _ := UpdateGist(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "update_gist", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "gist_id") assert.Contains(t, tool.InputSchema.Properties, "description") assert.Contains(t, tool.InputSchema.Properties, "filename") assert.Contains(t, tool.InputSchema.Properties, "content") // Verify required parameters assert.Contains(t, tool.InputSchema.Required, "gist_id") assert.Contains(t, tool.InputSchema.Required, "filename") assert.Contains(t, tool.InputSchema.Required, "content") // Setup mock data for test cases updatedGist := &github.Gist{ ID: github.Ptr("existing-gist-id"), Description: github.Ptr("Updated Test Gist"), HTMLURL: github.Ptr("https://gist.github.com/user/existing-gist-id"), Public: github.Ptr(true), UpdatedAt: &github.Timestamp{Time: time.Now()}, Owner: &github.User{Login: github.Ptr("user")}, Files: map[github.GistFilename]github.GistFile{ "updated.go": { Filename: github.Ptr("updated.go"), Content: github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedErrMsg string expectedGist *github.Gist }{ { name: "update gist successfully", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchGistsByGistId, mockResponse(t, http.StatusOK, updatedGist), ), ), requestArgs: map[string]interface{}{ "gist_id": "existing-gist-id", "filename": "updated.go", "content": "package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}", "description": "Updated Test Gist", }, expectError: false, expectedGist: updatedGist, }, { name: "missing required gist_id", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "filename": "updated.go", "content": "updated content", "description": "Updated Test Gist", }, expectError: true, expectedErrMsg: "missing required parameter: gist_id", }, { name: "missing required filename", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "gist_id": "existing-gist-id", "content": "updated content", "description": "Updated Test Gist", }, expectError: true, expectedErrMsg: "missing required parameter: filename", }, { name: "missing required content", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "gist_id": "existing-gist-id", "filename": "updated.go", "description": "Updated Test Gist", }, expectError: true, expectedErrMsg: "missing required parameter: content", }, { name: "api returns error", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchGistsByGistId, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "gist_id": "nonexistent-gist-id", "filename": "updated.go", "content": "package main", "description": "Updated Test Gist", }, expectError: true, expectedErrMsg: "failed to update gist", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := UpdateGist(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { if err != nil { assert.Contains(t, err.Error(), tc.expectedErrMsg) } else { // For errors returned as part of the result, not as an error assert.NotNil(t, result) textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) assert.NotNil(t, result) // Parse the result and get the text content textContent := getTextResult(t, result) // Unmarshal and verify the result var gist *github.Gist err = json.Unmarshal([]byte(textContent.Text), &gist) require.NoError(t, err) assert.Equal(t, *tc.expectedGist.ID, *gist.ID) assert.Equal(t, *tc.expectedGist.Description, *gist.Description) assert.Equal(t, *tc.expectedGist.HTMLURL, *gist.HTMLURL) // Verify file content for filename, expectedFile := range tc.expectedGist.Files { actualFile, exists := gist.Files[filename] assert.True(t, exists) assert.Equal(t, *expectedFile.Filename, *actualFile.Filename) assert.Equal(t, *expectedFile.Content, *actualFile.Content) } }) } }