chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:57 +08:00
commit e30f8ba47c
533 changed files with 115926 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
// githubv4mock package provides a mock GraphQL server used for testing queries produced via
// shurcooL/githubv4 or shurcooL/graphql modules.
package githubv4mock
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type Matcher struct {
Request string
Variables map[string]any
Response GQLResponse
}
// NewQueryMatcher constructs a new matcher for the provided query and variables.
// If the provided query is a string, it will be used-as-is, otherwise it will be
// converted to a string using the constructQuery function taken from shurcooL/graphql.
func NewQueryMatcher(query any, variables map[string]any, response GQLResponse) Matcher {
queryString, ok := query.(string)
if !ok {
queryString = constructQuery(query, variables)
}
return Matcher{
Request: queryString,
Variables: variables,
Response: response,
}
}
// NewMutationMatcher constructs a new matcher for the provided mutation and variables.
// If the provided mutation is a string, it will be used-as-is, otherwise it will be
// converted to a string using the constructMutation function taken from shurcooL/graphql.
//
// The input parameter is a special form of variable, matching the usage in shurcooL/githubv4. It will be added
// to the query as a variable called `input`. Furthermore, it will be converted to a map[string]any
// to be used for later equality comparison, as when the http handler is called, the request body will no longer
// contain the input struct type information.
func NewMutationMatcher(mutation any, input any, variables map[string]any, response GQLResponse) Matcher {
mutationString, ok := mutation.(string)
if !ok {
// Matching shurcooL/githubv4 mutation behaviour found in https://github.com/shurcooL/githubv4/blob/48295856cce734663ddbd790ff54800f784f3193/githubv4.go#L45-L56
if variables == nil {
variables = map[string]any{"input": input}
} else {
variables["input"] = input
}
mutationString = constructMutation(mutation, variables)
m, _ := githubv4InputStructToMap(input)
variables["input"] = m
}
return Matcher{
Request: mutationString,
Variables: variables,
Response: response,
}
}
type GQLResponse struct {
Data map[string]any `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors,omitempty"`
}
// DataResponse is the happy path response constructor for a mocked GraphQL request.
func DataResponse(data map[string]any) GQLResponse {
return GQLResponse{
Data: data,
}
}
// ErrorResponse is the unhappy path response constructor for a mocked GraphQL request.\
// Note that for the moment it is only possible to return a single error message.
func ErrorResponse(errorMsg string) GQLResponse {
return GQLResponse{
Errors: []struct {
Message string `json:"message"`
}{
{
Message: errorMsg,
},
},
}
}
// githubv4InputStructToMap converts a struct to a map[string]any, it uses JSON marshalling rather than reflection
// to do so, because the json struct tags are used in the real implementation to produce the variable key names,
// and we need to ensure that when variable matching occurs in the http handler, the keys correctly match.
func githubv4InputStructToMap(s any) (map[string]any, error) {
jsonBytes, err := json.Marshal(s)
if err != nil {
return nil, err
}
var result map[string]any
err = json.Unmarshal(jsonBytes, &result)
return result, err
}
// NewMockedHTTPClient creates a new HTTP client that registers a handler for /graphql POST requests.
// For each request, an attempt will be be made to match the request body against the provided matchers.
// If a match is found, the corresponding response will be returned with StatusOK.
//
// Note that query and variable matching can be slightly fickle. The client expects an EXACT match on the query,
// which in most cases will have been constructed from a type with graphql tags. The query construction code in
// shurcooL/githubv4 uses the field types to derive the query string, thus a go string is not the same as a graphql.ID,
// even though `type ID string`. It is therefore expected that matching variables have the right type for example:
//
// githubv4mock.NewQueryMatcher(
// struct {
// Repository struct {
// PullRequest struct {
// ID githubv4.ID
// } `graphql:"pullRequest(number: $prNum)"`
// } `graphql:"repository(owner: $owner, name: $repo)"`
// }{},
// map[string]any{
// "owner": githubv4.String("owner"),
// "repo": githubv4.String("repo"),
// "prNum": githubv4.Int(42),
// },
// githubv4mock.DataResponse(
// map[string]any{
// "repository": map[string]any{
// "pullRequest": map[string]any{
// "id": "PR_kwDODKw3uc6WYN1T",
// },
// },
// },
// ),
// )
//
// To aid in variable equality checks, values are considered equal if they approximate to the same type. This is
// required because when the http handler is called, the request body no longer has the type information. This manifests
// particularly when using the githubv4.Input types which have type deffed fields in their structs. For example:
//
// type CloseIssueInput struct {
// IssueID ID `json:"issueId"`
// StateReason *IssueClosedStateReason `json:"stateReason,omitempty"`
// }
//
// This client does not currently provide a mechanism for out-of-band errors e.g. returning a 500,
// and errors are constrained to GQL errors returned in the response body with a 200 status code.
func NewMockedHTTPClient(ms ...Matcher) *http.Client {
matchers := make(map[string]Matcher, len(ms))
for _, m := range ms {
matchers[m.Request] = m
}
mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
gqlRequest, err := parseBody(r.Body)
if err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
defer func() { _ = r.Body.Close() }()
matcher, ok := matchers[gqlRequest.Query]
if !ok {
http.Error(w, fmt.Sprintf("no matcher found for query %s", gqlRequest.Query), http.StatusNotFound)
return
}
if len(gqlRequest.Variables) > 0 {
if len(gqlRequest.Variables) != len(matcher.Variables) {
http.Error(w, "variables do not have the same length", http.StatusBadRequest)
return
}
for k, v := range matcher.Variables {
if !objectsAreEqualValues(v, gqlRequest.Variables[k]) {
http.Error(w, "variable does not match", http.StatusBadRequest)
return
}
}
}
responseBody, err := json.Marshal(matcher.Response)
if err != nil {
http.Error(w, "error marshalling response", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(responseBody)
})
return &http.Client{Transport: &localRoundTripper{
handler: mux,
}}
}
type gqlRequest struct {
Query string `json:"query"`
Variables map[string]any `json:"variables,omitempty"`
}
func parseBody(r io.Reader) (gqlRequest, error) {
var req gqlRequest
err := json.NewDecoder(r).Decode(&req)
return req, err
}
func Ptr[T any](v T) *T { return &v }
@@ -0,0 +1,44 @@
// Ths contents of this file are taken from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/graphql_test.go#L155-L165
// because they are not exported by the module, and we would like to use them in building the githubv4mock test utility.
//
// The original license, copied from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/LICENSE
//
// MIT License
// Copyright (c) 2017 Dmitri Shuralyov
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package githubv4mock
import (
"net/http"
"net/http/httptest"
)
// localRoundTripper is an http.RoundTripper that executes HTTP transactions
// by using handler directly, instead of going over an HTTP connection.
type localRoundTripper struct {
handler http.Handler
}
func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
w := httptest.NewRecorder()
l.handler.ServeHTTP(w, req)
return w.Result(), nil
}
@@ -0,0 +1,113 @@
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions.go#L166
// because I do not want to take a dependency on the entire testify module just to use this equality check.
//
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
//
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
//
// MIT License
//
// Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package githubv4mock
import (
"bytes"
"reflect"
)
func objectsAreEqualValues(expected, actual any) bool {
if objectsAreEqual(expected, actual) {
return true
}
expectedValue := reflect.ValueOf(expected)
actualValue := reflect.ValueOf(actual)
if !expectedValue.IsValid() || !actualValue.IsValid() {
return false
}
expectedType := expectedValue.Type()
actualType := actualValue.Type()
if !expectedType.ConvertibleTo(actualType) {
return false
}
if !isNumericType(expectedType) || !isNumericType(actualType) {
// Attempt comparison after type conversion
return reflect.DeepEqual(
expectedValue.Convert(actualType).Interface(), actual,
)
}
// If BOTH values are numeric, there are chances of false positives due
// to overflow or underflow. So, we need to make sure to always convert
// the smaller type to a larger type before comparing.
if expectedType.Size() >= actualType.Size() {
return actualValue.Convert(expectedType).Interface() == expected
}
return expectedValue.Convert(actualType).Interface() == actual
}
// objectsAreEqual determines if two objects are considered equal.
//
// This function does no assertion of any kind.
func objectsAreEqual(expected, actual any) bool {
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
// This is required because when a nil is provided as a variable, the type is not known.
if isNil(expected) && isNil(actual) {
return true
}
exp, ok := expected.([]byte)
if !ok {
return reflect.DeepEqual(expected, actual)
}
act, ok := actual.([]byte)
if !ok {
return false
}
if exp == nil || act == nil {
return exp == nil && act == nil
}
return bytes.Equal(exp, act)
}
// isNumericType returns true if the type is one of:
// int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64,
// float32, float64, complex64, complex128
func isNumericType(t reflect.Type) bool {
return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128
}
func isNil(i any) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
default:
return false
}
}
@@ -0,0 +1,73 @@
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions_test.go#L140-L174
//
// There is a modification to test objectsAreEqualValues to check that typed nils are equal, even if their types are different.
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
//
// MIT License
//
// Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package githubv4mock
import (
"fmt"
"math"
"testing"
"time"
)
func TestObjectsAreEqualValues(t *testing.T) {
now := time.Now()
cases := []struct {
expected interface{}
actual interface{}
result bool
}{
{uint32(10), int32(10), true},
{0, nil, false},
{nil, 0, false},
{now, now.In(time.Local), false}, // should not be time zone independent
{int(270), int8(14), false}, // should handle overflow/underflow
{int8(14), int(270), false},
{[]int{270, 270}, []int8{14, 14}, false},
{complex128(1e+100 + 1e+100i), complex64(complex(math.Inf(0), math.Inf(0))), false},
{complex64(complex(math.Inf(0), math.Inf(0))), complex128(1e+100 + 1e+100i), false},
{complex128(1e+100 + 1e+100i), 270, false},
{270, complex128(1e+100 + 1e+100i), false},
{complex128(1e+100 + 1e+100i), 3.14, false},
{3.14, complex128(1e+100 + 1e+100i), false},
{complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true},
{complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true},
{(*string)(nil), nil, true}, // typed nil vs untyped nil
{(*string)(nil), (*int)(nil), true}, // different typed nils
}
for _, c := range cases {
t.Run(fmt.Sprintf("ObjectsAreEqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) {
res := objectsAreEqualValues(c.expected, c.actual)
if res != c.result {
t.Errorf("ObjectsAreEqualValues(%#v, %#v) should return %#v", c.expected, c.actual, c.result)
}
})
}
}
+157
View File
@@ -0,0 +1,157 @@
// Ths contents of this file are taken from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/query.go
// because they are not exported by the module, and we would like to use them in building the githubv4mock test utility.
//
// The original license, copied from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/LICENSE
//
// MIT License
// Copyright (c) 2017 Dmitri Shuralyov
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package githubv4mock
import (
"bytes"
"encoding/json"
"io"
"reflect"
"sort"
"github.com/shurcooL/graphql/ident"
)
func constructQuery(v any, variables map[string]any) string {
query := query(v)
if len(variables) > 0 {
return "query(" + queryArguments(variables) + ")" + query
}
return query
}
func constructMutation(v any, variables map[string]any) string {
query := query(v)
if len(variables) > 0 {
return "mutation(" + queryArguments(variables) + ")" + query
}
return "mutation" + query
}
// queryArguments constructs a minified arguments string for variables.
//
// E.g., map[string]any{"a": Int(123), "b": NewBoolean(true)} -> "$a:Int!$b:Boolean".
func queryArguments(variables map[string]any) string {
// Sort keys in order to produce deterministic output for testing purposes.
// TODO: If tests can be made to work with non-deterministic output, then no need to sort.
keys := make([]string, 0, len(variables))
for k := range variables {
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
_, _ = io.WriteString(&buf, "$")
_, _ = io.WriteString(&buf, k)
_, _ = io.WriteString(&buf, ":")
writeArgumentType(&buf, reflect.TypeOf(variables[k]), true)
// Don't insert a comma here.
// Commas in GraphQL are insignificant, and we want minified output.
// See https://spec.graphql.org/October2021/#sec-Insignificant-Commas.
}
return buf.String()
}
// writeArgumentType writes a minified GraphQL type for t to w.
// value indicates whether t is a value (required) type or pointer (optional) type.
// If value is true, then "!" is written at the end of t.
func writeArgumentType(w io.Writer, t reflect.Type, value bool) {
if t.Kind() == reflect.Ptr {
// Pointer is an optional type, so no "!" at the end of the pointer's underlying type.
writeArgumentType(w, t.Elem(), false)
return
}
switch t.Kind() {
case reflect.Slice, reflect.Array:
// List. E.g., "[Int]".
_, _ = io.WriteString(w, "[")
writeArgumentType(w, t.Elem(), true)
_, _ = io.WriteString(w, "]")
default:
// Named type. E.g., "Int".
name := t.Name()
if name == "string" { // HACK: Workaround for https://github.com/shurcooL/githubv4/issues/12.
name = "ID"
}
_, _ = io.WriteString(w, name)
}
if value {
// Value is a required type, so add "!" to the end.
_, _ = io.WriteString(w, "!")
}
}
// query uses writeQuery to recursively construct
// a minified query string from the provided struct v.
//
// E.g., struct{Foo Int, BarBaz *Boolean} -> "{foo,barBaz}".
func query(v any) string {
var buf bytes.Buffer
writeQuery(&buf, reflect.TypeOf(v), false)
return buf.String()
}
// writeQuery writes a minified query for t to w.
// If inline is true, the struct fields of t are inlined into parent struct.
func writeQuery(w io.Writer, t reflect.Type, inline bool) {
switch t.Kind() {
case reflect.Ptr, reflect.Slice:
writeQuery(w, t.Elem(), false)
case reflect.Struct:
// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
if reflect.PointerTo(t).Implements(jsonUnmarshaler) {
return
}
if !inline {
_, _ = io.WriteString(w, "{")
}
for i := 0; i < t.NumField(); i++ {
if i != 0 {
_, _ = io.WriteString(w, ",")
}
f := t.Field(i)
value, ok := f.Tag.Lookup("graphql")
inlineField := f.Anonymous && !ok
if !inlineField {
if ok {
_, _ = io.WriteString(w, value)
} else {
_, _ = io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
}
}
writeQuery(w, f.Type, inlineField)
}
if !inline {
_, _ = io.WriteString(w, "}")
}
}
}
var jsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()