mirror of
https://github.com/Fishwaldo/huma.git
synced 2025-03-15 11:21:42 +00:00
332 lines
9.2 KiB
Go
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())
|
|
}
|