Files
cloud/apps/sync/sync_api_test.go
zeekayandhanzo-dev f6c9605bd7
Hanzo CI/CD / cicd (push) Successful in 18s
CI/CD / gate (push) Successful in 19s
CI/CD / containment (push) Successful in 1m38s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
zip v1.24.1: tests stop reaching through fiber, so they see what serving installs
Upstream landed the v1.23 verb migration (Graft/Add/Mount folded into Use). This
is the half that was missing, and it is the half that made tests lie.

App.Test used to skip prepare, which installs the deferred projections — /mcp, the
OpenAPI document, the op-call plane, the plugin route. So those four addresses
answered 404 under test and 200 in production, and the papering-over was an
exported Prepare each caller had to remember. zip v1.24.1 makes Test prepare;
apps/ai's MCP door test passes because of that, not because of anything here.

414 call sites move from app.Fiber().Test(...) to app.Test(...) with
zip.TestConfig. That is the point of the escape hatch living on the concrete type:
reaching through it bypasses what App.Test does, so the tests most wanting to
exercise the real program were the ones that did not. Sites whose receiver is a
raw fiber app keep fiber's type — the two are not interchangeable and pretending
otherwise is how the first sweep broke things.

Also: the multi-line `Use(func(c *zip.Ctx) error {…})` literals in tests, which
the verb migration missed because they fail vet rather than build; and the last
`.Prepare()` calls, now that it is implicit.

iam v1.34.11 → v1.34.12.

Measured against upstream on the same host: 103 failing packages before, 97 after
— ZERO new, 6 fixed. The remainder is the macOS SQLCipher limit (no tmpfs for the
pure-Go codec), unrelated and unchanged.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 01:33:56 -07:00

178 lines
6.7 KiB
Go

