Files
zip/keep_test.go
zeekay 8f3e825d41 Hold the new request kinds to the keep rule
A typed op's form field, cookie, multipart part and raw body are each
read off the connection's reused RequestCtx, so each is held to the
rule the other accessors already meet: a value kept past the handler
still reads as it did after the next request on the same connection,
over HTTP and over ZAP. The cases fail when readForm hands out a view
of PostArgs instead of a copy.
2026-10-09 09:45:20 -07:00

395 lines
12 KiB
Go

package zip_test
import (
"context"
"net"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/valyala/fasthttp"
zaphttp "github.com/zap-proto/http"
"github.com/zap-proto/zip"
)
// A string read off a request is the handler's to keep.
//
// A server answers every request on a connection from one fasthttp.RequestCtx
// and parses each request into the buffers the one before it used; fiber's own
// context is pooled the same way. A value that is a VIEW of those buffers is
// rewritten in place when the next request on the connection carries a value
// of the same length, so a handler that keeps one past its return — a map key,
// a cache entry, a goroutine, a usage record written after the answer — later
// reads the next caller's. In hanzo-inc/cloud that handed one API-key caller
// another tenant's principal.
//
// Each case keeps everything a handler can read off a request, sends a second
// request on the SAME connection with different values of the same length,
// and requires the first request's values to read as they did. Over both
// transports, because both serve a connection from one reused RequestCtx.
type keepIn struct {
ID string `json:"id"`
Q string `json:"q"`
Auth string `json:"auth" header:"Authorization"`
Name string `json:"name"`
}
type keepOut struct{}
// keepForm is a typed op's form, cookie and header, the request kinds that do
// not come from a JSON body.
type keepForm struct {
F string `json:"f" form:"f"`
Sid string `json:"sid" cookie:"sid"`
Auth string `json:"auth" header:"Authorization"`
Upload *zip.File `json:"upload" form:"upload"`
}
// keepBytes is a typed op's raw body.
type keepBytes struct {
Body zip.Body `json:"body"`
}
// keeper holds what each request's handler kept, in arrival order.
type keeper struct {
mu sync.Mutex
seen []map[string]string
body [][]byte
}
func (k *keeper) keep(m map[string]string, body []byte) {
k.mu.Lock()
defer k.mu.Unlock()
k.seen = append(k.seen, m)
k.body = append(k.body, body)
}
func (k *keeper) at(i int) (map[string]string, []byte) {
k.mu.Lock()
defer k.mu.Unlock()
return k.seen[i], k.body[i]
}
// val is field's value in request i: the same length for every i.
func val(field string, i int) string {
return field + "-" + strings.Repeat(string(rune('a'+i)), 12)
}
func keepApp(k *keeper) *zip.App {
app := zip.New(zip.Config{DisableStartupMessage: true})
app.Raw("POST", "/v1/raw/:id", func(c *zip.Ctx) error {
var q struct {
Q string `query:"q"`
}
if err := c.BindQuery(&q); err != nil {
return err
}
var f struct {
F string `form:"f"`
}
if err := c.Bind(&f); err != nil {
return err
}
var m map[string]string
if err := c.BindQuery(&m); err != nil {
return err
}
var u struct {
ID string `uri:"id"`
}
if err := c.BindURI(&u); err != nil {
return err
}
fc := c.Fiber()
k.keep(map[string]string{
"header": c.Header("Authorization"),
"org": c.Org(),
"user": c.User(),
"query": c.Query("q"),
"param": c.Param("id"),
"path": c.Path(),
"cookie": fc.Cookies("sid"),
"queries": fc.Queries()["q"],
"reqheaders": fc.GetReqHeaders()["Authorization"][0],
"formvalue": fc.FormValue("f"),
"bindquery": q.Q,
"bindmap": m["q"],
"bindform": f.F,
"binduri": u.ID,
"caller": zip.CallerOf(c.Forward()).Org,
}, c.Body())
return c.NoContent(204)
})
app.Raw("POST", "/v1/json", func(c *zip.Ctx) error {
var in struct {
Name string `json:"name"`
Tags []string `json:"tags"`
Meta map[string]string `json:"meta"`
Any any `json:"any"`
}
if err := c.Bind(&in); err != nil {
return err
}
k.keep(map[string]string{
"name": in.Name,
"tag": in.Tags[0],
"meta": in.Meta["k"],
"any": in.Any.(map[string]any)["k"].(string),
}, nil)
return c.NoContent(204)
})
app.Post("/v1/typed/:id", func(ctx context.Context, in *keepIn) (*keepOut, error) {
k.keep(map[string]string{
"param": in.ID,
"query": in.Q,
"header": in.Auth,
"body": in.Name,
"caller": zip.CallerOf(ctx).Org,
"read": zip.Header(ctx, "Authorization"),
}, nil)
return nil, nil
})
app.Post("/v1/form", func(_ context.Context, in *keepForm) (*keepOut, error) {
k.keep(map[string]string{
"form": in.F,
"cookie": in.Sid,
"header": in.Auth,
}, nil)
return nil, nil
}, zip.Consumes("application/x-www-form-urlencoded"))
app.Post("/v1/upload", func(_ context.Context, in *keepForm) (*keepOut, error) {
k.keep(map[string]string{
"form": in.F,
"name": in.Upload.Name,
"type": in.Upload.Type,
}, in.Upload.Bytes)
return nil, nil
}, zip.Consumes("multipart/form-data"))
app.Post("/v1/bytes", func(_ context.Context, in *keepBytes) (*keepOut, error) {
k.keep(map[string]string{"type": in.Body.Type}, in.Body.Bytes)
return nil, nil
})
return app
}
// formAsk is request i to the typed form op.
func formAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/form")
req.Header.Set("Authorization", "Bearer "+val("header", i))
req.Header.SetCookie("sid", val("cookie", i))
req.Header.SetContentType("application/x-www-form-urlencoded")
req.SetBodyString("f=" + val("form", i))
return req
}
func formWant(i int) map[string]string {
return map[string]string{
"form": val("form", i),
"cookie": val("cookie", i),
"header": "Bearer " + val("header", i),
}
}
// uploadAsk is request i to the typed multipart op: one field and one part.
func uploadAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/upload")
req.Header.SetContentType("multipart/form-data; boundary=keepboundary")
req.SetBodyString("--keepboundary\r\n" +
"Content-Disposition: form-data; name=\"f\"\r\n\r\n" + val("form", i) + "\r\n" +
"--keepboundary\r\n" +
"Content-Disposition: form-data; name=\"upload\"; filename=\"" + val("name", i) + "\"\r\n" +
"Content-Type: text/" + val("type", i) + "\r\n\r\n" + val("part", i) + "\r\n" +
"--keepboundary--\r\n")
return req
}
func uploadWant(i int) map[string]string {
return map[string]string{
"form": val("form", i),
"name": val("name", i),
"type": "text/" + val("type", i),
}
}
// bytesAsk is request i to the typed raw-body op.
func bytesAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/bytes")
req.Header.SetContentType("application/" + val("media", i))
req.SetBodyString(val("bytes", i))
return req
}
func bytesWant(i int) map[string]string {
return map[string]string{"type": "application/" + val("media", i)}
}
// rawAsk is request i to the untyped route: every value it carries is val(…, i).
func rawAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/raw/" + val("param", i) + "?q=" + val("query", i))
req.Header.Set("Authorization", "Bearer "+val("header", i))
req.Header.Set(zip.HeaderOrg, val("org", i))
req.Header.Set(zip.HeaderUser, val("user", i))
req.Header.SetCookie("sid", val("cookie", i))
req.Header.SetContentType("application/x-www-form-urlencoded")
req.SetBodyString("f=" + val("form", i))
return req
}
// rawWant is what request i's handler must still read after later requests.
func rawWant(i int) map[string]string {
return map[string]string{
"header": "Bearer " + val("header", i),
"org": val("org", i),
"user": val("user", i),
"query": val("query", i),
"param": val("param", i),
"path": "/v1/raw/" + val("param", i),
"cookie": val("cookie", i),
"queries": val("query", i),
"reqheaders": "Bearer " + val("header", i),
"formvalue": val("form", i),
"bindquery": val("query", i),
"bindmap": val("query", i),
"bindform": val("form", i),
"binduri": val("param", i),
"caller": val("org", i),
}
}
// jsonAsk is request i to the untyped route that binds a JSON body.
func jsonAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/json")
req.Header.SetContentType("application/json")
req.SetBodyString(`{"name":"` + val("name", i) + `","tags":["` + val("tag", i) + `"],"meta":{"k":"` +
val("meta", i) + `"},"any":{"k":"` + val("any", i) + `"}}`)
return req
}
func jsonWant(i int) map[string]string {
return map[string]string{
"name": val("name", i),
"tag": val("tag", i),
"meta": val("meta", i),
"any": val("any", i),
}
}
func typedAsk(i int) *fasthttp.Request {
req := fasthttp.AcquireRequest()
req.Header.SetMethod("POST")
req.SetRequestURI("http://keep/v1/typed/" + val("param", i) + "?q=" + val("query", i))
req.Header.Set("Authorization", "Bearer "+val("header", i))
req.Header.Set(zip.HeaderOrg, val("org", i))
req.Header.SetContentType("application/json")
req.SetBodyString(`{"name":"` + val("body", i) + `"}`)
return req
}
func typedWant(i int) map[string]string {
return map[string]string{
"param": val("param", i),
"query": val("query", i),
"header": "Bearer " + val("header", i),
"body": val("body", i),
"caller": val("org", i),
"read": "Bearer " + val("header", i),
}
}
// keepServe serves app over HTTP/1.1 and ZAP and returns a client for each
// that holds ONE connection, so every request it sends reuses the server's
// RequestCtx for that connection.
func keepServe(t *testing.T, app *zip.App) map[string]zip.Client {
t.Helper()
addr := free(t)
sock := filepath.Join(sockDir(t), "keep.sock")
h, err := zip.Serve(app, "http://"+addr, sock)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = h.Close() })
waitTCP(t, addr)
waitSock(t, sock)
return map[string]zip.Client{
"http": &fasthttp.HostClient{Addr: addr, MaxConns: 1},
"zap": zaphttp.Dial("unix", sock),
}
}
func waitTCP(t *testing.T, addr string) {
t.Helper()
for range 100 {
if c, err := net.DialTimeout("tcp", addr, 50*time.Millisecond); err == nil {
_ = c.Close()
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("%s never began listening", addr)
}
func TestKeep_AStringKeptPastTheHandlerIsNotTheNextRequests(t *testing.T) {
k := &keeper{}
clients := keepServe(t, keepApp(k))
cases := []struct {
route string
ask func(int) *fasthttp.Request
want func(int) map[string]string
body func(int) string
}{
{"raw", rawAsk, rawWant, func(i int) string { return "f=" + val("form", i) }},
{"json", jsonAsk, jsonWant, nil},
{"typed", typedAsk, typedWant, nil},
{"form", formAsk, formWant, nil},
{"upload", uploadAsk, uploadWant, func(i int) string { return val("part", i) }},
{"bytes", bytesAsk, bytesWant, func(i int) string { return val("bytes", i) }},
}
for _, tr := range []string{"http", "zap"} {
for _, tc := range cases {
t.Run(tr+"/"+tc.route, func(t *testing.T) {
client := clients[tr]
k.mu.Lock()
first := len(k.seen)
k.mu.Unlock()
// Request 0 is kept; requests 1-3 reuse its buffers.
for i := range 4 {
req, resp := tc.ask(i), fasthttp.AcquireResponse()
if err := client.Do(req, resp); err != nil {
t.Fatalf("request %d: %v", i, err)
}
if resp.StatusCode() != 204 {
t.Fatalf("request %d: status %d: %s", i, resp.StatusCode(), resp.Body())
}
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
}
for i := range 4 {
got, body := k.at(first + i)
for name, want := range tc.want(i) {
if got[name] != want {
t.Errorf("request %d's %s now reads %q, want %q", i, name, got[name], want)
}
}
if tc.body != nil && string(body) != tc.body(i) {
t.Errorf("request %d's body now reads %q, want %q", i, body, tc.body(i))
}
}
})
}
}
}