forked from security/hakurei
cmd/pkgserver: embed internal/rosa metadata
This change also cleans up and reduces some unnecessary copies. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -8,35 +8,36 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"hakurei.app/internal/info"
|
"hakurei.app/internal/info"
|
||||||
"hakurei.app/internal/pkg"
|
"hakurei.app/internal/pkg"
|
||||||
"hakurei.app/internal/rosa"
|
"hakurei.app/internal/rosa"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InfoPayload struct {
|
// for lazy initialisation of serveInfo
|
||||||
|
var (
|
||||||
|
infoPayload struct {
|
||||||
|
// Current package count.
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
|
// Hakurei version, set at link time.
|
||||||
HakureiVersion string `json:"hakurei_version"`
|
HakureiVersion string `json:"hakurei_version"`
|
||||||
}
|
|
||||||
|
|
||||||
func NewInfoPayload(index *PackageIndex) InfoPayload {
|
|
||||||
count := len(index.sorts[0])
|
|
||||||
return InfoPayload{
|
|
||||||
Count: count,
|
|
||||||
HakureiVersion: info.Version(),
|
|
||||||
}
|
}
|
||||||
}
|
infoPayloadOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
func serveInfo(index *PackageIndex) func(http.ResponseWriter, *http.Request) {
|
// serveInfo returns constant system information.
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
func serveInfo(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
infoPayloadOnce.Do(func() {
|
||||||
|
infoPayload.Count = int(rosa.PresetUnexportedStart)
|
||||||
|
infoPayload.HakureiVersion = info.Version()
|
||||||
|
})
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Header().Set("Content-Type", "text/json; charset=utf-8")
|
// TODO(mae): cache entire response if no additional fields are planned
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
WritePayload(w, infoPayload)
|
||||||
WritePayload(w, NewInfoPayload(index))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) {
|
func (index *packageIndex) serveStatus() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
download := path.Dir(r.URL.Path) == "/status"
|
download := path.Dir(r.URL.Path) == "/status"
|
||||||
if index == nil {
|
if index == nil {
|
||||||
@@ -83,24 +84,7 @@ func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetPayload struct {
|
func (index *packageIndex) serveGet() http.HandlerFunc {
|
||||||
Count int `json:"count"`
|
|
||||||
Values []PackageIndexEntry `json:"values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGetPayload(values []*PackageIndexEntry) GetPayload {
|
|
||||||
count := len(values)
|
|
||||||
v := make([]PackageIndexEntry, count)
|
|
||||||
for i, _ := range values {
|
|
||||||
v[i] = *values[i]
|
|
||||||
}
|
|
||||||
return GetPayload{
|
|
||||||
Count: count,
|
|
||||||
Values: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
limit, err := strconv.Atoi(q.Get("limit"))
|
limit, err := strconv.Atoi(q.Get("limit"))
|
||||||
@@ -119,17 +103,21 @@ func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||||
WritePayload(w, NewGetPayload(values))
|
// TODO(mae): remove count field
|
||||||
|
WritePayload(w, &struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Values []*metadata `json:"values"`
|
||||||
|
}{len(values), values})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApiVersion = "v1"
|
const ApiVersion = "v1"
|
||||||
|
|
||||||
func apiRoutes(mux *http.ServeMux, index *PackageIndex) {
|
func apiRoutes(mux *http.ServeMux, index *packageIndex) {
|
||||||
mux.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo(index))
|
mux.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo)
|
||||||
mux.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), serveGet(index))
|
mux.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), index.serveGet())
|
||||||
mux.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), serveStatus(index))
|
mux.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), index.serveStatus())
|
||||||
mux.HandleFunc("GET /status/", serveStatus(index))
|
mux.HandleFunc("GET /status/", index.serveStatus())
|
||||||
}
|
}
|
||||||
|
|
||||||
func WritePayload(w http.ResponseWriter, payload any) {
|
func WritePayload(w http.ResponseWriter, payload any) {
|
||||||
|
|||||||
183
cmd/pkgserver/api_test.go
Normal file
183
cmd/pkgserver/api_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prefix is prepended to every API path.
|
||||||
|
const prefix = "/api/" + ApiVersion + "/"
|
||||||
|
|
||||||
|
func TestAPIInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
serveInfo(w, httptest.NewRequestWithContext(
|
||||||
|
t.Context(),
|
||||||
|
http.MethodGet,
|
||||||
|
prefix+"info",
|
||||||
|
nil,
|
||||||
|
))
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
checkAPIHeader(t, w.Header())
|
||||||
|
|
||||||
|
checkPayload(t, resp, struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
HakureiVersion string `json:"hakurei_version"`
|
||||||
|
}{int(rosa.PresetUnexportedStart), info.Version()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const target = prefix + "get"
|
||||||
|
|
||||||
|
index := newIndex(t)
|
||||||
|
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
index.serveGet()(w, httptest.NewRequestWithContext(
|
||||||
|
t.Context(),
|
||||||
|
http.MethodGet,
|
||||||
|
target+suffix,
|
||||||
|
nil,
|
||||||
|
))
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=invalid")
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("min", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
|
||||||
|
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
||||||
|
resp = w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
|
||||||
|
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
||||||
|
resp = w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("limit", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "index=0&sort=0&limit", 1, 100,
|
||||||
|
"limit must be an integer between 1 and 100",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
||||||
|
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sort", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
||||||
|
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest(suffix)
|
||||||
|
resp := w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
checkAPIHeader(t, w.Header())
|
||||||
|
checkPayloadFunc(t, resp, func(got *struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Values []*metadata `json:"values"`
|
||||||
|
}) bool {
|
||||||
|
return got.Count == len(want) &&
|
||||||
|
slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
||||||
|
return (a.Version == b.Version ||
|
||||||
|
a.Version == rosa.Unversioned ||
|
||||||
|
b.Version == rosa.Unversioned) &&
|
||||||
|
a.HasReport == b.HasReport &&
|
||||||
|
a.Name == b.Name &&
|
||||||
|
a.Description == b.Description &&
|
||||||
|
a.Website == b.Website
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWithSuffix("declarationAscending", "?limit=2&index=0&sort=0", []*metadata{
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(0),
|
||||||
|
Version: rosa.Std.Version(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(1),
|
||||||
|
Version: rosa.Std.Version(1),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(5),
|
||||||
|
Version: rosa.Std.Version(5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(6),
|
||||||
|
Version: rosa.Std.Version(6),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(7),
|
||||||
|
Version: rosa.Std.Version(7),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
||||||
|
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
||||||
|
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
||||||
|
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
||||||
|
{
|
||||||
|
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
||||||
|
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,86 +1,82 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"unique"
|
"unique"
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
"hakurei.app/internal/pkg"
|
||||||
"hakurei.app/internal/rosa"
|
"hakurei.app/internal/rosa"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SortOrders int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DeclarationAscending SortOrders = iota
|
declarationAscending = iota
|
||||||
DeclarationDescending
|
declarationDescending
|
||||||
NameAscending
|
nameAscending
|
||||||
NameDescending
|
nameDescending
|
||||||
limitSortOrders
|
|
||||||
|
sortOrderEnd = iota - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type PackageIndex struct {
|
// packageIndex refers to metadata by name and various sort orders.
|
||||||
sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry
|
type packageIndex struct {
|
||||||
names map[string]*PackageIndexEntry
|
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
|
||||||
|
names map[string]*metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageIndexEntry struct {
|
// metadata holds [rosa.Metadata] extended with additional information.
|
||||||
Name string `json:"name"`
|
type metadata struct {
|
||||||
Description string `json:"description,omitempty"`
|
*rosa.Metadata
|
||||||
Website string `json:"website,omitempty"`
|
|
||||||
Version string `json:"version"`
|
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
|
||||||
|
// to the zero value. Otherwise, the zero value is invalid.
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||||
HasReport bool `json:"report,omitempty"`
|
HasReport bool `json:"report,omitempty"`
|
||||||
ident unique.Handle[pkg.ID] `json:"-"`
|
|
||||||
status []byte `json:"-"`
|
// Ident resolved from underlying [pkg.Artifact].
|
||||||
|
ident unique.Handle[pkg.ID]
|
||||||
|
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
||||||
|
status []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (index *PackageIndex, err error) {
|
// populate deterministically populates packageIndex, optionally with a report.
|
||||||
index = new(PackageIndex)
|
func (index *packageIndex) populate(cache *pkg.Cache, report *rosa.Report) (err error) {
|
||||||
index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart)
|
|
||||||
work := make([]PackageIndexEntry, rosa.PresetUnexportedStart)
|
|
||||||
if report != nil {
|
if report != nil {
|
||||||
defer report.HandleAccess(&err)()
|
defer report.HandleAccess(&err)()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var work [rosa.PresetUnexportedStart]*metadata
|
||||||
|
index.names = make(map[string]*metadata)
|
||||||
for p := range rosa.PresetUnexportedStart {
|
for p := range rosa.PresetUnexportedStart {
|
||||||
m := rosa.GetMetadata(p)
|
m := metadata{
|
||||||
v := rosa.Std.Version(p)
|
Metadata: rosa.GetMetadata(p),
|
||||||
a := rosa.Std.Load(p)
|
Version: rosa.Std.Version(p),
|
||||||
entry := PackageIndexEntry{
|
|
||||||
Name: m.Name,
|
|
||||||
Description: m.Description,
|
|
||||||
Website: m.Website,
|
|
||||||
Version: v,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cache != nil && report != nil {
|
if cache != nil && report != nil {
|
||||||
entry.ident = cache.Ident(a)
|
m.ident = cache.Ident(rosa.Std.Load(p))
|
||||||
status, n := report.ArtifactOf(entry.ident)
|
status, n := report.ArtifactOf(m.ident)
|
||||||
if n >= 0 {
|
if n >= 0 {
|
||||||
entry.HasReport = true
|
m.HasReport = true
|
||||||
entry.status = status
|
m.status = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
work[p] = entry
|
work[p] = &m
|
||||||
index.names[m.Name] = &entry
|
index.names[m.Name] = &m
|
||||||
}
|
}
|
||||||
for i, p := range work {
|
|
||||||
index.sorts[DeclarationAscending][i] = &p
|
index.sorts[declarationAscending] = work
|
||||||
}
|
index.sorts[declarationDescending] = work
|
||||||
slices.Reverse(work)
|
slices.Reverse(index.sorts[declarationDescending][:])
|
||||||
for i, p := range work {
|
|
||||||
index.sorts[DeclarationDescending][i] = &p
|
index.sorts[nameAscending] = work
|
||||||
}
|
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||||
slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int {
|
return strings.Compare(a.Name, b.Name)
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
})
|
||||||
for i, p := range work {
|
index.sorts[nameDescending] = index.sorts[nameAscending]
|
||||||
index.sorts[NameAscending][i] = &p
|
slices.Reverse(index.sorts[nameDescending][:])
|
||||||
}
|
|
||||||
slices.Reverse(work)
|
|
||||||
for i, p := range work {
|
|
||||||
index.sorts[NameDescending][i] = &p
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,20 +37,20 @@ func main() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
||||||
defer cache.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer cache.Close()
|
||||||
report, err := rosa.OpenReport(reportPath)
|
report, err := rosa.OpenReport(reportPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
index, err := createPackageIndex(cache, report)
|
var index packageIndex
|
||||||
if err != nil {
|
if err = index.populate(cache, report); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uiRoutes(http.DefaultServeMux)
|
uiRoutes(http.DefaultServeMux)
|
||||||
apiRoutes(http.DefaultServeMux, index)
|
apiRoutes(http.DefaultServeMux, &index)
|
||||||
err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil)
|
err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
82
cmd/pkgserver/main_test.go
Normal file
82
cmd/pkgserver/main_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newIndex returns the address of a newly populated packageIndex.
|
||||||
|
func newIndex(t *testing.T) *packageIndex {
|
||||||
|
var index packageIndex
|
||||||
|
if err := index.populate(nil, nil); err != nil {
|
||||||
|
t.Fatalf("populate: error = %v", err)
|
||||||
|
}
|
||||||
|
return &index
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStatus checks response status code.
|
||||||
|
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
||||||
|
if resp.StatusCode != want {
|
||||||
|
t.Errorf(
|
||||||
|
"StatusCode: %s, want %s",
|
||||||
|
http.StatusText(resp.StatusCode),
|
||||||
|
http.StatusText(want),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkHeader checks the value of a header entry.
|
||||||
|
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
||||||
|
if got := h.Get(key); got != want {
|
||||||
|
t.Errorf("%s: %q, want %q", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAPIHeader checks common entries set for API endpoints.
|
||||||
|
func checkAPIHeader(t *testing.T, h http.Header) {
|
||||||
|
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
||||||
|
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
checkHeader(t, h, "Pragma", "no-cache")
|
||||||
|
checkHeader(t, h, "Expires", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
||||||
|
func checkPayloadFunc[T any](
|
||||||
|
t *testing.T,
|
||||||
|
resp *http.Response,
|
||||||
|
f func(got *T) bool,
|
||||||
|
) {
|
||||||
|
var got T
|
||||||
|
r := io.Reader(resp.Body)
|
||||||
|
if testing.Verbose() {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
r = io.TeeReader(r, &buf)
|
||||||
|
defer func() { t.Log(buf.String()) }()
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
||||||
|
t.Fatalf("Decode: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f(&got) {
|
||||||
|
t.Errorf("Body: %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPayload checks the JSON response of an API endpoint.
|
||||||
|
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
||||||
|
checkPayloadFunc(t, resp, func(got *T) bool {
|
||||||
|
return reflect.DeepEqual(got, &want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
||||||
|
checkStatus(t, resp, code)
|
||||||
|
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
||||||
|
t.Errorf("Body: %q, want %q", string(got), error)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user