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:
245
modules/tide/roundtrip_test.go
Normal file
245
modules/tide/roundtrip_test.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package tide
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParityRoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
specPath := filepath.Join("testdata", "one-route-spec.yaml")
|
||||
spec, err := LoadFlow(specPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("json record replay and scalar mismatch", func(t *testing.T) {
|
||||
orig := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/sample" || r.Method != http.MethodGet {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"data":"ok"}`))
|
||||
}))
|
||||
t.Cleanup(orig.Close)
|
||||
|
||||
changed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"data":"no"}`))
|
||||
}))
|
||||
t.Cleanup(changed.Close)
|
||||
|
||||
recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: orig.URL})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := filepath.Join(t.TempDir(), "sample.yaml")
|
||||
if err := SaveFlow(out, recorded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
if !strings.Contains(text, "version: 1") {
|
||||
t.Fatalf("missing version:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "body: |") && !strings.Contains(text, "body: |-") {
|
||||
t.Fatalf("body is not a literal block scalar:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, `{"data":"ok"}`) {
|
||||
t.Fatalf("missing recorded JSON body:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "application/json") {
|
||||
t.Fatalf("missing Content-Type:\n%s", text)
|
||||
}
|
||||
|
||||
loaded, err := LoadFlow(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReplayFlow(ctx, loaded, ReplayConfig{Target: orig.URL}); err != nil {
|
||||
t.Fatalf("identical backend must replay: %v", err)
|
||||
}
|
||||
|
||||
_, err = ReplayFlow(ctx, loaded, ReplayConfig{Target: changed.URL})
|
||||
if err == nil {
|
||||
t.Fatal("changed JSON must fail")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "$.data") {
|
||||
t.Fatalf("mismatch missing $.data: %s", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "ok") || !strings.Contains(msg, "no") {
|
||||
t.Fatalf("mismatch missing expected/actual: %s", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-json byte offset mismatch", func(t *testing.T) {
|
||||
orig := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello"))
|
||||
}))
|
||||
t.Cleanup(orig.Close)
|
||||
|
||||
changed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hallo"))
|
||||
}))
|
||||
t.Cleanup(changed.Close)
|
||||
|
||||
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("identical plain body must replay: %v", err)
|
||||
}
|
||||
_, err = ReplayFlow(ctx, recorded, ReplayConfig{Target: changed.URL})
|
||||
if err == nil {
|
||||
t.Fatal("changed bytes must fail")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "1") {
|
||||
t.Fatalf("byte mismatch missing offset: %s", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "hello") || !strings.Contains(msg, "hallo") {
|
||||
t.Fatalf("byte mismatch missing printable bytes: %s", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json key order ignored missing keys and types", func(t *testing.T) {
|
||||
orig := jsonServer(t, `{"z":1,"a":"x"}`)
|
||||
reordered := jsonServer(t, `{"a":"x","z":1}`)
|
||||
missing := jsonServer(t, `{"a":"x"}`)
|
||||
wrongType := jsonServer(t, `{"z":"1","a":"x"}`)
|
||||
|
||||
recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: orig.URL})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReplayFlow(ctx, recorded, ReplayConfig{Target: reordered.URL}); err != nil {
|
||||
t.Fatalf("reordered keys must pass: %v", err)
|
||||
}
|
||||
_, err = ReplayFlow(ctx, recorded, ReplayConfig{Target: missing.URL})
|
||||
if err == nil || !strings.Contains(err.Error(), "$.z") {
|
||||
t.Fatalf("missing key must fail at $.z, got %v", err)
|
||||
}
|
||||
_, err = ReplayFlow(ctx, recorded, ReplayConfig{Target: wrongType.URL})
|
||||
if err == nil || !strings.Contains(err.Error(), "$.z") {
|
||||
t.Fatalf("number vs string must fail at $.z, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "1") {
|
||||
t.Fatalf("type mismatch missing values: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects unknown fields empty names duplicates and unsafe paths", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFlow := func(name, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
base := "version: 1\nname: sample\nsteps:\n - id: a\n request:\n method: GET\n path: /sample\n"
|
||||
if _, err := LoadFlow(writeFlow("unknown.yaml", base+"extra: true\n")); err == nil {
|
||||
t.Fatal("unknown field must fail")
|
||||
}
|
||||
if _, err := LoadFlow(writeFlow("empty-name.yaml", "version: 1\nname: \"\"\nsteps:\n - id: a\n request:\n method: GET\n path: /sample\n")); err == nil || !strings.Contains(err.Error(), "name") {
|
||||
t.Fatalf("empty name must fail, got %v", err)
|
||||
}
|
||||
dup := "version: 1\nname: sample\nsteps:\n - id: a\n request:\n method: GET\n path: /a\n - id: a\n request:\n method: GET\n path: /b\n"
|
||||
if _, err := LoadFlow(writeFlow("dup.yaml", dup)); err == nil || !strings.Contains(err.Error(), "duplicate") {
|
||||
t.Fatalf("duplicate step id must fail, got %v", err)
|
||||
}
|
||||
unsafe := base + " response:\n body_file: ../secret.bin\n"
|
||||
if _, err := LoadFlow(writeFlow("unsafe.yaml", unsafe)); err == nil || !strings.Contains(err.Error(), "body_file") {
|
||||
t.Fatalf("parent body_file must fail, got %v", err)
|
||||
}
|
||||
abs := base + " response:\n body_file: /tmp/secret.bin\n"
|
||||
if _, err := LoadFlow(writeFlow("abs.yaml", abs)); err == nil || !strings.Contains(err.Error(), "body_file") {
|
||||
t.Fatalf("absolute body_file must fail, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oversized body fails before fixture commit", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("hello"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
out := filepath.Join(t.TempDir(), "too-big.yaml")
|
||||
_, err := RecordFlow(ctx, spec, RecordConfig{Target: srv.URL, MaxBody: 4})
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("truncated body must fail, got %v", err)
|
||||
}
|
||||
if _, statErr := os.Stat(out); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("fixture was committed after truncation: %v", statErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("injected client is used and save leaves no temp files", func(t *testing.T) {
|
||||
srv := jsonServer(t, `{"ok":true}`)
|
||||
trip := &countTransport{rt: srv.Client().Transport}
|
||||
client := &http.Client{Transport: trip}
|
||||
recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: srv.URL, Client: client})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if trip.n == 0 {
|
||||
t.Fatal("injected client was not used")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
out := filepath.Join(dir, "saved.yaml")
|
||||
if err := SaveFlow(out, recorded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if strings.Contains(e.Name(), ".tmp") {
|
||||
t.Fatalf("temp file left behind: %s", e.Name())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func jsonServer(t *testing.T, body string) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
type countTransport struct {
|
||||
rt http.RoundTripper
|
||||
n int
|
||||
}
|
||||
|
||||
func (c *countTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
c.n++
|
||||
if c.rt == nil {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
return c.rt.RoundTrip(req)
|
||||
}
|
||||
Reference in New Issue
Block a user