feat(rbac): initial stable release v0.9.0
Foundational identity and permission types for role-based access control — bit-set PermissionMask, immutable Identity value type, and PermissionProvider interface. What's included: - `Identity` value type with NewIdentity / WithTenant constructors and SetInContext / FromContext context helpers - `Permission` (int64 bit position) and `PermissionMask` (int64 bit-set) with O(1) Has and non-mutating Grant - `PermissionProvider` interface for DB-backed ResolveMask(ctx, uid, resource) resolution Tested-via: todo-api POC integration Reviewed-against: docs/adr/
This commit is contained in:
95
permission_test.go
Normal file
95
permission_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package rbac
|
||||
|
||||
import "testing"
|
||||
|
||||
// App-level permission constants used only within this test file.
|
||||
const (
|
||||
read Permission = 0
|
||||
write Permission = 1
|
||||
del Permission = 2
|
||||
admin Permission = 62 // highest valid bit
|
||||
)
|
||||
|
||||
func TestPermissionMask_Has(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mask PermissionMask
|
||||
perm Permission
|
||||
want bool
|
||||
}{
|
||||
{"bit 0 set", 1, read, true},
|
||||
{"bit 4 set", 16, Permission(4), true},
|
||||
{"multi-bit, check bit 0", 17, read, true},
|
||||
{"multi-bit, check bit 4", 17, Permission(4), true},
|
||||
{"bit not set", 16, read, false},
|
||||
{"out of range low", 1, Permission(-1), false},
|
||||
{"out of range high", 1, Permission(63), false},
|
||||
{"max valid bit (62)", PermissionMask(1 << 62), admin, true},
|
||||
{"zero mask", 0, read, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.mask.Has(tt.perm); got != tt.want {
|
||||
t.Errorf("PermissionMask(%d).Has(%d) = %v, want %v", tt.mask, tt.perm, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionMask_Grant(t *testing.T) {
|
||||
mask := PermissionMask(0)
|
||||
|
||||
mask = mask.Grant(read)
|
||||
if !mask.Has(read) {
|
||||
t.Error("Grant(read): read not set")
|
||||
}
|
||||
if mask.Has(write) {
|
||||
t.Error("Grant(read): write must not be set")
|
||||
}
|
||||
|
||||
mask = mask.Grant(write)
|
||||
if !mask.Has(read) {
|
||||
t.Error("Grant(write): read must still be set")
|
||||
}
|
||||
if !mask.Has(write) {
|
||||
t.Error("Grant(write): write not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionMask_Grant_Chain(t *testing.T) {
|
||||
mask := PermissionMask(0).Grant(read).Grant(write).Grant(del)
|
||||
|
||||
if !mask.Has(read) {
|
||||
t.Error("chained Grant: read not set")
|
||||
}
|
||||
if !mask.Has(write) {
|
||||
t.Error("chained Grant: write not set")
|
||||
}
|
||||
if !mask.Has(del) {
|
||||
t.Error("chained Grant: del not set")
|
||||
}
|
||||
if mask.Has(admin) {
|
||||
t.Error("chained Grant: admin must not be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionMask_Grant_OutOfRange(t *testing.T) {
|
||||
mask := PermissionMask(0)
|
||||
result := mask.Grant(Permission(-1))
|
||||
if result != mask {
|
||||
t.Error("Grant with out-of-range permission must return the original mask unchanged")
|
||||
}
|
||||
result = mask.Grant(Permission(63))
|
||||
if result != mask {
|
||||
t.Error("Grant with out-of-range permission (63) must return the original mask unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionMask_Grant_DoesNotMutateReceiver(t *testing.T) {
|
||||
original := PermissionMask(0)
|
||||
_ = original.Grant(read)
|
||||
if original.Has(read) {
|
||||
t.Error("Grant mutated the receiver; PermissionMask must be a value type")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user