diff --git a/pkg/httpmock/legacy.go b/pkg/httpmock/legacy.go index ad92d0572..05938768e 100644 --- a/pkg/httpmock/legacy.go +++ b/pkg/httpmock/legacy.go @@ -6,6 +6,7 @@ import ( // TODO: clean up methods in this file when there are no more callers +// StubRepoInfoResponse registers a stub for the RepositoryInfo GraphQL query. func (r *Registry) StubRepoInfoResponse(owner, repo, branch string) { r.Register( GraphQL(`query RepositoryInfo\b`), @@ -22,14 +23,17 @@ func (r *Registry) StubRepoInfoResponse(owner, repo, branch string) { `, repo, owner, branch))) } +// StubRepoResponse registers a stub for the RepositoryNetwork GraphQL query with WRITE permission. func (r *Registry) StubRepoResponse(owner, repo string) { r.StubRepoResponseWithPermission(owner, repo, "WRITE") } +// StubRepoResponseWithPermission registers a stub for the RepositoryNetwork GraphQL query with the given permission. func (r *Registry) StubRepoResponseWithPermission(owner, repo, permission string) { r.Register(GraphQL(`query RepositoryNetwork\b`), StringResponse(RepoNetworkStubResponse(owner, repo, "master", permission))) } +// RepoNetworkStubResponse returns a JSON string representing a RepositoryNetwork GraphQL response. func RepoNetworkStubResponse(owner, repo, defaultBranch, permission string) string { return fmt.Sprintf(` { "data": { "repo_000": { diff --git a/pkg/httpmock/registry.go b/pkg/httpmock/registry.go index b7c5a117d..5445af419 100644 --- a/pkg/httpmock/registry.go +++ b/pkg/httpmock/registry.go @@ -15,12 +15,14 @@ func ReplaceTripper(client *http.Client, reg *Registry) { client.Transport = reg } +// Registry tracks HTTP stubs and records requests for verification in tests. type Registry struct { mu sync.Mutex stubs []*Stub Requests []*http.Request } +// Register adds a new stub with the given matcher and responder to the registry. func (r *Registry) Register(m Matcher, resp Responder) { r.stubs = append(r.stubs, &Stub{ Stack: string(debug.Stack()), @@ -29,6 +31,7 @@ func (r *Registry) Register(m Matcher, resp Responder) { }) } +// Exclude registers a stub that causes the test to fail if a matching HTTP request is made. func (r *Registry) Exclude(t *testing.T, m Matcher) { registrationStack := string(debug.Stack()) @@ -52,11 +55,13 @@ func (r *Registry) Exclude(t *testing.T, m Matcher) { r.stubs = append(r.stubs, excludedStub) } +// Testing is an interface for test error reporting used by Verify. type Testing interface { Errorf(string, ...interface{}) Helper() } +// Verify reports an error if any registered stubs were not matched by an HTTP request. func (r *Registry) Verify(t Testing) { var unmatchedStubStacks []string for _, s := range r.stubs { diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index a5444b2c8..63d7621af 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -13,9 +13,13 @@ import ( "github.com/cli/go-gh/v2/pkg/api" ) +// Matcher is a function that reports whether an HTTP request matches a stub. type Matcher func(req *http.Request) bool + +// Responder is a function that generates an HTTP response for a matched request. type Responder func(req *http.Request) (*http.Response, error) +// Stub pairs a Matcher with a Responder and tracks whether the stub has been matched. type Stub struct { Stack string matched bool @@ -24,6 +28,7 @@ type Stub struct { exclude bool } +// MatchAny is a Matcher that matches every HTTP request. func MatchAny(*http.Request) bool { return true } @@ -41,6 +46,7 @@ func REST(method, p string) Matcher { } } +// GraphQL returns a Matcher that matches POST requests to the GraphQL endpoint whose query matches the given regex. func GraphQL(q string) Matcher { re := regexp.MustCompile(q) @@ -61,6 +67,7 @@ func GraphQL(q string) Matcher { } } +// GraphQLMutationMatcher returns a Matcher for GraphQL mutations whose query matches the regex and whose input satisfies cb. func GraphQLMutationMatcher(q string, cb func(map[string]interface{}) bool) Matcher { re := regexp.MustCompile(q) @@ -88,6 +95,7 @@ func GraphQLMutationMatcher(q string, cb func(map[string]interface{}) bool) Matc } } +// QueryMatcher returns a Matcher that matches REST requests by method, path, and query parameters. func QueryMatcher(method string, path string, query url.Values) Matcher { return func(req *http.Request) bool { if !REST(method, path)(req) { @@ -121,18 +129,21 @@ func decodeJSONBody(req *http.Request, dest interface{}) error { return json.Unmarshal(b, dest) } +// StringResponse returns a Responder that replies with a 200 status and the given string body. func StringResponse(body string) Responder { return func(req *http.Request) (*http.Response, error) { return httpResponse(200, req, bytes.NewBufferString(body)), nil } } +// BinaryResponse returns a Responder that replies with a 200 status and the given byte slice body. func BinaryResponse(body []byte) Responder { return func(req *http.Request) (*http.Response, error) { return httpResponse(200, req, bytes.NewBuffer(body)), nil } } +// WithHost wraps a Matcher to additionally require the request host to match the given host. func WithHost(matcher Matcher, host string) Matcher { return func(req *http.Request) bool { if !strings.EqualFold(req.Host, host) { @@ -142,6 +153,7 @@ func WithHost(matcher Matcher, host string) Matcher { } } +// WithHeader wraps a Responder to add the specified header to the response. func WithHeader(responder Responder, header string, value string) Responder { return func(req *http.Request) (*http.Response, error) { resp, _ := responder(req) @@ -153,12 +165,14 @@ func WithHeader(responder Responder, header string, value string) Responder { } } +// StatusStringResponse returns a Responder that replies with the given status code and string body. func StatusStringResponse(status int, body string) Responder { return func(req *http.Request) (*http.Response, error) { return httpResponse(status, req, bytes.NewBufferString(body)), nil } } +// JSONResponse returns a Responder that JSON-encodes the given value and replies with a 200 status. func JSONResponse(body interface{}) Responder { return func(req *http.Request) (*http.Response, error) { b, _ := json.Marshal(body) @@ -188,6 +202,7 @@ func JSONErrorResponse(status int, err api.HTTPError) Responder { return StatusJSONResponse(status, err) } +// FileResponse returns a Responder that replies with the contents of the named file. func FileResponse(filename string) Responder { return func(req *http.Request) (*http.Response, error) { f, err := os.Open(filename) @@ -198,6 +213,7 @@ func FileResponse(filename string) Responder { } } +// RESTPayload returns a Responder that decodes the JSON request body, passes it to cb, and replies with the given status and body. func RESTPayload(responseStatus int, responseBody string, cb func(payload map[string]interface{})) Responder { return func(req *http.Request) (*http.Response, error) { bodyData := make(map[string]interface{}) @@ -214,6 +230,7 @@ func RESTPayload(responseStatus int, responseBody string, cb func(payload map[st } } +// GraphQLMutation returns a Responder that decodes a GraphQL mutation's input variables, passes them to cb, and replies with body. func GraphQLMutation(body string, cb func(map[string]interface{})) Responder { return func(req *http.Request) (*http.Response, error) { var bodyData struct { @@ -231,6 +248,7 @@ func GraphQLMutation(body string, cb func(map[string]interface{})) Responder { } } +// GraphQLQuery returns a Responder that decodes a GraphQL query and its variables, passes them to cb, and replies with body. func GraphQLQuery(body string, cb func(string, map[string]interface{})) Responder { return func(req *http.Request) (*http.Response, error) { var bodyData struct {