package sync
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/zap-proto/zip"
)
var testCfg = zip.TestConfig{Timeout: 10 * time.Second, FailOnTimeout: true}
// do runs a control-plane JSON request through the Fiber test harness, carrying the
// gateway identity headers a validated principal needs.
func do(t *testing.T, app *zip.App, method, path, org string, body any) (int, []byte) {
t.Helper()
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req := httptest.NewRequest(method, path, r)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if org != "" {
req.Header.Set("X-Org-Id", org)
req.Header.Set("X-User-Id", "u_"+org)
}
resp, err := app.Test(req, testCfg)
if err != nil {
t.Fatalf("Test %s %s: %v", method, path, err)
}
defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body)
return resp.StatusCode, b
}
const widgetsURL = "https://github.com/acme-gh/widgets.git"
// TestSyncCRUD proves the /v1/sync control plane: create (upsert + derived git
// target), list, get, patch, run, delete, and org-scoping.
func TestSyncCRUD(t *testing.T) {
app := mountSync(t)
// No validated principal → 401 (an authentication failure, not a permission one).
if code, _ := do(t, app, http.MethodGet, "/v1/sync", "", nil); code != http.StatusUnauthorized {
t.Fatalf("no-principal list want 401, got %d", code)
}
// Create a github→native link (run:false so no background git work in the test).
code, body := do(t, app, http.MethodPost, "/v1/sync", "acme", map[string]any{
"source": map[string]any{"provider": "github", "locator": widgetsURL},
"direction": "both",
})
if code != http.StatusOK {
t.Fatalf("create want 200, got %d (%s)", code, body)
}
var v syncView
if err := json.Unmarshal(body, &v); err != nil {
t.Fatalf("create json: %v (%s)", err, body)
}
if v.ID == "" || v.Kind != "git" || v.Direction != "both" {
t.Fatalf("unexpected link view: %+v", v)
}
if v.Source.Provider != "github" || v.Source.Locator != widgetsURL {
t.Fatalf("source not preserved: %+v", v.Source)
}
if v.Target.Provider != provNative || v.Target.Locator != "widgets" {
t.Fatalf("target must derive to native widgets: %+v", v.Target)
}
// Re-create the same source→target is an UPSERT (same id, no duplicate).
code, body2 := do(t, app, http.MethodPost, "/v1/sync", "acme", map[string]any{
"source": map[string]any{"provider": "github", "locator": widgetsURL},
"direction": "pull",
})
if code != http.StatusOK {
t.Fatalf("re-create want 200, got %d (%s)", code, body2)
}
var v2 syncView
_ = json.Unmarshal(body2, &v2)
if v2.ID != v.ID || v2.Direction != "pull" {
t.Fatalf("upsert must keep id and update direction, got %+v", v2)
}
// List → exactly one link for acme.
code, body = do(t, app, http.MethodGet, "/v1/sync", "acme", nil)
if code != http.StatusOK {
t.Fatalf("list want 200, got %d", code)
}
var listed struct {
Data []syncView `json:"data"`
}
_ = json.Unmarshal(body, &listed)
if len(listed.Data) != 1 || listed.Data[0].ID != v.ID {
t.Fatalf("acme should list exactly [%s], got %+v", v.ID, listed.Data)
}
// A different org sees none (physical per-org isolation).
code, body = do(t, app, http.MethodGet, "/v1/sync", "beta", nil)
_ = json.Unmarshal(body, &listed)
if code != http.StatusOK || len(listed.Data) != 0 {
t.Fatalf("beta must see zero links, got %d %+v", code, listed.Data)
}
// Get by id.
if code, _ := do(t, app, http.MethodGet, "/v1/sync/"+v.ID, "acme", nil); code != http.StatusOK {
t.Fatalf("get want 200, got %d", code)
}
// Get missing → 404. Get from another org → 404 (isolation).
if code, _ := do(t, app, http.MethodGet, "/v1/sync/sync_missing", "acme", nil); code != http.StatusNotFound {
t.Fatalf("get missing want 404, got %d", code)
}
if code, _ := do(t, app, http.MethodGet, "/v1/sync/"+v.ID, "beta", nil); code != http.StatusNotFound {
t.Fatalf("cross-org get want 404, got %d", code)
}
// Patch the direction in place (id + endpoints preserved).
code, body = do(t, app, http.MethodPatch, "/v1/sync/"+v.ID, "acme", map[string]any{"direction": "push"})
if code != http.StatusOK {
t.Fatalf("patch want 200, got %d (%s)", code, body)
}
var patched syncView
_ = json.Unmarshal(body, &patched)
if patched.ID != v.ID || patched.Direction != "push" {
t.Fatalf("patch must keep id + set direction=push, got %+v", patched)
}
// Patch with a bad direction → 400.
if code, _ := do(t, app, http.MethodPatch, "/v1/sync/"+v.ID, "acme", map[string]any{"direction": "nope"}); code != http.StatusBadRequest {
t.Fatalf("patch bad direction want 400, got %d", code)
}
// Manual run → 202 accepted (the reconcile runs detached; git unmounted here, so
// it fails closed in the background — the endpoint still queues).
if code, _ := do(t, app, http.MethodPost, "/v1/sync/"+v.ID+"/run", "acme", nil); code != http.StatusAccepted {
t.Fatalf("run want 202, got %d", code)
}
// Delete → 204, then gone.
if code, _ := do(t, app, http.MethodDelete, "/v1/sync/"+v.ID, "acme", nil); code != http.StatusNoContent {
t.Fatalf("delete want 204, got %d", code)
}
if code, _ := do(t, app, http.MethodGet, "/v1/sync/"+v.ID, "acme", nil); code != http.StatusNotFound {
t.Fatalf("get after delete want 404, got %d", code)
}
}
// TestSyncValidation proves the create-time guards: provider, https, host match,
// direction, kind.
func TestSyncValidation(t *testing.T) {
app := mountSync(t)
cases := []struct {
name string
body map[string]any
want int
}{
{"ok", map[string]any{"source": map[string]any{"provider": "github", "locator": widgetsURL}}, http.StatusOK},
{"bad provider", map[string]any{"source": map[string]any{"provider": "svn", "locator": widgetsURL}}, http.StatusBadRequest},
{"not https", map[string]any{"source": map[string]any{"provider": "github", "locator": "http://github.com/a/b.git"}}, http.StatusBadRequest},
{"host mismatch", map[string]any{"source": map[string]any{"provider": "github", "locator": "https://gitlab.com/a/b.git"}}, http.StatusBadRequest},
{"bad direction", map[string]any{"source": map[string]any{"provider": "github", "locator": widgetsURL}, "direction": "sideways"}, http.StatusBadRequest},
{"bad kind", map[string]any{"kind": "db", "source": map[string]any{"provider": "github", "locator": widgetsURL}}, http.StatusBadRequest},
{"gitlab ok", map[string]any{"source": map[string]any{"provider": "gitlab", "locator": "https://gitlab.com/acme/widgets.git"}}, http.StatusOK},
}
for _, tc := range cases {
code, body := do(t, app, http.MethodPost, "/v1/sync", "acme", tc.body)
if code != tc.want {
t.Fatalf("%s: want %d, got %d (%s)", tc.name, tc.want, code, body)
}
}
}