diff --git a/examples/gno.land/p/moul/authz/authz.gno b/examples/gno.land/p/moul/authz/authz.gno new file mode 100644 index 00000000000..059d1234b10 --- /dev/null +++ b/examples/gno.land/p/moul/authz/authz.gno @@ -0,0 +1,261 @@ +// Package authz provides flexible authorization control for privileged actions. +// +// # Authorization Strategies +// +// The package supports multiple authorization strategies: +// - Member-based: Single user or team of users +// - Contract-based: Async authorization (e.g., via DAO) +// - Auto-accept: Allow all actions +// - Drop: Deny all actions +// +// Core Components +// +// - Authority interface: Base interface implemented by all authorities +// - Authorizer: Main wrapper object for authority management +// - MemberAuthority: Manages authorized addresses +// - ContractAuthority: Delegates to another contract +// - AutoAcceptAuthority: Accepts all actions +// - DroppedAuthority: Denies all actions +// +// Quick Start +// +// // Initialize with contract deployer as authority +// var auth = authz.New() +// +// // Create functions that require authorization +// func UpdateConfig(newValue string) error { +// return auth.Do("update_config", func() error { +// config = newValue +// return nil +// }) +// } +// +// See example_test.gno for more usage examples. +package authz + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/addrset" + "gno.land/p/moul/once" +) + +// Authorizer is the main wrapper object that handles authority management +type Authorizer struct { + current Authority +} + +// Authority represents an entity that can authorize privileged actions +type Authority interface { + // Authorize executes a privileged action if the caller is authorized + // Additional args can be provided for context (e.g., for proposal creation) + Authorize(title string, action PrivilegedAction, args ...interface{}) error + + // String returns a human-readable description of the authority + String() string +} + +// PrivilegedAction defines a function that performs a privileged action. +type PrivilegedAction func() error + +// PrivilegedActionHandler is called by contract-based authorities to handle +// privileged actions. +type PrivilegedActionHandler func(title string, action PrivilegedAction) error + +// New creates a new Authorizer with the current realm's address as authority +func New() *Authorizer { + return &Authorizer{ + current: NewMemberAuthority(std.PreviousRealm().Address()), + } +} + +// NewWithAuthority creates a new Authorizer with a specific authority +func NewWithAuthority(authority Authority) *Authorizer { + return &Authorizer{ + current: authority, + } +} + +// Current returns the current authority implementation +func (a *Authorizer) Current() Authority { + return a.current +} + +// Transfer changes the current authority after validation +func (a *Authorizer) Transfer(newAuthority Authority) error { + // Ask current authority to validate the transfer + return a.current.Authorize("transfer_authority", func() error { + a.current = newAuthority + return nil + }) +} + +// Do executes a privileged action through the current authority +func (a *Authorizer) Do(title string, action PrivilegedAction, args ...interface{}) error { + return a.current.Authorize(title, action, args...) +} + +// String returns a string representation of the current authority +func (a *Authorizer) String() string { + return a.current.String() +} + +// MemberAuthority is the default implementation using addrset for member management +type MemberAuthority struct { + members addrset.Set +} + +func NewMemberAuthority(members ...std.Address) *MemberAuthority { + auth := &MemberAuthority{} + for _, addr := range members { + auth.members.Add(addr) + } + return auth +} + +func (a *MemberAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + caller := std.PreviousRealm().Address() + if !a.members.Has(caller) { + return errors.New("unauthorized") + } + + if err := action(); err != nil { + return err + } + return nil +} + +func (a *MemberAuthority) String() string { + return ufmt.Sprintf("member_authority[size=%d]", a.members.Size()) +} + +// AddMember adds a new member to the authority +func (a *MemberAuthority) AddMember(addr std.Address) error { + return a.Authorize("add_member", func() error { + a.members.Add(addr) + return nil + }) +} + +// RemoveMember removes a member from the authority +func (a *MemberAuthority) RemoveMember(addr std.Address) error { + return a.Authorize("remove_member", func() error { + a.members.Remove(addr) + return nil + }) +} + +// Tree returns a read-only view of the members tree +func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree { + tree := a.members.Tree().(*avl.Tree) + return rotree.Wrap(tree, nil) +} + +// Has checks if the given address is a member of the authority +func (a *MemberAuthority) Has(addr std.Address) bool { + return a.members.Has(addr) +} + +// ContractAuthority implements async contract-based authority +type ContractAuthority struct { + contractPath string + contractAddr std.Address + contractHandler PrivilegedActionHandler + proposer Authority // controls who can create proposals + executionOnce once.Once +} + +func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority { + return &ContractAuthority{ + contractPath: path, + contractAddr: std.DerivePkgAddr(path), + contractHandler: handler, + proposer: NewAutoAcceptAuthority(), // default: anyone can propose + executionOnce: once.Once{}, // initialize execution once + } +} + +// NewRestrictedContractAuthority creates a new contract authority with a proposer restriction +func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority { + if path == "" { + panic("contract path cannot be empty") + } + if handler == nil { + panic("contract handler cannot be nil") + } + if proposer == nil { + panic("proposer cannot be nil") + } + return &ContractAuthority{ + contractPath: path, + contractAddr: std.DerivePkgAddr(path), + contractHandler: handler, + proposer: proposer, + executionOnce: once.Once{}, + } +} + +func (a *ContractAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + if a.contractHandler == nil { + return errors.New("contract handler is not set") + } + + // Wrap the action to ensure it can only be executed by the contract + wrappedAction := func() error { + caller := std.PreviousRealm().Address() + if caller != a.contractAddr { + return errors.New("action can only be executed by the contract") + } + return a.executionOnce.DoErr(func() error { + return action() + }) + } + + // Use the proposer authority to control who can create proposals + return a.proposer.Authorize(title+"_proposal", func() error { + if err := a.contractHandler(title, wrappedAction); err != nil { + return err + } + return nil + }, args...) +} + +func (a *ContractAuthority) String() string { + return ufmt.Sprintf("contract_authority[contract=%s]", a.contractPath) +} + +// AutoAcceptAuthority implements an authority that accepts all actions +// AutoAcceptAuthority is a simple authority that automatically accepts all actions. +// It can be used as a proposer authority to allow anyone to create proposals. +type AutoAcceptAuthority struct{} + +func NewAutoAcceptAuthority() *AutoAcceptAuthority { + return &AutoAcceptAuthority{} +} + +func (a *AutoAcceptAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + return action() +} + +func (a *AutoAcceptAuthority) String() string { + return "auto_accept_authority" +} + +// droppedAuthority implements an authority that denies all actions +type droppedAuthority struct{} + +func NewDroppedAuthority() Authority { + return &droppedAuthority{} +} + +func (a *droppedAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + return errors.New("dropped authority: all actions are denied") +} + +func (a *droppedAuthority) String() string { + return "dropped_authority" +} diff --git a/examples/gno.land/p/moul/authz/authz_test.gno b/examples/gno.land/p/moul/authz/authz_test.gno new file mode 100644 index 00000000000..f8915cc2448 --- /dev/null +++ b/examples/gno.land/p/moul/authz/authz_test.gno @@ -0,0 +1,384 @@ +package authz + +import ( + "errors" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" +) + +func TestNew(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Check that the current authority is a MemberAuthority + memberAuth, ok := auth.Current().(*MemberAuthority) + uassert.True(t, ok, "expected MemberAuthority") + + // Check that the caller is a member + uassert.True(t, memberAuth.Has(testAddr), "caller should be a member") + + // Check string representation + expected := ufmt.Sprintf("member_authority[size=%d]", 1) + uassert.Equal(t, expected, auth.String()) +} + +func TestNewWithAuthority(t *testing.T) { + testAddr := testutils.TestAddress("alice") + memberAuth := NewMemberAuthority(testAddr) + + auth := NewWithAuthority(memberAuth) + + // Check that the current authority is the one we provided + uassert.True(t, auth.Current() == memberAuth, "expected provided authority") +} + +func TestAuthorizerAuthorize(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Test successful action with args + executed := false + args := []interface{}{"test_arg", 123} + err := auth.Do("test_action", func() error { + executed = true + return nil + }, args...) + + uassert.True(t, err == nil, "expected no error") + uassert.True(t, executed, "action should have been executed") + + // Test unauthorized action with args + std.TestSetOriginCaller(testutils.TestAddress("bob")) + + executed = false + err = auth.Do("test_action", func() error { + executed = true + return nil + }, "unauthorized_arg") + + uassert.True(t, err != nil, "expected error") + uassert.False(t, executed, "action should not have been executed") + + // Test action returning error + std.TestSetOriginCaller(testAddr) + expectedErr := errors.New("test error") + + err = auth.Do("test_action", func() error { + return expectedErr + }) + + uassert.True(t, err == expectedErr, "expected specific error") +} + +func TestAuthorizerTransfer(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Test transfer to new member authority + newAddr := testutils.TestAddress("bob") + newAuth := NewMemberAuthority(newAddr) + + err := auth.Transfer(newAuth) + uassert.True(t, err == nil, "expected no error") + uassert.True(t, auth.Current() == newAuth, "expected new authority") + + // Test unauthorized transfer + std.TestSetOriginCaller(testutils.TestAddress("carol")) + + err = auth.Transfer(NewMemberAuthority(testAddr)) + uassert.True(t, err != nil, "expected error") + + // Test transfer to contract authority + std.TestSetOriginCaller(newAddr) + + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + return action() + }) + + err = auth.Transfer(contractAuth) + uassert.True(t, err == nil, "expected no error") + uassert.True(t, auth.Current() == contractAuth, "expected contract authority") +} + +func TestAuthorizerTransferChain(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + // Create a chain of transfers + auth := New() + + // First transfer to a new member authority + newAddr := testutils.TestAddress("bob") + memberAuth := NewMemberAuthority(newAddr) + + err := auth.Transfer(memberAuth) + uassert.True(t, err == nil, "unexpected error in first transfer") + + // Then transfer to a contract authority + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + return action() + }) + + std.TestSetOriginCaller(newAddr) + err = auth.Transfer(contractAuth) + uassert.True(t, err == nil, "unexpected error in second transfer") + + // Finally transfer to an auto-accept authority + autoAuth := NewAutoAcceptAuthority() + + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = auth.Transfer(autoAuth) + uassert.True(t, err == nil, "unexpected error in final transfer") + uassert.True(t, auth.Current() == autoAuth, "expected auto-accept authority") +} + +func TestAuthorizerWithDroppedAuthority(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Transfer to dropped authority + err := auth.Transfer(NewDroppedAuthority()) + uassert.True(t, err == nil, "expected no error") + + // Try to execute action + err = auth.Do("test_action", func() error { + return nil + }) + uassert.True(t, err != nil, "expected error from dropped authority") + + // Try to transfer again + err = auth.Transfer(NewMemberAuthority(testAddr)) + uassert.True(t, err != nil, "expected error when transferring from dropped authority") +} + +func TestContractAuthorityExecutionOnce(t *testing.T) { + attempts := 0 + executed := 0 + var capturedArgs []interface{} + + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + // Try to execute the action twice in the same handler + if err := action(); err != nil { + return err + } + attempts++ + + // Second execution should fail + if err := action(); err != nil { + return err + } + attempts++ + return nil + }) + + // Set caller to contract address + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + + testArgs := []interface{}{"proposal_id", 42, "metadata", map[string]string{"key": "value"}} + err := contractAuth.Authorize("test_action", func() error { + executed++ + return nil + }, testArgs...) + + uassert.True(t, err == nil, "handler execution should succeed") + uassert.True(t, attempts == 2, "handler should have attempted execution twice") + uassert.True(t, executed == 1, "handler should have executed once") +} + +func TestContractAuthorityWithProposer(t *testing.T) { + testAddr := testutils.TestAddress("alice") + memberAuth := NewMemberAuthority(testAddr) + + handlerCalled := false + actionExecuted := false + + contractAuth := NewRestrictedContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + handlerCalled = true + // Set caller to contract address before executing action + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + return action() + }, memberAuth) + + // Test authorized member + std.TestSetOriginCaller(testAddr) + testArgs := []interface{}{"proposal_metadata", "test value"} + err := contractAuth.Authorize("test_action", func() error { + actionExecuted = true + return nil + }, testArgs...) + + uassert.True(t, err == nil, "authorized member should be able to propose") + uassert.True(t, handlerCalled, "contract handler should be called") + uassert.True(t, actionExecuted, "action should be executed") + + // Reset flags for unauthorized test + handlerCalled = false + actionExecuted = false + + // Test unauthorized proposer + std.TestSetOriginCaller(testutils.TestAddress("bob")) + err = contractAuth.Authorize("test_action", func() error { + actionExecuted = true + return nil + }, testArgs...) + + uassert.True(t, err != nil, "unauthorized member should not be able to propose") + uassert.False(t, handlerCalled, "contract handler should not be called for unauthorized proposer") + uassert.False(t, actionExecuted, "action should not be executed for unauthorized proposer") +} + +func TestAutoAcceptAuthority(t *testing.T) { + auth := NewAutoAcceptAuthority() + + // Test that any action is authorized + executed := false + err := auth.Authorize("test_action", func() error { + executed = true + return nil + }) + + uassert.True(t, err == nil, "auto-accept should not return error") + uassert.True(t, executed, "action should have been executed") + + // Test with different caller + std.TestSetOriginCaller(testutils.TestAddress("random")) + executed = false + err = auth.Authorize("test_action", func() error { + executed = true + return nil + }) + + uassert.True(t, err == nil, "auto-accept should not care about caller") + uassert.True(t, executed, "action should have been executed") +} + +func TestAutoAcceptAuthorityWithArgs(t *testing.T) { + auth := NewAutoAcceptAuthority() + + // Test that any action is authorized with args + executed := false + testArgs := []interface{}{"arg1", 42, "arg3"} + err := auth.Authorize("test_action", func() error { + executed = true + return nil + }, testArgs...) + + uassert.True(t, err == nil, "auto-accept should not return error") + uassert.True(t, executed, "action should have been executed") +} + +func TestMemberAuthorityMultipleMembers(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + carol := testutils.TestAddress("carol") + + // Create authority with multiple members + auth := NewMemberAuthority(alice, bob) + + // Test that both members can execute actions + for _, member := range []std.Address{alice, bob} { + std.TestSetOriginCaller(member) + err := auth.Authorize("test_action", func() error { + return nil + }) + uassert.True(t, err == nil, "member should be authorized") + } + + // Test that non-member cannot execute + std.TestSetOriginCaller(carol) + err := auth.Authorize("test_action", func() error { + return nil + }) + uassert.True(t, err != nil, "non-member should not be authorized") + + // Test Tree() functionality + tree := auth.Tree() + uassert.True(t, tree.Size() == 2, "tree should have 2 members") + + // Verify both members are in the tree + found := make(map[std.Address]bool) + tree.Iterate("", "", func(key string, _ interface{}) bool { + found[std.Address(key)] = true + return false + }) + uassert.True(t, found[alice], "alice should be in the tree") + uassert.True(t, found[bob], "bob should be in the tree") + uassert.False(t, found[carol], "carol should not be in the tree") + + // Test read-only nature of the tree + defer func() { + r := recover() + uassert.True(t, r != nil, "modifying read-only tree should panic") + }() + tree.Set(string(carol), nil) // This should panic +} + +func TestAuthorizerCurrentNeverNil(t *testing.T) { + auth := New() + + // Current should never be nil after initialization + uassert.True(t, auth.Current() != nil, "current authority should not be nil") + + // Current should not be nil after transfer + err := auth.Transfer(NewAutoAcceptAuthority()) + uassert.True(t, err == nil, "transfer should succeed") + uassert.True(t, auth.Current() != nil, "current authority should not be nil after transfer") +} + +func TestAuthorizerString(t *testing.T) { + auth := New() + + // Test initial string representation + str := auth.String() + uassert.True(t, str != "", "string representation should not be empty") + + // Test string after transfer + autoAuth := NewAutoAcceptAuthority() + err := auth.Transfer(autoAuth) + uassert.True(t, err == nil, "transfer should succeed") + + newStr := auth.String() + uassert.True(t, newStr != "", "string representation should not be empty") + uassert.True(t, newStr != str, "string should change after transfer") +} + +func TestContractAuthorityValidation(t *testing.T) { + // Test empty path + auth := NewContractAuthority("", nil) + std.TestSetOriginCaller(std.DerivePkgAddr("")) + err := auth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err != nil, "empty path authority should fail to authorize") + + // Test nil handler + auth = NewContractAuthority("gno.land/r/test", nil) + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = auth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err != nil, "nil handler authority should fail to authorize") + + // Test valid configuration + handler := func(title string, action PrivilegedAction) error { + return nil + } + contractAuth := NewContractAuthority("gno.land/r/test", handler) + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = contractAuth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err == nil, "valid contract authority should authorize successfully") +} diff --git a/examples/gno.land/p/moul/authz/example_test.gno b/examples/gno.land/p/moul/authz/example_test.gno new file mode 100644 index 00000000000..a283204dfc2 --- /dev/null +++ b/examples/gno.land/p/moul/authz/example_test.gno @@ -0,0 +1,88 @@ +package authz + +import ( + "std" +) + +// Example_basic demonstrates initializing and using a basic member authority +func Example_basic() { + // Initialize with contract deployer as authority + auth := New() + + // Use the authority to perform a privileged action + auth.Do("update_config", func() error { + // config = newValue + return nil + }) +} + +// Example_addingMembers demonstrates how to add new members to a member authority +func Example_addingMembers() { + // Initialize with contract deployer as authority + auth := New() + + // Add a new member to the authority + memberAuth := auth.Current().(*MemberAuthority) + memberAuth.AddMember(std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")) +} + +// Example_contractAuthority demonstrates using a contract-based authority +func Example_contractAuthority() { + // Initialize with contract authority (e.g., DAO) + auth := NewWithAuthority( + NewContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, // defined elsewhere for example + ), + ) + + // Privileged actions will be handled by the contract + auth.Do("update_params", func() error { + // Executes after DAO approval + return nil + }) +} + +// Example_restrictedContractAuthority demonstrates a contract authority with member-only proposals +func Example_restrictedContractAuthority() { + // Initialize member authority for proposers + proposerAuth := NewMemberAuthority( + std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), // admin1 + std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"), // admin2 + ) + + // Create contract authority with restricted proposers + auth := NewWithAuthority( + NewRestrictedContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, + proposerAuth, + ), + ) + + // Only members can propose, and contract must approve + auth.Do("update_params", func() error { + // Executes after: + // 1. Proposer initiates + // 2. DAO approves + return nil + }) +} + +// Example_switchingAuthority demonstrates switching from member to contract authority +func Example_switchingAuthority() { + // Start with member authority (deployer) + auth := New() + + // Create and switch to contract authority + daoAuthority := NewContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, + ) + auth.Transfer(daoAuthority) +} + +// Mock handler for examples +func mockDAOHandler(title string, action PrivilegedAction) error { + return action() +} diff --git a/examples/gno.land/p/moul/authz/gno.mod b/examples/gno.land/p/moul/authz/gno.mod new file mode 100644 index 00000000000..2ae057d875b --- /dev/null +++ b/examples/gno.land/p/moul/authz/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/authz diff --git a/examples/gno.land/p/moul/once/gno.mod b/examples/gno.land/p/moul/once/gno.mod new file mode 100644 index 00000000000..29afe6841b2 --- /dev/null +++ b/examples/gno.land/p/moul/once/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/once diff --git a/examples/gno.land/p/moul/once/once.gno b/examples/gno.land/p/moul/once/once.gno new file mode 100644 index 00000000000..b9b6054c037 --- /dev/null +++ b/examples/gno.land/p/moul/once/once.gno @@ -0,0 +1,100 @@ +// Package once provides utilities for one-time execution patterns. +// It extends the concept of sync.Once with error handling and panic options. +package once + +import ( + "errors" +) + +// Once represents a one-time execution guard +type Once struct { + done bool + err error + paniced bool + value interface{} // stores the result of the execution +} + +// New creates a new Once instance +func New() *Once { + return &Once{} +} + +// Do executes fn only once and returns nil on subsequent calls +func (o *Once) Do(fn func()) { + if o.done { + return + } + defer func() { o.done = true }() + fn() +} + +// DoErr executes fn only once and returns the same error on subsequent calls +func (o *Once) DoErr(fn func() error) error { + if o.done { + return o.err + } + defer func() { o.done = true }() + o.err = fn() + return o.err +} + +// DoOrPanic executes fn only once and panics on subsequent calls +func (o *Once) DoOrPanic(fn func()) { + if o.done { + panic("once: multiple execution attempted") + } + defer func() { o.done = true }() + fn() +} + +// DoValue executes fn only once and returns its value, subsequent calls return the cached value +func (o *Once) DoValue(fn func() interface{}) interface{} { + if o.done { + return o.value + } + defer func() { o.done = true }() + o.value = fn() + return o.value +} + +// DoValueErr executes fn only once and returns its value and error +// Subsequent calls return the cached value and error +func (o *Once) DoValueErr(fn func() (interface{}, error)) (interface{}, error) { + if o.done { + return o.value, o.err + } + defer func() { o.done = true }() + o.value, o.err = fn() + return o.value, o.err +} + +// Reset resets the Once instance to its initial state +// This is mainly useful for testing purposes +func (o *Once) Reset() { + o.done = false + o.err = nil + o.paniced = false + o.value = nil +} + +// IsDone returns whether the Once has been executed +func (o *Once) IsDone() bool { + return o.done +} + +// Error returns the error from the last execution if any +func (o *Once) Error() error { + return o.err +} + +var ( + ErrNotExecuted = errors.New("once: not executed yet") +) + +// Value returns the stored value and an error if not executed yet +func (o *Once) Value() (interface{}, error) { + if !o.done { + return nil, ErrNotExecuted + } + return o.value, nil +} diff --git a/examples/gno.land/p/moul/once/once_test.gno b/examples/gno.land/p/moul/once/once_test.gno new file mode 100644 index 00000000000..7d6114f4a5a --- /dev/null +++ b/examples/gno.land/p/moul/once/once_test.gno @@ -0,0 +1,231 @@ +package once + +import ( + "errors" + "testing" +) + +func TestOnce_Do(t *testing.T) { + counter := 0 + once := New() + + increment := func() { + counter++ + } + + // First call should execute + once.Do(increment) + if counter != 1 { + t.Errorf("expected counter to be 1, got %d", counter) + } + + // Second call should not execute + once.Do(increment) + if counter != 1 { + t.Errorf("expected counter to still be 1, got %d", counter) + } +} + +func TestOnce_DoErr(t *testing.T) { + once := New() + expectedErr := errors.New("test error") + + fn := func() error { + return expectedErr + } + + // First call should return error + if err := once.DoErr(fn); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } + + // Second call should return same error + if err := once.DoErr(fn); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestOnce_DoOrPanic(t *testing.T) { + once := New() + executed := false + + fn := func() { + executed = true + } + + // First call should execute + once.DoOrPanic(fn) + if !executed { + t.Error("function should have executed") + } + + // Second call should panic + defer func() { + if r := recover(); r == nil { + t.Error("expected panic on second execution") + } + }() + once.DoOrPanic(fn) +} + +func TestOnce_DoValue(t *testing.T) { + once := New() + expected := "test value" + counter := 0 + + fn := func() interface{} { + counter++ + return expected + } + + // First call should return value + if result := once.DoValue(fn); result != expected { + t.Errorf("expected %v, got %v", expected, result) + } + + // Second call should return cached value + if result := once.DoValue(fn); result != expected { + t.Errorf("expected %v, got %v", expected, result) + } + + if counter != 1 { + t.Errorf("function should have executed only once, got %d executions", counter) + } +} + +func TestOnce_DoValueErr(t *testing.T) { + once := New() + expectedVal := "test value" + expectedErr := errors.New("test error") + counter := 0 + + fn := func() (interface{}, error) { + counter++ + return expectedVal, expectedErr + } + + // First call should return value and error + val, err := once.DoValueErr(fn) + if val != expectedVal || err != expectedErr { + t.Errorf("expected (%v, %v), got (%v, %v)", expectedVal, expectedErr, val, err) + } + + // Second call should return cached value and error + val, err = once.DoValueErr(fn) + if val != expectedVal || err != expectedErr { + t.Errorf("expected (%v, %v), got (%v, %v)", expectedVal, expectedErr, val, err) + } + + if counter != 1 { + t.Errorf("function should have executed only once, got %d executions", counter) + } +} + +func TestOnce_Reset(t *testing.T) { + once := New() + counter := 0 + + fn := func() { + counter++ + } + + once.Do(fn) + if counter != 1 { + t.Errorf("expected counter to be 1, got %d", counter) + } + + once.Reset() + once.Do(fn) + if counter != 2 { + t.Errorf("expected counter to be 2 after reset, got %d", counter) + } +} + +func TestOnce_IsDone(t *testing.T) { + once := New() + + if once.IsDone() { + t.Error("new Once instance should not be done") + } + + once.Do(func() {}) + + if !once.IsDone() { + t.Error("Once instance should be done after execution") + } +} + +func TestOnce_Error(t *testing.T) { + once := New() + expectedErr := errors.New("test error") + + if err := once.Error(); err != nil { + t.Errorf("expected nil error, got %v", err) + } + + once.DoErr(func() error { + return expectedErr + }) + + if err := once.Error(); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestOnce_Value(t *testing.T) { + once := New() + + // Test unexecuted state + val, err := once.Value() + if err != ErrNotExecuted { + t.Errorf("expected ErrNotExecuted, got %v", err) + } + if val != nil { + t.Errorf("expected nil value, got %v", val) + } + + // Test after execution + expected := "test value" + once.DoValue(func() interface{} { + return expected + }) + + val, err = once.Value() + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if val != expected { + t.Errorf("expected value %v, got %v", expected, val) + } +} + +func TestOnce_DoValueErr_Panic_MarkedDone(t *testing.T) { + once := New() + count := 0 + fn := func() (interface{}, error) { + count++ + panic("panic") + } + var r interface{} + func() { + defer func() { r = recover() }() + once.DoValueErr(fn) + }() + if r == nil { + t.Error("expected panic on first call") + } + if !once.IsDone() { + t.Error("expected once to be marked as done after panic") + } + r = nil + func() { + defer func() { r = recover() }() + once.DoValueErr(fn) + }() + if r != nil { + t.Error("expected no panic on subsequent call") + } + if count != 1 { + t.Errorf("expected count to be 1, got %d", count) + } +} diff --git a/gno.land/pkg/integration/testdata/moul_authz.txtar b/gno.land/pkg/integration/testdata/moul_authz.txtar new file mode 100644 index 00000000000..d0da3f535e8 --- /dev/null +++ b/gno.land/pkg/integration/testdata/moul_authz.txtar @@ -0,0 +1,82 @@ +loadpkg gno.land/p/moul/authz +loadpkg gno.land/r/testing/admin $WORK/admin +loadpkg gno.land/r/testing/resource $WORK/resource + +adduserfrom alice 'smooth crawl poverty trumpet glare useful curtain annual pluck lunar example merge ready forum better verb rescue rule mechanic dynamic drift bench release weekend' +stdout 'g1rfznvu6qfa0sc76cplk5wpqexvefqccjunady0' + +gnoland start + +gnokey maketx call -pkgpath gno.land/r/testing/resource -func Edit -args edited -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice + +gnokey maketx call -pkgpath gno.land/r/testing/admin -func ExecuteAction -args 0 -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice + +gnokey maketx call -pkgpath gno.land/r/testing/resource -func Value -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice +stdout 'edited' + + +-- admin/gno.mod -- +module gno.land/r/testing/admin + +-- admin/admin.gno -- +package admin + +import ( + "std" + "errors" + "gno.land/p/moul/authz" +) + +type prop struct { + title string + action authz.PrivilegedAction +} + +var props []*prop + +func HandlePrivilegedAction(title string, action authz.PrivilegedAction) error { + if std.PreviousRealm().PkgPath() != "gno.land/r/testing/resource" { + return errors.New("unauthorized proposer") + } + props = append(props, &prop{title: title, action: action}) + return nil +} + +func ExecuteAction(index int) { + if std.PreviousRealm().Address() != "g1rfznvu6qfa0sc76cplk5wpqexvefqccjunady0" { + panic(errors.New("not alice")) + } + if err := props[index].action(); err != nil { + panic(err) + } +} + +-- resource/gno.mod -- +module gno.land/r/testing/resource + +-- resource/resource.gno -- +package resource + +import ( + "gno.land/p/moul/authz" + "gno.land/r/testing/admin" +) + +var a *authz.Authorizer = authz.NewWithAuthority(authz.NewContractAuthority("gno.land/r/testing/admin", admin.HandlePrivilegedAction)) + +var value = "init" + +func Value() string { + return value +} + +func Edit(newValue string) { + doEdit := func() error { + value = newValue + return nil + } + if err := a.Do("Edit", doEdit); err != nil { + panic(err) + } +} +