mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Every seam states the address, not just the route
v1.37.9 fixed the REST path and the three by-name seams that already looked the op up in the composed registry. Two did not: the graph resolved its ops from that same registry and then invoked them without saying so, and the in-process invoke did the same. So the contract fell back to the declaration — for anything declared on a group, the bare leaf — and a rule reading op.Path governed the REST call while missing the graph call to the SAME operation. A test now asks both seams about one op and requires the same answer. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
package zip_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zap-proto/zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
type probeIn struct{}
|
||||||
|
type probeOut struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type crm struct{}
|
||||||
|
|
||||||
|
// Probe answers whether the surface is up.
|
||||||
|
func (crm) Probe(context.Context, *probeIn) (*probeOut, error) { return &probeOut{OK: true}, nil }
|
||||||
|
|
||||||
|
// EVERY SEAM STATES THE ADDRESS, not just the REST route.
|
||||||
|
//
|
||||||
|
// An op reached by NAME — through the graph, a tools/call, the call plane or an
|
||||||
|
// in-process invoke — is the same operation as the one reached by path, so the
|
||||||
|
// rule has to see the same identity either way. The graph resolved ops out of
|
||||||
|
// the composed registry and then invoked them without saying so, which left the
|
||||||
|
// contract falling back to the declaration: for anything declared on a group,
|
||||||
|
// the bare leaf. A rule reading op.Path governed the REST call and not the graph
|
||||||
|
// call to the same operation.
|
||||||
|
func TestGraph_ARuleSeesTheServedAddress(t *testing.T) {
|
||||||
|
app := zip.New(zip.Config{AppName: "crm", DisableStartupMessage: true})
|
||||||
|
app.Group("").Group("/v1/crm").Post("/probe", crm{}.Probe, zip.WithOperationID("crm_probe"))
|
||||||
|
app.MountGraph("/v1/graphql")
|
||||||
|
|
||||||
|
var seen []string
|
||||||
|
app.Authorize(func(_ context.Context, op zip.Op, _ any) (zip.Decision, error) {
|
||||||
|
seen = append(seen, op.Method+" "+op.Path+" "+op.OperationID)
|
||||||
|
return zip.Decision{Effect: zip.Allow}, nil
|
||||||
|
})
|
||||||
|
if err := app.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Through the graph, by name.
|
||||||
|
body, _ := json.Marshal(zip.GraphRequest{Query: `mutation { crm_probe { ok } }`})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/graphql", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
// And over REST, by path.
|
||||||
|
rr := httptest.NewRequest(http.MethodPost, "/v1/crm/probe", bytes.NewReader([]byte(`{}`)))
|
||||||
|
rr.Header.Set("Content-Type", "application/json")
|
||||||
|
resp2, err := app.Test(rr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = resp2.Body.Close()
|
||||||
|
|
||||||
|
if len(seen) != 2 {
|
||||||
|
t.Fatalf("authorizer ran %d times, want 2 (graph and REST): %v", len(seen), seen)
|
||||||
|
}
|
||||||
|
const want = "POST /v1/crm/probe crm_probe"
|
||||||
|
for i, got := range seen {
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("call %d saw %q, want %q — both seams answer for one operation", i, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ func Here[In, Out any](ctx context.Context, a *App, op string, in *In) (*Out, er
|
|||||||
if o == nil || o.direct == nil {
|
if o == nil || o.direct == nil {
|
||||||
return nil, ErrNotFound("unknown op: " + op)
|
return nil, ErrNotFound("unknown op: " + op)
|
||||||
}
|
}
|
||||||
out, err := o.direct(ctx, in)
|
out, err := o.direct(withOp(ctx, servedOp(o)), in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -249,7 +249,11 @@ func (e *graph) call(ctx context.Context, op *registeredOp, s *gqlSel, path []an
|
|||||||
// seam binds them: the header is the authority for the field it names.
|
// seam binds them: the header is the authority for the field it names.
|
||||||
bindHeaders(in, headerOf(ctx))
|
bindHeaders(in, headerOf(ctx))
|
||||||
}
|
}
|
||||||
out, err := op.direct(ctx, in)
|
// The op came out of the composed registry, so it knows the address it is
|
||||||
|
// served at. Saying so is what lets a path-keyed rule govern a graph field
|
||||||
|
// the same way it governs the REST route: without it the contract fell back
|
||||||
|
// to the declaration, which for anything declared on a group is the leaf.
|
||||||
|
out, err := op.direct(withOp(ctx, servedOp(op)), in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.fail(path, err.Error())
|
e.fail(path, err.Error())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user