refactor(10.2-01): nest framework packages under modules

- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
Jakub Zych
2026-09-28 02:21:02 +02:00
parent ac1f6d14f4
commit 5e50b166ef
277 changed files with 303 additions and 303 deletions

View File

@@ -0,0 +1,189 @@
package tide
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
)
func TestDiffContract(t *testing.T) {
ctx := context.Background()
baseline := `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`
reordered := `{"meta":{"total":1,"per_page":15,"last_page":1,"current_page":1},"data":[{"deleted_at":null,"created_at":"2026-01-01T00:00:00+00:00","is_owner":true,"tracklist":[],"price":"1.5000","slug":"keep-me","id":9}]}`
spec := Flow{Version: 1, Name: "diff-contract", Steps: []Step{{
ID: "get",
Request: Request{Method: http.MethodGet, Path: "/item"},
}}}
orig := jsonServer(t, baseline)
recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: orig.URL})
if err != nil {
t.Fatal(err)
}
if _, err := ReplayFlow(ctx, recorded, ReplayConfig{Target: orig.URL}); err != nil {
t.Fatalf("baseline must pass: %v", err)
}
if _, err := ReplayFlow(ctx, recorded, ReplayConfig{Target: jsonServer(t, reordered).URL}); err != nil {
t.Fatalf("object key reorder must pass: %v", err)
}
type mutation struct {
name string
live string
path string
}
cases := []mutation{
{"null vs array", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":null,"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "tracklist"},
{"empty object vs array", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":{},"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "tracklist"},
{"carbon Z vs +00:00", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00Z","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "created_at"},
{"non-date text", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"yesterday","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "created_at"},
{"date present-null vs absent", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00"}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "deleted_at"},
{"tri-state true vs false", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":false,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "is_owner"},
{"tri-state true vs null", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":null,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "is_owner"},
{"missing meta envelope", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}]}`, "meta"},
{"extra links envelope key", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1},"links":{}}`, "links"},
{"conditional reservation key", `{"data":[{"id":1,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null,"reservation":{"id":2}}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "reservation"},
{"money string vs number", `{"data":[{"id":1,"slug":"keep-me","price":1.5,"tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "price"},
{"integer id vs string", `{"data":[{"id":"1","slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "id"},
{"integer id vs fraction", `{"data":[{"id":1.5,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "id"},
{"exact slug mismatch", `{"data":[{"id":1,"slug":"other","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-01-01T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`, "slug"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mut := cloneRecorded(t, recorded)
mut.Steps[0].Response.Body = Body(tc.live)
_, err := ReplayFlow(ctx, mut, ReplayConfig{Target: orig.URL})
if err == nil {
t.Fatal("mutated fixture must fail")
}
assertPathMismatch(t, err, tc.path)
})
}
t.Run("csv exact-byte mismatch", func(t *testing.T) {
want := csvServer(t, "a,b\n1,2\n", `attachment; filename="albums.csv"`)
got := csvServer(t, "a,b\n1,3\n", `attachment; filename="albums.csv"`)
spec := Flow{Version: 1, Name: "csv", Steps: []Step{{
ID: "export",
Request: Request{Method: http.MethodGet, Path: "/export"},
}}}
rec, err := RecordFlow(ctx, spec, RecordConfig{Target: want.URL})
if err != nil {
t.Fatal(err)
}
if _, err := ReplayFlow(ctx, rec, ReplayConfig{Target: want.URL}); err != nil {
t.Fatalf("csv baseline must pass: %v", err)
}
_, err = ReplayFlow(ctx, rec, ReplayConfig{Target: got.URL})
if err == nil {
t.Fatal("csv byte mismatch must fail")
}
msg := err.Error()
if !strings.Contains(msg, "byte") && !strings.Contains(msg, "body[") {
t.Fatalf("csv mismatch missing byte diagnostic: %s", msg)
}
})
t.Run("image exact-byte mismatch", func(t *testing.T) {
want := binaryServer(t, "image/png", []byte{0x89, 'P', 'N', 'G', 1})
got := binaryServer(t, "image/png", []byte{0x89, 'P', 'N', 'G', 2})
spec := Flow{Version: 1, Name: "png", Steps: []Step{{
ID: "cover",
Request: Request{Method: http.MethodGet, Path: "/cover"},
}}}
rec, err := RecordFlow(ctx, spec, RecordConfig{Target: want.URL})
if err != nil {
t.Fatal(err)
}
if _, err := ReplayFlow(ctx, rec, ReplayConfig{Target: want.URL}); err != nil {
t.Fatalf("image baseline must pass: %v", err)
}
_, err = ReplayFlow(ctx, rec, ReplayConfig{Target: got.URL})
if err == nil {
t.Fatal("image byte mismatch must fail")
}
if !strings.Contains(err.Error(), "byte") && !strings.Contains(err.Error(), "body[") {
t.Fatalf("image mismatch missing byte diagnostic: %v", err)
}
})
t.Run("per-step normalization disable", func(t *testing.T) {
later := jsonServer(t, `{"data":[{"id":9,"slug":"keep-me","price":"1.5000","tracklist":[],"is_owner":true,"created_at":"2026-02-02T00:00:00+00:00","deleted_at":null}],"meta":{"current_page":1,"last_page":1,"per_page":15,"total":1}}`)
if _, err := ReplayFlow(ctx, recorded, ReplayConfig{Target: later.URL}); err != nil {
t.Fatalf("masked dates/ids must pass: %v", err)
}
disabled := cloneRecorded(t, recorded)
disabled.Steps[0].Normalize = []NormalizeRule{{Path: "created_at", Disable: true}}
_, err := ReplayFlow(ctx, disabled, ReplayConfig{Target: later.URL})
if err == nil {
t.Fatal("disabled date mask must fail")
}
assertPathMismatch(t, err, "created_at")
})
}
func cloneRecorded(t *testing.T, flow Flow) Flow {
t.Helper()
path := filepath.Join(t.TempDir(), "clone.yaml")
if err := SaveFlow(path, flow); err != nil {
t.Fatal(err)
}
cloned, err := LoadFlow(path)
if err != nil {
t.Fatal(err)
}
return cloned
}
func assertPathMismatch(t *testing.T, err error, path string) {
t.Helper()
msg := err.Error()
if !strings.Contains(msg, path) {
t.Fatalf("want path %s in %s", path, msg)
}
if !strings.Contains(msg, "expected") || !strings.Contains(msg, "actual") {
t.Fatalf("want expected/actual diagnostic in %s", msg)
}
var mis *MismatchError
if !errors.As(err, &mis) {
return
}
found := false
for _, step := range mis.Result.Steps {
for _, d := range step.Diffs {
if strings.Contains(d.Path, path) && d.Expected != d.Actual {
found = true
}
}
}
if !found {
t.Fatalf("result diffs missing path %s: %+v", path, mis.Result.Steps)
}
}
func csvServer(t *testing.T, body, disposition string) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", disposition)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
return srv
}
func binaryServer(t *testing.T, ct string, body []byte) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", ct)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}))
t.Cleanup(srv.Close)
return srv
}