feat(14-03): tide keeps binary upstream bodies and compares every *_url upload by shape
- A response body that is not valid UTF-8 (a cover image) is written as a YAML !!binary scalar, never masked and replayed byte for byte - The upload URL normalizer covers every key ending in _url (cover_url), not only url and thumb_url - README and parity-testing docs updated
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
@@ -71,13 +72,49 @@ const MaxUpstreamBody = 32 << 20
|
||||
// stored as a {{sha256:<hex>}} placeholder.
|
||||
const upstreamHashMin = 1024
|
||||
|
||||
// UpstreamResponse is the recorded vendor answer the fake replays.
|
||||
// UpstreamResponse is the recorded vendor answer the fake replays. A body
|
||||
// that is not valid UTF-8 (an image, for example) is written as a YAML
|
||||
// !!binary scalar (base64) and read back byte for byte.
|
||||
type UpstreamResponse struct {
|
||||
Status int `yaml:"status"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
Body string `yaml:"body,omitempty"`
|
||||
}
|
||||
|
||||
// upstreamResponseYAML is UpstreamResponse as written: the body is a plain
|
||||
// string, a binaryScalar, or nil when empty.
|
||||
type upstreamResponseYAML struct {
|
||||
Status int `yaml:"status"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
Body any `yaml:"body,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalYAML writes a non-UTF-8 body as !!binary so the bytes survive the
|
||||
// round trip.
|
||||
func (r UpstreamResponse) MarshalYAML() (any, error) {
|
||||
return upstreamResponseYAML{Status: r.Status, Headers: r.Headers, Body: yamlBody(r.Body)}, nil
|
||||
}
|
||||
|
||||
// binaryScalar is a byte string written as a YAML !!binary scalar.
|
||||
type binaryScalar string
|
||||
|
||||
// MarshalYAML implements yaml.BytesMarshaler.
|
||||
func (b binaryScalar) MarshalYAML() ([]byte, error) {
|
||||
return []byte("!!binary " + base64.StdEncoding.EncodeToString([]byte(b))), nil
|
||||
}
|
||||
|
||||
// yamlBody is the value written for a body: nil when empty, a binaryScalar
|
||||
// when it is not valid UTF-8, else the string.
|
||||
func yamlBody(body string) any {
|
||||
switch {
|
||||
case body == "":
|
||||
return nil
|
||||
case !utf8.ValidString(body):
|
||||
return binaryScalar(body)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// UpstreamCompareHeaders are the request headers the fake asserts. A header
|
||||
// is compared when either the recorded or the sent request carries it.
|
||||
var UpstreamCompareHeaders = []string{
|
||||
@@ -305,9 +342,13 @@ func (f *UpstreamFake) response(r UpstreamResponse, req *http.Request) (*http.Re
|
||||
}
|
||||
h.Set(k, ev)
|
||||
}
|
||||
body, err := f.store.Expand(r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("response body: %w", err)
|
||||
body := r.Body
|
||||
if utf8.ValidString(body) {
|
||||
// A binary body is replayed byte for byte, never expanded.
|
||||
var err error
|
||||
if body, err = f.store.Expand(body); err != nil {
|
||||
return nil, fmt.Errorf("response body: %w", err)
|
||||
}
|
||||
}
|
||||
return &http.Response{
|
||||
Status: fmt.Sprintf("%d %s", r.Status, http.StatusText(r.Status)),
|
||||
@@ -598,7 +639,11 @@ func maskUpstream(s UpstreamSidecar, store *Store) (UpstreamSidecar, error) {
|
||||
}
|
||||
resp := ex.Response
|
||||
resp.Headers = scrubMap(resp.Headers, pairs, true)
|
||||
resp.Body = replaceAll(resp.Body, pairs, false)
|
||||
if utf8.ValidString(resp.Body) {
|
||||
// A binary body (an image) is kept byte for byte: masking a
|
||||
// short variable value inside it would corrupt the file.
|
||||
resp.Body = replaceAll(resp.Body, pairs, false)
|
||||
}
|
||||
out.Exchanges[i] = UpstreamExchange{Request: req, Response: resp}
|
||||
}
|
||||
return out, nil
|
||||
|
||||
Reference in New Issue
Block a user