huma/graphql_test.go
2022-07-27 12:05:32 -07:00

332 lines
9.2 KiB
Go

package huma
import (
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type CategoryParam struct {
CategoryID string `path:"category-id"`
}
type CategorySummary struct {
ID string `json:"id" graphParam:"category-id" doc:"Category ID"`
}
type Category struct {
CategorySummary
Featured bool `json:"featured" doc:"Display as featured in the app"`
Code []byte `json:"code" doc:"Category code"`
products map[string]*Product `json:"-"`
}
type ProductParam struct {
ProductID string `path:"product-id"`
}
type ProductSummary struct {
ID string `json:"id" graphParam:"product-id" doc:"Product ID"`
}
type Product struct {
ProductSummary
SuggestedPrice float32 `json:"suggested_price"`
Created *time.Time `json:"created,omitempty" doc:"When this product was created"`
Metadata map[string]string `json:"metadata,omitempty" doc:"Additional information about the product"`
Empty *struct{} `json:"empty"`
stores map[string]*Store `json:"-"`
}
type StoreSummary struct {
ID string `json:"id" graphParam:"store-id" doc:"Store ID"`
}
type Store struct {
StoreSummary
URL string `json:"url" doc:"Web link to buy product"`
}
func TestGraphQL(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2022-02-22T22:22:22Z")
amazon := &Store{StoreSummary: StoreSummary{ID: "amazon"}, URL: "https://www.amazon.com/"}
target := &Store{StoreSummary: StoreSummary{ID: "target"}, URL: "https://www.target.com/"}
xsx := &Product{ProductSummary: ProductSummary{ID: "xbox_series_x"}, SuggestedPrice: 499.99, Created: &now, Metadata: map[string]string{"foo": "bar"}, stores: map[string]*Store{"amazon": amazon, "target": target}, Empty: &struct{}{}}
ps5 := &Product{ProductSummary: ProductSummary{ID: "playstation_ps5"}, SuggestedPrice: 499.99, Created: &now, stores: map[string]*Store{"amazon": amazon}}
ns := &Product{ProductSummary: ProductSummary{ID: "nintendo_switch"}, SuggestedPrice: 349.99, stores: map[string]*Store{"target": target}}
videoGames := &Category{
CategorySummary: CategorySummary{ID: "video_games"},
Featured: true,
Code: []byte{'h', 'i'},
products: map[string]*Product{
"xbox_series_x": xsx,
"playstation_ps5": ps5,
"nintendo_switch": ns,
},
}
categories := map[string]*Category{
"video_games": videoGames,
}
app := newTestRouter()
categoriesResource := app.Resource("/categories")
categoriesResource.Get("get-categories", "doc",
NewResponse(http.StatusOK, "").Model([]CategorySummary{}).Headers("link"),
).Run(func(ctx Context, input struct {
Cursor string `query:"cursor"`
Limit int `query:"limit" default:"10"`
}) {
summaries := []CategorySummary{}
for _, cat := range categories {
summaries = append(summaries, cat.CategorySummary)
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].ID < summaries[j].ID
})
if input.Limit == 0 {
input.Limit = 10
}
if len(summaries) < input.Limit {
input.Limit = len(summaries)
}
ctx.Header().Set("Link", "</categories?cursor=abc123>; rel=\"next\"")
ctx.WriteModel(http.StatusOK, summaries[:input.Limit])
})
categoriesResource.Delete("delete-category", "doc",
NewResponse(http.StatusNoContent, ""),
).Run(func(ctx Context) {
ctx.WriteHeader(http.StatusNoContent)
})
categoriesResource.SubResource("/{category-id}").Get("get-category", "doc",
NewResponse(http.StatusOK, "").Model(&Category{}),
NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
).Run(func(ctx Context, input struct {
CategoryParam
}) {
if categories[input.CategoryID] == nil {
ctx.WriteError(http.StatusNotFound, "Not found")
return
}
ctx.WriteModel(http.StatusOK, categories[input.CategoryID])
})
app.Resource("/categories/{category-id}/products").Get("get-items", "doc",
NewResponse(http.StatusOK, "").Model([]ProductSummary{}),
NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
).Run(func(ctx Context, input struct {
CategoryParam
}) {
if categories[input.CategoryID] == nil {
ctx.WriteError(http.StatusNotFound, "Not found")
return
}
summaries := []ProductSummary{}
for _, item := range categories[input.CategoryID].products {
summaries = append(summaries, item.ProductSummary)
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].ID < summaries[j].ID
})
ctx.WriteModel(http.StatusOK, summaries)
})
app.Resource("/categories/{category-id}/products/{product-id}").Get("get-item", "doc",
NewResponse(http.StatusOK, "").Model(&Product{}),
NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
).Run(func(ctx Context, input struct {
CategoryParam
ProductParam
}) {
if categories[input.CategoryID] == nil || categories[input.CategoryID].products[input.ProductID] == nil {
ctx.WriteError(http.StatusNotFound, "Not found")
return
}
ctx.WriteModel(http.StatusOK, categories[input.CategoryID].products[input.ProductID])
})
app.Resource("/categories/{category-id}/products/{product-id}/stores").Get("get-stores", "doc",
NewResponse(http.StatusOK, "").Model([]StoreSummary{}),
NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
).Run(func(ctx Context, input struct {
CategoryParam
ProductParam
}) {
if categories[input.CategoryID] == nil || categories[input.CategoryID].products[input.ProductID] == nil {
ctx.WriteError(http.StatusNotFound, "Not found")
return
}
summaries := []StoreSummary{}
for _, store := range categories[input.CategoryID].products[input.ProductID].stores {
summaries = append(summaries, store.StoreSummary)
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].ID < summaries[j].ID
})
ctx.WriteModel(http.StatusOK, summaries)
})
app.Resource("/categories/{category-id}/products/{product-id}/stores/{store-id}").Get("get-store", "doc",
NewResponse(http.StatusOK, "").Model(&Store{}),
NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
).Run(func(ctx Context, input struct {
CategoryParam
ProductParam
StoreID string `path:"store-id" doc:"Store ID"`
}) {
if categories[input.CategoryID] == nil || categories[input.CategoryID].products[input.ProductID] == nil {
ctx.WriteError(http.StatusNotFound, "Not found")
return
}
ctx.WriteModel(http.StatusOK, categories[input.CategoryID].products[input.ProductID].stores[input.StoreID])
})
app.Resource("/ignored").Get("get-ignored", "doc",
NewResponse(http.StatusOK, "").Model(struct{ ID string }{}),
).Run(func(ctx Context) {
ctx.WriteHeader(http.StatusOK)
})
app.EnableGraphQL(&GraphQLConfig{
ComplexityLimit: 250,
IgnorePrefixes: []string{"/ignored"},
})
query := strings.Replace(strings.Replace(`{
categories(limit: 1) {
headers {
link
}
links {
next {
key
value
}
}
edges {
categoriesItem {
id
featured
code
products {
edges {
productsItem {
id
suggested_price
created
metadata{
key
value
}
empty {
_
}
stores {
edges {
storesItem {
id
url
}
}
}
}
}
}
}
}
}
}`, "\n", " ", -1), "\t", "", -1)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/graphql?query="+query, nil)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.YAMLEq(t, strings.Replace(`
data:
categories:
headers:
link: </categories?cursor=abc123>; rel="next", </schemas/CategorySummaryList.json>; rel="describedby"
links:
next:
- key: cursor
value: abc123
edges:
- categoriesItem:
id: video_games
featured: true
code: aGk=
products:
edges:
- productsItem:
id: nintendo_switch
suggested_price: 349.99
created: null
metadata: null
empty: null
stores:
edges:
- storesItem:
id: target
url: https://www.target.com/
- productsItem:
id: playstation_ps5
suggested_price: 499.99
created: "2022-02-22T22:22:22Z"
metadata: null
empty: null
stores:
edges:
- storesItem:
id: amazon
url: https://www.amazon.com/
- productsItem:
id: xbox_series_x
suggested_price: 499.99
created: "2022-02-22T22:22:22Z"
metadata:
- key: foo
value: bar
empty:
_: null
stores:
edges:
- storesItem:
id: amazon
url: https://www.amazon.com/
- storesItem:
id: target
url: https://www.target.com/
`, "\t", " ", -1), w.Body.String())
// Confirm expected top-level fields in the query API.
w = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/graphql?query={__schema{queryType{fields{name}}}}", nil)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.YAMLEq(t, strings.Replace(`data:
__schema:
queryType:
fields:
- name: categories
- name: categoriesItem
- name: products
- name: productsItem
- name: stores
- name: storesItem
`, "\t", " ", -1), w.Body.String())
}