test(14-06): tide reports every sidecar refusal and multipart mismatch

- LoadUpstream names the file for a missing, unknown-field, wrong-version,
  method-less, relative-URL or out-of-range-status sidecar
- WriteUpstream writes nothing for an invalid sidecar or an uncreatable
  directory
- compareParts reports count, name, filename, content type, sha256 and
  value mismatches and a non-multipart body
This commit is contained in:
Jakub Zych
2026-10-04 01:44:19 +02:00
parent 88f7683b99
commit fc90ae2382

View File

@@ -0,0 +1,110 @@
package tide
import (
"bytes"
"errors"
"io/fs"
"mime/multipart"
"net/textproto"
"os"
"path/filepath"
"strings"
"testing"
)
// TestUpstreamSidecarRefusals covers the refusals of the sidecar loader,
// writer and multipart comparison that the replay tests do not reach: a
// missing, malformed or invalid sidecar is an error naming the file, a
// write that cannot validate or create its directory writes nothing, every
// part mismatch is reported, and an unresolved placeholder is an error.
func TestUpstreamSidecarRefusals(t *testing.T) {
dir := t.TempDir()
if _, err := LoadUpstream(filepath.Join(dir, "missing.upstream.yaml")); !errors.Is(err, fs.ErrNotExist) {
t.Errorf("missing sidecar = %v", err)
}
write := func(name, body string) string {
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return path
}
for name, body := range map[string]string{
"unknown-field.yaml": "version: 1\nsurprise: true\n",
"version.yaml": "version: 9\n",
"method.yaml": "version: 1\nexchanges:\n - request: {method: \" \", url: \"https://x.test/\"}\n response: {status: 200}\n",
"url.yaml": "version: 1\nexchanges:\n - request: {method: GET, url: \"/relative\"}\n response: {status: 200}\n",
"status.yaml": "version: 1\nexchanges:\n - request: {method: GET, url: \"https://x.test/\"}\n response: {status: 42}\n",
} {
if _, err := LoadUpstream(write(name, body)); err == nil || !strings.Contains(err.Error(), name) {
t.Errorf("%s = %v, want an error naming the file", name, err)
}
}
store, err := OpenStore("")
if err != nil {
t.Fatal(err)
}
bad := UpstreamSidecar{Exchanges: []UpstreamExchange{{Request: UpstreamRequest{Method: "", URL: "https://x.test/"}, Response: UpstreamResponse{Status: 200}}}}
if err := WriteUpstream(filepath.Join(dir, "bad.upstream.yaml"), bad, store); err == nil {
t.Error("an invalid sidecar was written")
}
blocker := write("blocker", "a file, not a directory")
good := UpstreamSidecar{Exchanges: []UpstreamExchange{{Request: UpstreamRequest{Method: "GET", URL: "https://x.test/"}, Response: UpstreamResponse{Status: 200}}}}
if err := WriteUpstream(filepath.Join(blocker, "x.upstream.yaml"), good, store); err == nil {
t.Error("a sidecar was written under a file")
}
path := filepath.Join(dir, "nested", "good.upstream.yaml")
if err := WriteUpstream(path, good, store); err != nil {
t.Fatal(err)
}
if s, err := LoadUpstream(path); err != nil || s.Version != CurrentVersion {
t.Fatalf("written sidecar = %+v %v", s, err)
}
var body bytes.Buffer
mw := multipart.NewWriter(&body)
_ = mw.WriteField("title", "Bug")
h := textproto.MIMEHeader{}
h.Set("Content-Disposition", `form-data; name="file"; filename="s.png"`)
h.Set("Content-Type", "image/png")
fw, _ := mw.CreatePart(h)
_, _ = fw.Write([]byte("png"))
_ = mw.Close()
ctype := mw.FormDataContentType()
fake := NewUpstreamFake(UpstreamSidecar{}, store)
got, err := upstreamParts(ctype, body.Bytes())
if err != nil || len(got) != 2 {
t.Fatalf("parts %+v %v", got, err)
}
for _, c := range []struct {
name string
want []UpstreamPart
frag string
}{
{"count", got[:1], "want 1, got 2"},
{"name", []UpstreamPart{{Name: "other", Value: "Bug"}, got[1]}, "name: want"},
{"filename", []UpstreamPart{got[0], {Name: "file", Filename: "x.png", ContentType: "image/png", SHA256: got[1].SHA256}}, "filename"},
{"content type", []UpstreamPart{got[0], {Name: "file", Filename: "s.png", ContentType: "image/jpeg", SHA256: got[1].SHA256}}, "content type"},
{"sha256", []UpstreamPart{got[0], {Name: "file", Filename: "s.png", ContentType: "image/png", SHA256: strings.Repeat("0", 64)}}, "sha256"},
{"value", []UpstreamPart{{Name: "title", Value: "Feature"}, got[1]}, "value differs"},
{"placeholder", []UpstreamPart{{Name: "title", Value: "{{missing}}"}, got[1]}, "unresolved placeholder"},
} {
problems := fake.compareParts(c.want, ctype, body.Bytes())
if len(problems) == 0 || !strings.Contains(strings.Join(problems, "; "), c.frag) {
t.Errorf("%s mismatch reported %v", c.name, problems)
}
}
if problems := fake.compareParts(got, "application/json", body.Bytes()); len(problems) != 1 || !strings.Contains(problems[0], "not multipart") {
t.Errorf("a non-multipart body = %v", problems)
}
if problems := fake.compareParts(got, ctype, body.Bytes()); len(problems) != 0 {
t.Errorf("identical parts = %v", problems)
}
if _, err := store.expandKeeping("{{absent}} and {{sha256:kept}}", isHashPlaceholder); err == nil || !strings.Contains(err.Error(), "absent") {
t.Errorf("expandKeeping with a missing name = %v", err)
}
if out, err := store.expandKeeping("plain", isHashPlaceholder); err != nil || out != "plain" {
t.Errorf("expandKeeping(plain) = %q %v", out, err)
}
}