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:
93
identity_test.go
Normal file
93
identity_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIdentity(t *testing.T) {
|
||||
id := NewIdentity("uid-1", "Jane Doe", "jane@example.com")
|
||||
|
||||
if id.UID != "uid-1" {
|
||||
t.Errorf("UID = %q, want uid-1", id.UID)
|
||||
}
|
||||
if id.DisplayName != "Jane Doe" {
|
||||
t.Errorf("DisplayName = %q, want Jane Doe", id.DisplayName)
|
||||
}
|
||||
if id.Email != "jane@example.com" {
|
||||
t.Errorf("Email = %q, want jane@example.com", id.Email)
|
||||
}
|
||||
if id.TenantID != "" {
|
||||
t.Errorf("TenantID must be empty at construction, got %q", id.TenantID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity_WithTenant(t *testing.T) {
|
||||
original := NewIdentity("uid-1", "Jane Doe", "jane@example.com")
|
||||
enriched := original.WithTenant("tenant-abc")
|
||||
|
||||
if enriched.TenantID != "tenant-abc" {
|
||||
t.Errorf("enriched.TenantID = %q, want tenant-abc", enriched.TenantID)
|
||||
}
|
||||
// WithTenant must not mutate the original.
|
||||
if original.TenantID != "" {
|
||||
t.Errorf("original.TenantID mutated to %q, must remain empty", original.TenantID)
|
||||
}
|
||||
// Other fields must be preserved.
|
||||
if enriched.UID != original.UID {
|
||||
t.Errorf("WithTenant changed UID: got %q, want %q", enriched.UID, original.UID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextHelpers_SetAndGet(t *testing.T) {
|
||||
id := NewIdentity("uid-1", "Jane Doe", "jane@example.com")
|
||||
ctx := SetInContext(context.Background(), id)
|
||||
|
||||
retrieved, ok := FromContext(ctx)
|
||||
if !ok {
|
||||
t.Fatal("FromContext returned ok=false, expected to find identity")
|
||||
}
|
||||
if retrieved.UID != id.UID {
|
||||
t.Errorf("retrieved UID = %q, want %q", retrieved.UID, id.UID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextHelpers_MissingReturnsZeroValue(t *testing.T) {
|
||||
id, ok := FromContext(context.Background())
|
||||
if ok {
|
||||
t.Error("FromContext on empty context returned ok=true")
|
||||
}
|
||||
if id != (Identity{}) {
|
||||
t.Errorf("FromContext on empty context returned non-zero Identity: %+v", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextHelpers_ValueSemanticsOnSet(t *testing.T) {
|
||||
id := NewIdentity("uid-1", "Jane Doe", "jane@example.com")
|
||||
ctx := SetInContext(context.Background(), id)
|
||||
|
||||
// Mutating the original struct after storing must not affect the context value
|
||||
// because Identity is a value type.
|
||||
id.UID = "mutated"
|
||||
|
||||
retrieved, _ := FromContext(ctx)
|
||||
if retrieved.UID == "mutated" {
|
||||
t.Error("context value was mutated after SetInContext; Identity must be a value type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextHelpers_OverwriteInContext(t *testing.T) {
|
||||
first := NewIdentity("uid-1", "Jane", "jane@example.com")
|
||||
second := NewIdentity("uid-2", "John", "john@example.com")
|
||||
|
||||
ctx := SetInContext(context.Background(), first)
|
||||
ctx = SetInContext(ctx, second)
|
||||
|
||||
retrieved, ok := FromContext(ctx)
|
||||
if !ok {
|
||||
t.Fatal("expected identity in context")
|
||||
}
|
||||
if retrieved.UID != "uid-2" {
|
||||
t.Errorf("expected overwritten identity uid-2, got %s", retrieved.UID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user