Skip to content

Commit 7ab8bcb

Browse files
CopilotJoannaaKL
andcommitted
Complete pilot migration and add migration documentation
- Migrate pkg/github/code_scanning_test.go to new infrastructure - Add comprehensive migration documentation in docs/testing-migration.md - Fix linter warning in helper_test.go - All tests and lint checks pass - 2 of 16 test files migrated, 14 remaining for incremental migration Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com>
1 parent ac61a9f commit 7ab8bcb

File tree

3 files changed

+171
-54
lines changed

3 files changed

+171
-54
lines changed

docs/testing-migration.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Testing Infrastructure Migration
2+
3+
## Overview
4+
5+
This document describes the ongoing migration from `migueleliasweb/go-github-mock` to `stretchr/testify`-based HTTP mocking infrastructure.
6+
7+
## Motivation
8+
9+
As described in issue #1492, we are migrating from `go-github-mock` to consolidate our testing dependencies:
10+
11+
1. **Dependency Consolidation**: We already use `stretchr/testify` for assertions
12+
2. **Maintenance**: Reduce the number of external dependencies
13+
3. **Flexibility**: Custom HTTP mocking provides more control over test scenarios
14+
4. **Clarity**: Simpler, more readable test setup
15+
16+
## New Testing Infrastructure
17+
18+
The new mocking infrastructure is located in `pkg/github/helper_test.go` and provides:
19+
20+
### MockHTTPClientWithHandlers
21+
22+
Creates an HTTP client with multiple handlers for different API endpoints:
23+
24+
```go
25+
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
26+
"GET /repos/{owner}/{repo}": mockResponse(t, http.StatusOK, mockRepo),
27+
"GET /repos/{owner}/{repo}/git/trees/{tree}": mockResponse(t, http.StatusOK, mockTree),
28+
})
29+
```
30+
31+
### Path Pattern Matching
32+
33+
Supports GitHub API path patterns with parameter placeholders:
34+
- `{owner}`, `{repo}`, `{tree}`, etc. are treated as wildcards
35+
- Exact paths are matched first, then patterns
36+
- Query parameters can be validated using the existing `expectQueryParams` helper
37+
38+
### Helper Functions
39+
40+
- `mockResponse(t, statusCode, body)` - Creates JSON responses
41+
- `expectQueryParams(t, params).andThen(handler)` - Validates query parameters
42+
- `MockHTTPClientWithHandler(handler)` - Single handler for all requests
43+
44+
## Migration Pattern
45+
46+
### Before (using go-github-mock):
47+
48+
```go
49+
mockedClient := mock.NewMockedHTTPClient(
50+
mock.WithRequestMatch(
51+
mock.GetReposByOwnerByRepo,
52+
mockRepo,
53+
),
54+
mock.WithRequestMatchHandler(
55+
mock.GetReposGitTreesByOwnerByRepoByTreeSha,
56+
mockResponse(t, http.StatusOK, mockTree),
57+
),
58+
)
59+
```
60+
61+
### After (using new infrastructure):
62+
63+
```go
64+
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
65+
"GET /repos/{owner}/{repo}": mockResponse(t, http.StatusOK, mockRepo),
66+
"GET /repos/{owner}/{repo}/git/trees/{tree}": mockResponse(t, http.StatusOK, mockTree),
67+
})
68+
```
69+
70+
## Migration Status
71+
72+
### Completed
73+
-`pkg/github/git_test.go` - 196 lines (pilot migration)
74+
-`pkg/github/code_scanning_test.go` - 258 lines
75+
76+
### Remaining (in order of size, smallest first)
77+
-`pkg/github/secret_scanning_test.go` - 263 lines
78+
-`pkg/github/dependabot_test.go` - 269 lines
79+
-`pkg/github/repository_resource_test.go` - 314 lines
80+
-`pkg/github/context_tools_test.go` - 498 lines
81+
-`pkg/github/security_advisories_test.go` - 546 lines
82+
-`pkg/github/gists_test.go` - 621 lines
83+
-`pkg/github/search_test.go` - 760 lines
84+
-`pkg/github/notifications_test.go` - 783 lines
85+
-`pkg/github/actions_test.go` - 1442 lines
86+
-`pkg/github/projects_test.go` - 1684 lines
87+
-`pkg/github/pullrequests_test.go` - 3263 lines
88+
-`pkg/github/repositories_test.go` - 3472 lines
89+
-`pkg/github/issues_test.go` - 3705 lines
90+
-`pkg/raw/raw_mock.go` - Special case (defines endpoint patterns)
91+
92+
**Total remaining: ~14,210 lines of test code across 14 files**
93+
94+
## API Endpoint Patterns
95+
96+
Common GitHub API endpoints and their patterns:
97+
98+
| Endpoint | Pattern |
99+
|----------|---------|
100+
| Get Repository | `GET /repos/{owner}/{repo}` |
101+
| Get Tree | `GET /repos/{owner}/{repo}/git/trees/{tree}` |
102+
| Get Issue | `GET /repos/{owner}/{repo}/issues/{issue_number}` |
103+
| List Issues | `GET /repos/{owner}/{repo}/issues` |
104+
| Get Pull Request | `GET /repos/{owner}/{repo}/pulls/{pull_number}` |
105+
| Code Scanning Alert | `GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}` |
106+
| Secret Scanning Alert | `GET /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}` |
107+
108+
For a complete reference, see the [GitHub REST API documentation](https://docs.github.com/en/rest).
109+
110+
## Testing Best Practices
111+
112+
1. **Use pattern matching for flexibility**: `{owner}`, `{repo}`, etc.
113+
2. **Validate query parameters when needed**: Use `expectQueryParams`
114+
3. **Keep test setup close to test cases**: Define handlers inline
115+
4. **Reuse mock data across test cases**: Define once at the top
116+
5. **Test both success and failure cases**: Include error scenarios
117+
118+
## Next Steps
119+
120+
1. Continue migrating test files incrementally (smallest first)
121+
2. Once all files are migrated, remove `go-github-mock` dependency
122+
3. Update `pkg/raw/raw_mock.go` to use new patterns
123+
4. Run `go mod tidy` to clean up dependencies
124+
5. Update third-party licenses with `./script/licenses`
125+
126+
## References
127+
128+
- Issue: #1492
129+
- testify documentation: https://pkg.go.dev/github.com/stretchr/testify
130+
- GitHub API: https://docs.github.com/en/rest

pkg/github/code_scanning_test.go

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/github/github-mcp-server/pkg/translations"
1111
"github.com/google/go-github/v79/github"
1212
"github.com/google/jsonschema-go/jsonschema"
13-
"github.com/migueleliasweb/go-github-mock/src/mock"
1413
"github.com/stretchr/testify/assert"
1514
"github.com/stretchr/testify/require"
1615
)
@@ -50,12 +49,9 @@ func Test_GetCodeScanningAlert(t *testing.T) {
5049
}{
5150
{
5251
name: "successful alert fetch",
53-
mockedClient: mock.NewMockedHTTPClient(
54-
mock.WithRequestMatch(
55-
mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber,
56-
mockAlert,
57-
),
58-
),
52+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
53+
"GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}": mockResponse(t, http.StatusOK, mockAlert),
54+
}),
5955
requestArgs: map[string]interface{}{
6056
"owner": "owner",
6157
"repo": "repo",
@@ -66,15 +62,12 @@ func Test_GetCodeScanningAlert(t *testing.T) {
6662
},
6763
{
6864
name: "alert fetch fails",
69-
mockedClient: mock.NewMockedHTTPClient(
70-
mock.WithRequestMatchHandler(
71-
mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber,
72-
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
73-
w.WriteHeader(http.StatusNotFound)
74-
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
75-
}),
76-
),
77-
),
65+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
66+
"GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
67+
w.WriteHeader(http.StatusNotFound)
68+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
69+
}),
70+
}),
7871
requestArgs: map[string]interface{}{
7972
"owner": "owner",
8073
"repo": "repo",
@@ -171,19 +164,16 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
171164
}{
172165
{
173166
name: "successful alerts listing",
174-
mockedClient: mock.NewMockedHTTPClient(
175-
mock.WithRequestMatchHandler(
176-
mock.GetReposCodeScanningAlertsByOwnerByRepo,
177-
expectQueryParams(t, map[string]string{
178-
"ref": "main",
179-
"state": "open",
180-
"severity": "high",
181-
"tool_name": "codeql",
182-
}).andThen(
183-
mockResponse(t, http.StatusOK, mockAlerts),
184-
),
167+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
168+
"GET /repos/{owner}/{repo}/code-scanning/alerts": expectQueryParams(t, map[string]string{
169+
"ref": "main",
170+
"state": "open",
171+
"severity": "high",
172+
"tool_name": "codeql",
173+
}).andThen(
174+
mockResponse(t, http.StatusOK, mockAlerts),
185175
),
186-
),
176+
}),
187177
requestArgs: map[string]interface{}{
188178
"owner": "owner",
189179
"repo": "repo",
@@ -197,15 +187,12 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
197187
},
198188
{
199189
name: "alerts listing fails",
200-
mockedClient: mock.NewMockedHTTPClient(
201-
mock.WithRequestMatchHandler(
202-
mock.GetReposCodeScanningAlertsByOwnerByRepo,
203-
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
204-
w.WriteHeader(http.StatusUnauthorized)
205-
_, _ = w.Write([]byte(`{"message": "Unauthorized access"}`))
206-
}),
207-
),
208-
),
190+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
191+
"GET /repos/{owner}/{repo}/code-scanning/alerts": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
192+
w.WriteHeader(http.StatusUnauthorized)
193+
_, _ = w.Write([]byte(`{"message": "Unauthorized access"}`))
194+
}),
195+
}),
209196
requestArgs: map[string]interface{}{
210197
"owner": "owner",
211198
"repo": "repo",

pkg/github/helper_test.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func NewMockRoundTripper() *MockRoundTripper {
295295
func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
296296
// Normalize the request path and method for matching
297297
key := req.Method + " " + req.URL.Path
298-
298+
299299
// Check if we have a specific handler for this request
300300
if handler, ok := m.handlers[key]; ok {
301301
// Use httptest.ResponseRecorder to capture the handler's response
@@ -304,15 +304,15 @@ func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
304304
body: &bytes.Buffer{},
305305
}
306306
handler(recorder, req)
307-
307+
308308
return &http.Response{
309309
StatusCode: recorder.statusCode,
310310
Header: recorder.header,
311311
Body: io.NopCloser(bytes.NewReader(recorder.body.Bytes())),
312312
Request: req,
313313
}, nil
314314
}
315-
315+
316316
// Fall back to mock.Mock assertions if defined
317317
args := m.Called(req)
318318
if args.Get(0) == nil {
@@ -329,7 +329,7 @@ func (m *MockRoundTripper) OnRequest(method, path string, handler http.HandlerFu
329329
}
330330

331331
// OnAny registers a catch-all handler
332-
func (m *MockRoundTripper) OnAny(handler http.HandlerFunc) *MockRoundTripper {
332+
func (m *MockRoundTripper) OnAny(_ http.HandlerFunc) *MockRoundTripper {
333333
// This is a simple implementation that doesn't use the handler map
334334
// It's kept for compatibility but not recommended
335335
return m
@@ -370,15 +370,15 @@ func matchPath(pattern, path string) bool {
370370
if pattern == path {
371371
return true
372372
}
373-
373+
374374
// Support for path parameters like /repos/{owner}/{repo}/issues/{issue_number}
375375
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
376376
pathParts := strings.Split(strings.Trim(path, "/"), "/")
377-
377+
378378
if len(patternParts) != len(pathParts) {
379379
return false
380380
}
381-
381+
382382
for i := range patternParts {
383383
// Check if this is a path parameter (enclosed in {})
384384
if strings.HasPrefix(patternParts[i], "{") && strings.HasSuffix(patternParts[i], "}") {
@@ -388,7 +388,7 @@ func matchPath(pattern, path string) bool {
388388
return false
389389
}
390390
}
391-
391+
392392
return true
393393
}
394394

@@ -408,7 +408,7 @@ func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
408408
body: &bytes.Buffer{},
409409
}
410410
m.handler(recorder, req)
411-
411+
412412
return &http.Response{
413413
StatusCode: recorder.statusCode,
414414
Header: recorder.header,
@@ -430,23 +430,23 @@ type multiHandlerTransport struct {
430430
func (m *multiHandlerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
431431
// Try to find a handler for this request
432432
key := req.Method + " " + req.URL.Path
433-
433+
434434
// First try exact match
435435
if handler, ok := m.handlers[key]; ok {
436436
recorder := &responseRecorder{
437437
header: make(http.Header),
438438
body: &bytes.Buffer{},
439439
}
440440
handler(recorder, req)
441-
441+
442442
return &http.Response{
443443
StatusCode: recorder.statusCode,
444444
Header: recorder.header,
445445
Body: io.NopCloser(bytes.NewReader(recorder.body.Bytes())),
446446
Request: req,
447447
}, nil
448448
}
449-
449+
450450
// Then try pattern matching
451451
for pattern, handler := range m.handlers {
452452
parts := strings.SplitN(pattern, " ", 2)
@@ -458,7 +458,7 @@ func (m *multiHandlerTransport) RoundTrip(req *http.Request) (*http.Response, er
458458
body: &bytes.Buffer{},
459459
}
460460
handler(recorder, req)
461-
461+
462462
return &http.Response{
463463
StatusCode: recorder.statusCode,
464464
Header: recorder.header,
@@ -468,7 +468,7 @@ func (m *multiHandlerTransport) RoundTrip(req *http.Request) (*http.Response, er
468468
}
469469
}
470470
}
471-
471+
472472
// No handler found
473473
return &http.Response{
474474
StatusCode: http.StatusNotFound,
@@ -482,18 +482,18 @@ func extractPathParams(pattern, path string) map[string]string {
482482
params := make(map[string]string)
483483
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
484484
pathParts := strings.Split(strings.Trim(path, "/"), "/")
485-
485+
486486
if len(patternParts) != len(pathParts) {
487487
return params
488488
}
489-
489+
490490
for i := range patternParts {
491491
if strings.HasPrefix(patternParts[i], "{") && strings.HasSuffix(patternParts[i], "}") {
492492
paramName := strings.Trim(patternParts[i], "{}")
493493
params[paramName] = pathParts[i]
494494
}
495495
}
496-
496+
497497
return params
498498
}
499499

0 commit comments

Comments
 (0)