Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ type builderOutput struct {

// invalidBuilderInput contains a type outside the restricted binder contract.
type invalidBuilderInput struct {
// Count is intentionally unsupported as a required integer.
Count int64 `json:"count"`
// Count is intentionally unsupported as a 32-bit integer.
Count int32 `json:"count"`
}

// nilPolicy is a typed-nil authorization implementation used to test Build validation.
Expand Down
191 changes: 162 additions & 29 deletions dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package codemode
import (
"context"
"errors"
"math"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -31,6 +32,33 @@ type dispatchOutput struct {
Value string `json:"value"`
}

// widenedDispatchInput is the eight-form dispatcher test input.
type widenedDispatchInput struct {
// Org is the required string argument.
Org string `json:"org"`

// Count is the required signed integer argument.
Count int64 `json:"count"`

// Active is the required Boolean argument.
Active bool `json:"active"`

// Score is the required finite floating-point argument.
Score float64 `json:"score"`

// Label is the optional string argument.
Label *string `json:"label,omitempty"`

// Limit is the optional signed integer argument.
Limit *int64 `json:"limit,omitempty"`

// Enabled is the optional Boolean argument.
Enabled *bool `json:"enabled"`

// Weight is the optional finite floating-point argument.
Weight *float64 `json:"weight,omitempty"`
}

// dispatchOutcome carries one asynchronous dispatcher result.
type dispatchOutcome struct {
// value is the converted native result.
Expand All @@ -44,33 +72,70 @@ type dispatchOutcome struct {
func TestDispatchBindsAuthorizesThenInvokes(t *testing.T) {
authorizer := authzmocks.NewMockAuthorizer(t)
events := make([]string, 0, 2)
label := "beta"
limit := int64(25)
enabled := true
weight := 2.5
canonical := map[string]any{
"org": "meigma",
"count": int64(3),
"active": true,
"score": 1.5,
"label": "beta",
"limit": int64(25),
"enabled": true,
"weight": 2.5,
}
authorizer.EXPECT().Authorize(mock.Anything, mock.MatchedBy(func(input authz.AuthorizationInput) bool {
return input.Subject.ID == "subject-1" &&
input.CapabilityID == "cap.lookup" &&
input.CapabilityName == "records.lookup" &&
assert.ObjectsAreEqual(map[string]any{"value": "alpha"}, input.Arguments)
assert.ObjectsAreEqual(canonical, input.Arguments)
})).Run(func(context.Context, authz.AuthorizationInput) {
events = append(events, "authorize")
}).Return(nil).Once()
subject := newDispatchSubject(t, authorizer, func(_ context.Context, _ authz.Subject, input any) (any, error) {
events = append(events, "handler")
typed, ok := input.(dispatchInput)
if !ok {
return nil, catalog.ErrInputTypeMismatch
}
return dispatchOutput(typed), nil
})
subject := newWidenedDispatchSubject(
t,
authorizer,
func(_ context.Context, _ authz.Subject, input any) (any, error) {
events = append(events, "handler")
typed, ok := input.(widenedDispatchInput)
if !ok {
return nil, catalog.ErrInputTypeMismatch
}
assert.Equal(t, widenedDispatchInput{
Org: "meigma",
Count: 3,
Active: true,
Score: 1.5,
Label: &label,
Limit: &limit,
Enabled: &enabled,
Weight: &weight,
}, typed)
return dispatchOutput{Value: typed.Org}, nil
},
)

result, err := subject.dispatch.dispatch(
t.Context(),
authz.Subject{ID: "subject-1"},
"cap.lookup",
map[string]any{"value": "alpha"},
map[string]any{
"org": "meigma",
"count": int64(3),
"active": true,
"score": 1.5,
"label": "beta",
"limit": int64(25),
"enabled": true,
"weight": 2.5,
},
)

require.NoError(t, err)
assert.Equal(t, []string{"authorize", "handler"}, events)
assert.Equal(t, map[string]any{"value": "alpha"}, result)
assert.Equal(t, map[string]any{"value": "meigma"}, result)
}

// TestDispatchTranslatesEveryBindValueFailureInternally proves decoded maps never reach policy.
Expand All @@ -81,20 +146,52 @@ func TestDispatchTranslatesEveryBindValueFailureInternally(t *testing.T) {

// arguments is the decoded child map presented to dispatch.
arguments map[string]any

// widened selects the eight-form input contract.
widened bool
}{
{name: "missing required", arguments: map[string]any{}},
{name: "unknown", arguments: map[string]any{"value": "alpha", "other": "extra"}},
{name: "mistyped required string", arguments: map[string]any{"value": int64(1)}},
{name: "widened missing required integer", widened: true, arguments: map[string]any{
"org": "meigma",
"active": true,
"score": 1.5,
}},
{name: "widened float where int is required", widened: true, arguments: map[string]any{
"org": "meigma",
"count": float64(3),
"active": true,
"score": 1.5,
}},
{name: "widened NaN float", widened: true, arguments: map[string]any{
"org": "meigma",
"count": int64(3),
"active": true,
"score": math.NaN(),
}},
{name: "widened infinity float", widened: true, arguments: map[string]any{
"org": "meigma",
"count": int64(3),
"active": true,
"score": math.Inf(1),
}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authorizer := authzmocks.NewMockAuthorizer(t)
var handlerCalls atomic.Int64
subject := newDispatchSubject(t, authorizer, func(context.Context, authz.Subject, any) (any, error) {
invoke := func(context.Context, authz.Subject, any) (any, error) {
handlerCalls.Add(1)
return dispatchOutput{}, nil
})
}
var subject *dispatchSubject
if tt.widened {
subject = newWidenedDispatchSubject(t, authorizer, invoke)
} else {
subject = newDispatchSubject(t, authorizer, invoke)
}

_, err := subject.dispatch.dispatch(
t.Context(),
Expand Down Expand Up @@ -250,24 +347,34 @@ func TestDispatchClassifiesPolicyAndHandlerFailures(t *testing.T) {
// TestDispatchReturnsFreshCanonicalMaps proves decoded child maps never become authorization input.
func TestDispatchReturnsFreshCanonicalMaps(t *testing.T) {
authorizer := authzmocks.NewMockAuthorizer(t)
decoded := map[string]any{"value": "alpha"}
decoded := map[string]any{
"org": "meigma",
"count": int64(3),
"active": true,
"score": 1.5,
"limit": int64(25),
}
var authorized map[string]any
authorizer.EXPECT().Authorize(mock.Anything, mock.Anything).Run(
func(_ context.Context, input authz.AuthorizationInput) {
authorized = input.Arguments
input.Arguments["value"] = "mutated-policy"
decoded["value"] = "mutated-decoded"
input.Arguments["org"] = "mutated-policy"
decoded["org"] = "mutated-decoded"
},
).Return(nil).Once()
var received dispatchInput
subject := newDispatchSubject(t, authorizer, func(_ context.Context, _ authz.Subject, input any) (any, error) {
typed, ok := input.(dispatchInput)
if !ok {
return nil, catalog.ErrInputTypeMismatch
}
received = typed
return dispatchOutput(typed), nil
})
var received widenedDispatchInput
subject := newWidenedDispatchSubject(
t,
authorizer,
func(_ context.Context, _ authz.Subject, input any) (any, error) {
typed, ok := input.(widenedDispatchInput)
if !ok {
return nil, catalog.ErrInputTypeMismatch
}
received = typed
return dispatchOutput{Value: typed.Org}, nil
},
)

result, err := subject.dispatch.dispatch(
t.Context(),
Expand All @@ -278,10 +385,14 @@ func TestDispatchReturnsFreshCanonicalMaps(t *testing.T) {

require.NoError(t, err)
require.NotNil(t, authorized)
assert.Equal(t, "mutated-policy", authorized["value"])
assert.Equal(t, "mutated-decoded", decoded["value"])
assert.Equal(t, "alpha", received.Value)
assert.Equal(t, map[string]any{"value": "alpha"}, result)
assert.Equal(t, "mutated-policy", authorized["org"])
assert.Equal(t, "mutated-decoded", decoded["org"])
assert.Equal(t, "meigma", received.Org)
assert.Equal(t, int64(3), received.Count)
require.NotNil(t, received.Limit)
assert.Equal(t, int64(25), *received.Limit)
assert.Nil(t, received.Label)
assert.Equal(t, map[string]any{"value": "meigma"}, result)
}

// TestDispatchCancellationAfterAllowPreventsInvoke proves a stale allow cannot reach the handler.
Expand Down Expand Up @@ -473,3 +584,25 @@ func newDispatchSubject(t *testing.T, authorizer authz.Authorizer, invoke catalo
dispatch: newDispatcher(capabilityCatalog, authorizer, 16, 64*1024),
}
}

// newWidenedDispatchSubject builds a dispatcher around the eight-form input contract.
func newWidenedDispatchSubject(t *testing.T, authorizer authz.Authorizer, invoke catalog.Invoker) *dispatchSubject {
t.Helper()
plan, err := binding.CompileFor[widenedDispatchInput, dispatchOutput]()
require.NoError(t, err)
capabilityCatalog, err := catalog.Build([]catalog.Registration{{
ID: "cap.lookup",
Name: "records.lookup",
Summary: "Return one record.",
Description: "Returns the supplied record value.",
Plan: plan,
Invoke: invoke,
}}, catalog.Options{
MaxSearchQueryBytes: 256,
MaxSearchResults: 20,
})
require.NoError(t, err)
return &dispatchSubject{
dispatch: newDispatcher(capabilityCatalog, authorizer, 16, 64*1024),
}
}
Loading