go/entitlement_test.go

236 lines
6.8 KiB
Go
Raw Permalink Normal View History

package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Entitled ---
func TestEntitlement_Entitled_Good_DefaultPermissive(t *testing.T) {
c := New()
e := c.Entitled("anything")
assert.True(t, e.Allowed, "default checker permits everything")
assert.True(t, e.Unlimited)
}
func TestEntitlement_Entitled_Good_BooleanGate(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "premium.feature" {
return Entitlement{Allowed: true}
}
return Entitlement{Allowed: false, Reason: "not in package"}
})
assert.True(t, c.Entitled("premium.feature").Allowed)
assert.False(t, c.Entitled("other.feature").Allowed)
assert.Equal(t, "not in package", c.Entitled("other.feature").Reason)
}
func TestEntitlement_Entitled_Good_QuantityCheck(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "social.accounts" {
limit := 5
used := 3
remaining := limit - used
if qty > remaining {
return Entitlement{Allowed: false, Limit: limit, Used: used, Remaining: remaining, Reason: "limit exceeded"}
}
return Entitlement{Allowed: true, Limit: limit, Used: used, Remaining: remaining}
}
return Entitlement{Allowed: true, Unlimited: true}
})
// Can create 2 more (3 used of 5)
e := c.Entitled("social.accounts", 2)
assert.True(t, e.Allowed)
assert.Equal(t, 5, e.Limit)
assert.Equal(t, 3, e.Used)
assert.Equal(t, 2, e.Remaining)
// Can't create 3 more
e = c.Entitled("social.accounts", 3)
assert.False(t, e.Allowed)
assert.Equal(t, "limit exceeded", e.Reason)
}
func TestEntitlement_Entitled_Bad_Denied(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
return Entitlement{Allowed: false, Reason: "locked by M1"}
})
e := c.Entitled("product.create")
assert.False(t, e.Allowed)
assert.Equal(t, "locked by M1", e.Reason)
}
func TestEntitlement_Entitled_Ugly_DefaultQuantityIsOne(t *testing.T) {
c := New()
var receivedQty int
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
receivedQty = qty
return Entitlement{Allowed: true}
})
c.Entitled("test")
assert.Equal(t, 1, receivedQty, "default quantity should be 1")
}
// --- Action.Run Entitlement Enforcement ---
func TestEntitlement_ActionRun_Good_Permitted(t *testing.T) {
c := New()
c.Action("work", func(_ context.Context, _ Options) Result {
return Result{Value: "done", OK: true}
})
r := c.Action("work").Run(context.Background(), NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "done", r.Value)
}
func TestEntitlement_ActionRun_Bad_Denied(t *testing.T) {
c := New()
c.Action("restricted", func(_ context.Context, _ Options) Result {
return Result{Value: "should not reach", OK: true}
})
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "restricted" {
return Entitlement{Allowed: false, Reason: "tier too low"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
r := c.Action("restricted").Run(context.Background(), NewOptions())
assert.False(t, r.OK, "denied action must not execute")
err, ok := r.Value.(error)
assert.True(t, ok)
assert.Contains(t, err.Error(), "not entitled")
assert.Contains(t, err.Error(), "tier too low")
}
func TestEntitlement_ActionRun_Good_OtherActionsStillWork(t *testing.T) {
c := New()
c.Action("allowed", func(_ context.Context, _ Options) Result {
return Result{Value: "ok", OK: true}
})
c.Action("blocked", func(_ context.Context, _ Options) Result {
return Result{Value: "nope", OK: true}
})
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "blocked" {
return Entitlement{Allowed: false, Reason: "nope"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
assert.True(t, c.Action("allowed").Run(context.Background(), NewOptions()).OK)
assert.False(t, c.Action("blocked").Run(context.Background(), NewOptions()).OK)
}
// --- NearLimit ---
func TestEntitlement_NearLimit_Good(t *testing.T) {
e := Entitlement{Allowed: true, Limit: 100, Used: 85, Remaining: 15}
assert.True(t, e.NearLimit(0.8))
assert.False(t, e.NearLimit(0.9))
}
func TestEntitlement_NearLimit_Bad_Unlimited(t *testing.T) {
e := Entitlement{Allowed: true, Unlimited: true}
assert.False(t, e.NearLimit(0.8), "unlimited should never be near limit")
}
func TestEntitlement_NearLimit_Ugly_ZeroLimit(t *testing.T) {
e := Entitlement{Allowed: true, Limit: 0}
assert.False(t, e.NearLimit(0.8), "boolean gate (limit=0) should not report near limit")
}
// --- UsagePercent ---
func TestEntitlement_UsagePercent_Good(t *testing.T) {
e := Entitlement{Limit: 100, Used: 75}
assert.Equal(t, 75.0, e.UsagePercent())
}
func TestEntitlement_UsagePercent_Ugly_ZeroLimit(t *testing.T) {
e := Entitlement{Limit: 0, Used: 5}
assert.Equal(t, 0.0, e.UsagePercent(), "zero limit = boolean gate, no percentage")
}
// --- RecordUsage ---
func TestEntitlement_RecordUsage_Good(t *testing.T) {
c := New()
var recorded string
var recordedQty int
c.SetUsageRecorder(func(action string, qty int, ctx context.Context) {
recorded = action
recordedQty = qty
})
c.RecordUsage("ai.credits", 10)
assert.Equal(t, "ai.credits", recorded)
assert.Equal(t, 10, recordedQty)
}
func TestEntitlement_RecordUsage_Good_NoRecorder(t *testing.T) {
c := New()
// No recorder set — should not panic
assert.NotPanics(t, func() {
c.RecordUsage("anything", 5)
})
}
// --- Permission Model Integration ---
func TestEntitlement_Ugly_SaaSGatingPattern(t *testing.T) {
c := New()
// Simulate RFC-004 entitlement service
packages := map[string]int{
"social.accounts": 5,
"social.posts.scheduled": 100,
"ai.credits": 50,
}
usage := map[string]int{
"social.accounts": 3,
"social.posts.scheduled": 45,
"ai.credits": 48,
}
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
limit, hasFeature := packages[action]
if !hasFeature {
return Entitlement{Allowed: false, Reason: "feature not in package"}
}
used := usage[action]
remaining := limit - used
if qty > remaining {
return Entitlement{Allowed: false, Limit: limit, Used: used, Remaining: remaining, Reason: "limit exceeded"}
}
return Entitlement{Allowed: true, Limit: limit, Used: used, Remaining: remaining}
})
// Can create 2 social accounts
e := c.Entitled("social.accounts", 2)
assert.True(t, e.Allowed)
// AI credits near limit
e = c.Entitled("ai.credits", 1)
assert.True(t, e.Allowed)
assert.True(t, e.NearLimit(0.8))
assert.Equal(t, 96.0, e.UsagePercent())
// Feature not in package
e = c.Entitled("premium.feature")
assert.False(t, e.Allowed)
}