Files
upstream-casbin/transaction_test.go
upstream-mirror d8971e16fb
Some checks failed
Build / test (1.21) (push) Successful in 2m21s
Build / benchmark (1.21) (push) Failing after 2m28s
golangci-lint / golangci (push) Successful in 19m40s
snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:46:19 +05:00

579 lines
17 KiB
Go

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"context"
"errors"
"testing"
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
)
// MockTransactionalAdapter implements TransactionalAdapter interface for testing.
type MockTransactionalAdapter struct {
Enforcer *Enforcer
}
// MockTransactionContext implements TransactionContext interface for testing.
type MockTransactionContext struct {
adapter *MockTransactionalAdapter
committed bool
rolledBack bool
}
// NewMockTransactionalAdapter creates a new mock adapter.
func NewMockTransactionalAdapter() *MockTransactionalAdapter {
return &MockTransactionalAdapter{}
}
// LoadPolicy implements Adapter interface.
func (a *MockTransactionalAdapter) LoadPolicy(model model.Model) error {
return nil
}
// SavePolicy implements Adapter interface.
func (a *MockTransactionalAdapter) SavePolicy(model model.Model) error {
return nil
}
// AddPolicy implements Adapter interface.
func (a *MockTransactionalAdapter) AddPolicy(sec string, ptype string, rule []string) error {
return nil
}
// RemovePolicy implements Adapter interface.
func (a *MockTransactionalAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
return nil
}
// RemoveFilteredPolicy implements Adapter interface.
func (a *MockTransactionalAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return nil
}
// BeginTransaction implements TransactionalAdapter interface.
func (a *MockTransactionalAdapter) BeginTransaction(ctx context.Context) (persist.TransactionContext, error) {
return &MockTransactionContext{adapter: a}, nil
}
// Commit implements TransactionContext interface.
func (tx *MockTransactionContext) Commit() error {
if tx.committed || tx.rolledBack {
return errors.New("transaction already finished")
}
tx.committed = true
return nil
}
// Rollback implements TransactionContext interface.
func (tx *MockTransactionContext) Rollback() error {
if tx.committed || tx.rolledBack {
return errors.New("transaction already finished")
}
tx.rolledBack = true
return nil
}
// GetAdapter implements TransactionContext interface.
func (tx *MockTransactionContext) GetAdapter() persist.Adapter {
return tx.adapter
}
// Test basic transaction functionality.
func TestTransactionBasicOperations(t *testing.T) {
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
ctx := context.Background()
// Begin transaction.
tx, err := e.BeginTransaction(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction: %v", err)
}
// Add policies in transaction.
ok, err := tx.AddPolicy("alice", "data1", "read")
if !ok || err != nil {
t.Fatalf("Failed to add policy in transaction: %v", err)
}
ok, err = tx.AddPolicy("bob", "data2", "write")
if !ok || err != nil {
t.Fatalf("Failed to add policy in transaction: %v", err)
}
// Commit transaction.
if err := tx.Commit(); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
// Verify transaction was committed.
if !tx.IsCommitted() {
t.Error("Transaction should be committed")
}
}
// Test transaction rollback.
func TestTransactionRollback(t *testing.T) {
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
ctx := context.Background()
// Begin transaction.
tx, err := e.BeginTransaction(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction: %v", err)
}
// Add policy in transaction.
ok, err := tx.AddPolicy("alice", "data1", "read")
if !ok || err != nil {
t.Fatalf("Failed to add policy in transaction: %v", err)
}
// Rollback transaction.
if err := tx.Rollback(); err != nil {
t.Fatalf("Failed to rollback transaction: %v", err)
}
// Verify transaction was rolled back.
if !tx.IsRolledBack() {
t.Error("Transaction should be rolled back")
}
}
// Test concurrent transactions.
func TestConcurrentTransactions(t *testing.T) {
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
ctx := context.Background()
// Start first transaction
tx1, err := e.BeginTransaction(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction 1: %v", err)
}
// Add policy in first transaction
ok, err := tx1.AddPolicy("alice", "data1", "read")
if !ok || err != nil {
t.Fatalf("Failed to add policy in transaction 1: %v", err)
}
// Start second transaction
tx2, err := e.BeginTransaction(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction 2: %v", err)
}
// Add different policy in second transaction
ok, err = tx2.AddPolicy("bob", "data2", "write")
if !ok || err != nil {
t.Fatalf("Failed to add policy in transaction 2: %v", err)
}
// Commit first transaction
if err := tx1.Commit(); err != nil {
t.Fatalf("Failed to commit transaction 1: %v", err)
}
// Commit second transaction
if err := tx2.Commit(); err != nil {
t.Fatalf("Failed to commit transaction 2: %v", err)
}
// Verify transactions were committed
if !tx1.IsCommitted() {
t.Error("Transaction 1 should be committed")
}
if !tx2.IsCommitted() {
t.Error("Transaction 2 should be committed")
}
}
// Test transaction conflicts.
func TestTransactionConflicts(t *testing.T) {
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
ctx := context.Background()
// Test Case 1: Two transactions commit
t.Run("TwoTransactionsCommit", func(t *testing.T) {
tx1, _ := e.BeginTransaction(ctx)
tx2, _ := e.BeginTransaction(ctx)
// Commit both transactions
if err := tx1.Commit(); err != nil {
t.Fatalf("Failed to commit tx1: %v", err)
}
if err := tx2.Commit(); err != nil {
t.Fatalf("Failed to commit tx2: %v", err)
}
// Verify both transactions were committed
if !tx1.IsCommitted() {
t.Error("Transaction 1 should be committed")
}
if !tx2.IsCommitted() {
t.Error("Transaction 2 should be committed")
}
})
// Test Case 2: Transaction rollback
t.Run("TransactionRollback", func(t *testing.T) {
tx, _ := e.BeginTransaction(ctx)
// Rollback transaction
if err := tx.Rollback(); err != nil {
t.Fatalf("Failed to rollback transaction: %v", err)
}
// Verify transaction was rolled back
if !tx.IsRolledBack() {
t.Error("Transaction should be rolled back")
}
})
// Test Case 3: Cannot commit after rollback
t.Run("NoCommitAfterRollback", func(t *testing.T) {
tx, _ := e.BeginTransaction(ctx)
// Rollback transaction
if err := tx.Rollback(); err != nil {
t.Fatalf("Failed to rollback transaction: %v", err)
}
// Try to commit
if err := tx.Commit(); err == nil {
t.Error("Should not be able to commit after rollback")
}
})
}
// Test transaction buffer operations.
func TestTransactionBuffer(t *testing.T) {
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
ctx := context.Background()
tx, err := e.BeginTransaction(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction: %v", err)
}
// Initially no operations.
if tx.HasOperations() {
t.Fatal("Transaction should have no operations initially")
}
if tx.OperationCount() != 0 {
t.Fatal("Operation count should be 0 initially")
}
// Add some operations.
tx.AddPolicy("alice", "data1", "read")
tx.AddPolicy("bob", "data2", "write")
if !tx.HasOperations() {
t.Fatal("Transaction should have operations")
}
if tx.OperationCount() != 2 {
t.Fatalf("Expected 2 operations, got %d", tx.OperationCount())
}
// Get buffered model.
bufferedModel, err := tx.GetBufferedModel()
if err != nil {
t.Fatalf("Failed to get buffered model: %v", err)
}
// Check that buffered model contains the policies.
hasPolicy, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"})
if !hasPolicy {
t.Fatal("Buffered model should contain the added policy")
}
tx.Rollback()
}
// newTestTransactionalEnforcer creates a transactional enforcer backed by the mock adapter.
func newTestTransactionalEnforcer(t *testing.T) *TransactionalEnforcer {
t.Helper()
adapter := NewMockTransactionalAdapter()
e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
if err != nil {
t.Fatalf("Failed to create transactional enforcer: %v", err)
}
adapter.Enforcer = e.Enforcer
return e
}
// hasPolicy reports whether the enforcer model holds the given policy rule.
func hasPolicy(t *testing.T, e *TransactionalEnforcer, params ...interface{}) bool {
t.Helper()
ok, err := e.HasPolicy(params...)
if err != nil {
t.Fatalf("Failed to check policy %v: %v", params, err)
}
return ok
}
// hasGroupingPolicy reports whether the enforcer model holds the given grouping policy rule.
func hasGroupingPolicy(t *testing.T, e *TransactionalEnforcer, params ...interface{}) bool {
t.Helper()
ok, err := e.HasGroupingPolicy(params...)
if err != nil {
t.Fatalf("Failed to check grouping policy %v: %v", params, err)
}
return ok
}
// Test batch grouping policy operations within a transaction.
func TestTransactionGroupingPolicies(t *testing.T) {
e := newTestTransactionalEnforcer(t)
ctx := context.Background()
err := e.WithTransaction(ctx, func(tx *Transaction) error {
if ok, err := tx.AddPolicy("admin", "data1", "read"); !ok || err != nil {
t.Fatalf("Failed to add policy in transaction: %v", err)
}
if ok, err := tx.AddGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}}); !ok || err != nil {
t.Fatalf("Failed to add grouping policies in transaction: %v", err)
}
return nil
})
if err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if !hasGroupingPolicy(t, e, "alice", "admin") || !hasGroupingPolicy(t, e, "bob", "admin") {
t.Fatal("Both grouping policies should have been added")
}
// Role links must have been rebuilt for the committed grouping policies.
if allowed, enforceErr := e.Enforce("alice", "data1", "read"); enforceErr != nil || !allowed {
t.Fatalf("alice should inherit the admin permission, got %v (err %v)", allowed, enforceErr)
}
// Removing them again must take effect too.
err = e.WithTransaction(ctx, func(tx *Transaction) error {
removed, removeErr := tx.RemoveGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}})
if !removed || removeErr != nil {
t.Fatalf("Failed to remove grouping policies in transaction: %v", removeErr)
}
return nil
})
if err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, "bob", "admin") {
t.Fatal("Both grouping policies should have been removed")
}
if ok, _ := e.Enforce("alice", "data1", "read"); ok {
t.Fatal("alice should no longer inherit the admin permission")
}
}
// Test updating a grouping policy within a transaction.
func TestTransactionUpdateGroupingPolicy(t *testing.T) {
e := newTestTransactionalEnforcer(t)
ctx := context.Background()
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
_, err := tx.AddGroupingPolicy("alice", "admin")
return err
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
// Updating a rule that does not exist changes nothing.
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
ok, err := tx.UpdateGroupingPolicy([]string{"carol", "admin"}, []string{"carol", "user"})
if err != nil {
return err
}
if ok {
t.Fatal("Updating a missing grouping policy should return false")
}
return nil
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
// Mismatched batch lengths are rejected.
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
if _, err := tx.UpdateGroupingPolicies([][]string{{"alice", "admin"}}, [][]string{}); err == nil {
t.Fatal("Mismatched old/new rule counts should return an error")
}
return nil
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
ok, err := tx.UpdateGroupingPolicy([]string{"alice", "admin"}, []string{"alice", "user"})
if !ok || err != nil {
t.Fatalf("Failed to update grouping policy in transaction: %v", err)
}
return nil
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if hasGroupingPolicy(t, e, "alice", "admin") {
t.Fatal("The old grouping policy should have been removed")
}
if !hasGroupingPolicy(t, e, "alice", "user") {
t.Fatal("The new grouping policy should have been added")
}
}
// Test filtered removal within a transaction.
func TestTransactionRemoveFilteredPolicy(t *testing.T) {
e := newTestTransactionalEnforcer(t)
ctx := context.Background()
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
if _, err := tx.AddPolicies([][]string{
{"alice", "data1", "read"},
{"alice", "data2", "write"},
{"bob", "data2", "write"},
}); err != nil {
return err
}
_, err := tx.AddGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}})
return err
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
// A filter that matches nothing is a no-op.
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
ok, err := tx.RemoveFilteredPolicy(0, "carol")
if err != nil {
return err
}
if ok || tx.HasOperations() {
t.Fatal("A filter matching no rule should buffer nothing")
}
return nil
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
ok, err := tx.RemoveFilteredPolicy(0, "alice")
if !ok || err != nil {
t.Fatalf("Failed to remove filtered policy in transaction: %v", err)
}
// The removal is visible inside the transaction before committing.
bufferedModel, err := tx.GetBufferedModel()
if err != nil {
return err
}
if has, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"}); has {
t.Fatal("The buffered model should no longer contain alice's policy")
}
// But not outside of it.
if !hasPolicy(t, e, "alice", "data1", "read") {
t.Fatal("The enforcer model should still contain alice's policy before commit")
}
ok, err = tx.RemoveFilteredGroupingPolicy(1, "admin")
if !ok || err != nil {
t.Fatalf("Failed to remove filtered grouping policy in transaction: %v", err)
}
return nil
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
if hasPolicy(t, e, "alice", "data1", "read") || hasPolicy(t, e, "alice", "data2", "write") {
t.Fatal("All of alice's policies should have been removed")
}
if !hasPolicy(t, e, "bob", "data2", "write") {
t.Fatal("bob's policy should have been kept")
}
if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, "bob", "admin") {
t.Fatal("All grouping policies for admin should have been removed")
}
}
// Test that a rolled back filtered removal leaves the model untouched.
func TestTransactionRemoveFilteredPolicyRollback(t *testing.T) {
e := newTestTransactionalEnforcer(t)
ctx := context.Background()
if err := e.WithTransaction(ctx, func(tx *Transaction) error {
_, err := tx.AddPolicy("alice", "data1", "read")
return err
}); err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
wantErr := errors.New("business operation failed")
err := e.WithTransaction(ctx, func(tx *Transaction) error {
if _, err := tx.RemoveFilteredPolicy(0, "alice"); err != nil {
return err
}
return wantErr
})
if !errors.Is(err, wantErr) {
t.Fatalf("Expected the business error to be returned, got %v", err)
}
if !hasPolicy(t, e, "alice", "data1", "read") {
t.Fatal("The policy should still exist after a rollback")
}
}