access-engine/internal/decision/emission_atomicity_test.go
tegwick f758381cea
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 6s
Build and Publish Container Image / build-and-push (push) Successful in 1m23s
FLEX-WP-0036: Go module is github.com/netkingdom/access-engine; T02 consumer inventory
- module path renamed (no external importers); golangci local-prefixes
- ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION replaces FLEX_AUTH_RUN_TOPAZ_INTEGRATION
- README title and identity paragraph reflect the landed renames
- docs/runtime-rename-inventory.md: per-consumer references by kind and order
- chart name, image package and /opt paths deliberately left for T05

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 735967@bnt-lap001
Assistant-Session: 5971967f-77f8-4ced-9e02-c6ef67040ffc
2026-10-10 15:29:00 +02:00

105 lines
3.3 KiB
Go

package decision_test
import (
"context"
"errors"
"testing"
"github.com/netkingdom/access-engine/internal/decision"
"github.com/netkingdom/access-engine/pkg/api"
)
// failingRecorder is an outbox whose commit always fails.
type failingRecorder struct {
appends int
batches int
uncommitted []api.DecisionEffect
}
func (r *failingRecorder) Append(api.DecisionEnvelope) error {
r.appends++
return errors.New("disk full")
}
func (r *failingRecorder) AppendBatch([]api.DecisionEnvelope) error {
r.batches++
return errors.New("disk full")
}
func (r *failingRecorder) NoteReleasedUncommitted(effect api.DecisionEffect) {
r.uncommitted = append(r.uncommitted, effect)
}
var (
allowedRead = api.CheckRequest{
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", System: "markitect-tool"},
}
deniedRead = api.CheckRequest{
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:missing", Type: "document", System: "markitect-tool"},
}
)
// FLEX-DEC-2026-018: withholding a deny turns it into an error an open-stance
// consumer reads as proceed, so a restriction is released and counted.
func TestUncommittedDenyIsReleasedAndCounted(t *testing.T) {
engine := newTestEngine(t)
recorder := &failingRecorder{}
engine.SetDecisionLog(recorder)
got, err := engine.Check(context.Background(), deniedRead)
if err != nil {
t.Fatalf("Check: %v; a restriction must never be withheld", err)
}
if got.Effect != api.DecisionEffectDeny {
t.Fatalf("effect = %q; want deny", got.Effect)
}
if len(recorder.uncommitted) != 1 || recorder.uncommitted[0] != api.DecisionEffectDeny {
t.Fatalf("released_uncommitted = %v; want [deny]", recorder.uncommitted)
}
}
// FLEX-DEC-2026-018: authority is never granted without its evidence.
func TestUncommittedAllowIsWithheld(t *testing.T) {
engine := newTestEngine(t)
recorder := &failingRecorder{}
engine.SetDecisionLog(recorder)
got, err := engine.Check(context.Background(), allowedRead)
if !errors.Is(err, decision.ErrRecordNotCommitted) {
t.Fatalf("err = %v, decision = %+v; want ErrRecordNotCommitted", err, got)
}
if len(recorder.uncommitted) != 0 {
t.Fatalf("a withheld allow was counted as released: %v", recorder.uncommitted)
}
}
func TestBatchCommitsOnceAndWithholdsOnAnyUncommittedAllow(t *testing.T) {
engine := newTestEngine(t)
recorder := &failingRecorder{}
engine.SetDecisionLog(recorder)
_, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{
Subject: allowedRead.Subject,
Action: "read",
Resources: []api.ResourceRef{allowedRead.Resource, deniedRead.Resource},
})
if !errors.Is(err, decision.ErrRecordNotCommitted) {
t.Fatalf("err = %v; want ErrRecordNotCommitted for a batch holding an allow", err)
}
if recorder.batches != 1 || recorder.appends != 0 {
t.Fatalf("batches=%d appends=%d; want one batch commit", recorder.batches, recorder.appends)
}
decisions, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{
Subject: deniedRead.Subject,
Action: "read",
Resources: []api.ResourceRef{deniedRead.Resource},
})
if err != nil || len(decisions) != 1 {
t.Fatalf("restriction-only batch: decisions=%v err=%v; want it released", decisions, err)
}
}