Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add integration tests for Pausable plugins #51

Merged
merged 7 commits into from
Dec 21, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
450 changes: 0 additions & 450 deletions near-plugins/src/pausable.rs

Large diffs are not rendered by default.

327 changes: 134 additions & 193 deletions near-plugins/tests/access_controllable.rs

Large diffs are not rendered by default.

126 changes: 47 additions & 79 deletions near-plugins/tests/common/access_controllable_contract.rs
Original file line number Diff line number Diff line change
@@ -2,21 +2,6 @@ use near_sdk::serde_json::json;
use workspaces::result::ExecutionFinalResult;
use workspaces::{Account, AccountId, Contract};

/// Specifies who calls a method on the contract.
#[derive(Clone)]
pub enum Caller {
/// The contract itself.
Contract,
/// The provided account.
Account(Account),
}

impl From<Account> for Caller {
fn from(account: Account) -> Self {
Self::Account(account)
}
}

/// Wrapper for a contract that is `#[access_controllable]`. It allows
/// implementing helpers for calling contract methods.
pub struct AccessControllableContract {
@@ -32,20 +17,12 @@ impl AccessControllableContract {
&self.contract
}

fn account(&self, caller: Caller) -> Account {
match caller {
Caller::Contract => self.contract.as_account().clone(),
Caller::Account(account) => account,
}
}

pub async fn acl_is_super_admin(
&self,
caller: Caller,
caller: &Account,
account_id: &AccountId,
) -> anyhow::Result<bool> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_is_super_admin")
.args_json(json!({
"account_id": account_id,
@@ -55,20 +32,22 @@ impl AccessControllableContract {
Ok(res.json::<bool>()?)
}

pub async fn assert_acl_is_super_admin(&self, expected: bool, account_id: &AccountId) {
let is_super_admin = self
.acl_is_super_admin(Caller::Contract, account_id)
.await
.unwrap();
pub async fn assert_acl_is_super_admin(
&self,
expected: bool,
caller: &Account,
account_id: &AccountId,
) {
let is_super_admin = self.acl_is_super_admin(caller, account_id).await.unwrap();
assert_eq!(is_super_admin, expected);
}

pub async fn acl_init_super_admin(
&self,
caller: Caller,
caller: &Account,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_init_super_admin")
.args_json(json!({
"account_id": account_id,
@@ -80,10 +59,10 @@ impl AccessControllableContract {

pub async fn acl_add_super_admin_unchecked(
&self,
caller: Caller,
caller: &Account,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_add_super_admin_unchecked")
.args_json(json!({
"account_id": account_id,
@@ -95,10 +74,10 @@ impl AccessControllableContract {

pub async fn acl_revoke_super_admin_unchecked(
&self,
caller: Caller,
caller: &Account,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_revoke_super_admin_unchecked")
.args_json(json!({
"account_id": account_id,
@@ -110,12 +89,11 @@ impl AccessControllableContract {

pub async fn acl_is_admin(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<bool> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_is_admin")
.args_json(json!({
"role": role,
@@ -128,20 +106,19 @@ impl AccessControllableContract {

pub async fn assert_acl_is_admin(&self, expected: bool, role: &str, account_id: &AccountId) {
let is_admin = self
.acl_is_admin(Caller::Contract, role, account_id)
.acl_is_admin(self.contract.as_account(), role, account_id)
.await
.unwrap();
assert_eq!(is_admin, expected);
}

pub async fn acl_add_admin(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<Option<bool>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_add_admin")
.args_json(json!({
"role": role,
@@ -157,11 +134,11 @@ impl AccessControllableContract {

pub async fn acl_add_admin_unchecked(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_add_admin_unchecked")
.args_json(json!({
"role": role,
@@ -174,12 +151,11 @@ impl AccessControllableContract {

pub async fn acl_revoke_admin(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<Option<bool>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_revoke_admin")
.args_json(json!({
"role": role,
@@ -193,9 +169,8 @@ impl AccessControllableContract {
Ok(res)
}

pub async fn acl_renounce_admin(&self, caller: Caller, role: &str) -> anyhow::Result<bool> {
let res = self
.account(caller)
pub async fn acl_renounce_admin(&self, caller: &Account, role: &str) -> anyhow::Result<bool> {
let res = caller
.call(self.contract.id(), "acl_renounce_admin")
.args_json(json!({
"role": role,
@@ -210,11 +185,11 @@ impl AccessControllableContract {

pub async fn acl_revoke_admin_unchecked(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_revoke_admin_unchecked")
.args_json(json!({
"role": role,
@@ -227,12 +202,11 @@ impl AccessControllableContract {

pub async fn acl_has_role(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<bool> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_has_role")
.args_json(json!({
"role": role,
@@ -245,20 +219,19 @@ impl AccessControllableContract {

pub async fn assert_acl_has_role(&self, expected: bool, role: &str, account_id: &AccountId) {
let has_role = self
.acl_has_role(Caller::Contract, role, account_id)
.acl_has_role(self.contract.as_account(), role, account_id)
.await
.unwrap();
assert_eq!(has_role, expected);
}

pub async fn acl_grant_role(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<Option<bool>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_grant_role")
.args_json(json!({
"role": role,
@@ -274,11 +247,11 @@ impl AccessControllableContract {

pub async fn acl_grant_role_unchecked(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_grant_role_unchecked")
.args_json(json!({
"role": role,
@@ -291,12 +264,11 @@ impl AccessControllableContract {

pub async fn acl_revoke_role(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> anyhow::Result<Option<bool>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_revoke_role")
.args_json(json!({
"role": role,
@@ -310,9 +282,8 @@ impl AccessControllableContract {
Ok(res)
}

pub async fn acl_renounce_role(&self, caller: Caller, role: &str) -> anyhow::Result<bool> {
let res = self
.account(caller)
pub async fn acl_renounce_role(&self, caller: &Account, role: &str) -> anyhow::Result<bool> {
let res = caller
.call(self.contract.id(), "acl_renounce_role")
.args_json(json!({
"role": role,
@@ -327,11 +298,11 @@ impl AccessControllableContract {

pub async fn acl_revoke_role_unchecked(
&self,
caller: Caller,
caller: &Account,
role: &str,
account_id: &AccountId,
) -> workspaces::Result<ExecutionFinalResult> {
self.account(caller)
caller
.call(self.contract.id(), "acl_revoke_role_unchecked")
.args_json(json!({
"role": role,
@@ -344,12 +315,11 @@ impl AccessControllableContract {

pub async fn acl_get_super_admins(
&self,
caller: Caller,
caller: &Account,
skip: u64,
limit: u64,
) -> anyhow::Result<Vec<AccountId>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_get_super_admins")
.args_json(json!({
"skip": skip,
@@ -365,13 +335,12 @@ impl AccessControllableContract {

pub async fn acl_get_admins(
&self,
caller: Caller,
caller: &Account,
role: &str,
skip: u64,
limit: u64,
) -> anyhow::Result<Vec<AccountId>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_get_admins")
.args_json(json!({
"role": role,
@@ -388,13 +357,12 @@ impl AccessControllableContract {

pub async fn acl_get_grantees(
&self,
caller: Caller,
caller: &Account,
role: &str,
skip: u64,
limit: u64,
) -> anyhow::Result<Vec<AccountId>> {
let res = self
.account(caller)
let res = caller
.call(self.contract.id(), "acl_get_grantees")
.args_json(json!({
"role": role,
1 change: 1 addition & 0 deletions near-plugins/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod access_controllable_contract;
pub mod pausable_contract;
pub mod repo;
pub mod utils;
65 changes: 65 additions & 0 deletions near-plugins/tests/common/pausable_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use near_sdk::serde_json::json;
use std::collections::HashSet;
use workspaces::result::ExecutionFinalResult;
use workspaces::{Account, Contract};

/// Wrapper for a contract that is `#[pausable]`. It allows implementing helpers
/// for calling contract methods.
pub struct PausableContract {
contract: Contract,
}

impl PausableContract {
pub fn new(contract: Contract) -> Self {
Self { contract }
}

pub fn contract(&self) -> &Contract {
&self.contract
}

pub async fn pa_is_paused(&self, caller: &Account, key: &str) -> anyhow::Result<bool> {
let res = caller
.call(self.contract.id(), "pa_is_paused")
.args_json(json!({
"key": key,
}))
.view()
.await?;
Ok(res.json::<bool>()?)
}

pub async fn pa_pause_feature(
&self,
caller: &Account,
key: &str,
) -> workspaces::Result<ExecutionFinalResult> {
caller
.call(self.contract.id(), "pa_pause_feature")
.args_json(json!({ "key": key }))
.max_gas()
.transact()
.await
}

pub async fn pa_unpause_feature(
&self,
caller: &Account,
key: &str,
) -> workspaces::Result<ExecutionFinalResult> {
caller
.call(self.contract.id(), "pa_unpause_feature")
.args_json(json!({ "key": key }))
.max_gas()
.transact()
.await
}

pub async fn pa_all_paused(&self, caller: &Account) -> anyhow::Result<Option<HashSet<String>>> {
let res = caller
.call(self.contract.id(), "pa_all_paused")
.view()
.await?;
Ok(res.json::<Option<HashSet<String>>>()?)
}
}
6 changes: 3 additions & 3 deletions near-plugins/tests/common/repo.rs
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ async fn add_wasm_target(project_path: &Path, toolchain: &str) -> anyhow::Result
Ok(())
}

pub async fn compile_project(project_path: &Path) -> anyhow::Result<Vec<u8>> {
pub async fn compile_project(project_path: &Path, package_name: &str) -> anyhow::Result<Vec<u8>> {
let toolchain = read_toolchain(project_path).await?;
add_wasm_target(project_path, &toolchain).await?;
let output = tokio::process::Command::new("cargo")
@@ -47,7 +47,7 @@ pub async fn compile_project(project_path: &Path) -> anyhow::Result<Vec<u8>> {
"--release",
"--no-default-features",
"-p",
"access_controllable",
package_name,
])
.output()
.await?;
@@ -58,7 +58,7 @@ pub async fn compile_project(project_path: &Path) -> anyhow::Result<Vec<u8>> {
"target",
"wasm32-unknown-unknown",
"release",
"access_controllable.wasm",
format!("{}.wasm", package_name).as_str(),
]
.iter()
.collect::<PathBuf>(),
38 changes: 38 additions & 0 deletions near-plugins/tests/common/utils.rs
Original file line number Diff line number Diff line change
@@ -3,6 +3,15 @@ use std::cmp::PartialEq;
use std::fmt::Debug;
use workspaces::result::ExecutionFinalResult;

/// Asserts execution was successful and returned `()`.
pub fn assert_success_with_unit_return(res: ExecutionFinalResult) {
assert!(res.is_success(), "Transaction should have succeeded");
assert!(
res.raw_bytes().unwrap().is_empty(),
"Unexpected return value"
);
}

/// Asserts execution was successful and returned the `expected` value.
pub fn assert_success_with<T>(res: ExecutionFinalResult, expected: T)
where
@@ -60,3 +69,32 @@ pub fn assert_insufficient_acl_permissions(
err,
);
}

pub fn assert_method_is_paused(res: ExecutionFinalResult) {
let err = res
.into_result()
.err()
.expect("Transaction should have failed");
let err = format!("{}", err);
let must_contain = "Pausable: Method is paused";
assert!(
err.contains(&must_contain),
"Expected method to be paused, instead it failed with: {}",
err
);
}

/// Asserts the execution of `res` failed and the error contains `must_contain`.
pub fn assert_failure_with(res: ExecutionFinalResult, must_contain: &str) {
let err = res
.into_result()
.err()
.expect("Transaction should have failed");
let err = format!("{}", err);
assert!(
err.contains(must_contain),
"The expected message\n'{}'\nis not contained in error\n'{}'",
must_contain,
err,
);
}
2 changes: 2 additions & 0 deletions near-plugins/tests/contracts/access_controllable/Makefile
Original file line number Diff line number Diff line change
@@ -4,3 +4,5 @@ build:
# Helpful for debugging. Requires `cargo-expand`.
expand:
cargo expand > expanded.rs

.PHONY: build expand
21 changes: 21 additions & 0 deletions near-plugins/tests/contracts/pausable/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "pausable"
version = "0.0.0"
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-plugins = { path = "../../../../near-plugins" }
near-sdk = "4.1.0"

[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true

[workspace]
8 changes: 8 additions & 0 deletions near-plugins/tests/contracts/pausable/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build:
birchmd marked this conversation as resolved.
Show resolved Hide resolved
cargo build --target wasm32-unknown-unknown --release

# Helpful for debugging. Requires `cargo-expand`.
expand:
cargo expand > expanded.rs

.PHONY: build expand
3 changes: 3 additions & 0 deletions near-plugins/tests/contracts/pausable/rust-toolchain
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.64.0"
birchmd marked this conversation as resolved.
Show resolved Hide resolved
components = ["clippy", "rustfmt"]
115 changes: 115 additions & 0 deletions near-plugins/tests/contracts/pausable/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use near_plugins::{
access_control, if_paused, pause, AccessControlRole, AccessControllable, Pausable,
};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault};

#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
/// May pause and unpause features.
PauseManager,
/// May call `increase_4` even when it is paused.
Unrestricted4Increaser,
/// May call `decrease_4` even when `increase_4` is not paused.
Unrestricted4Decreaser,
/// May always call both `increase_4` and `decrease_4`.
Unrestricted4Modifier,
}

#[access_control(role_type(Role))]
#[near_bindgen]
#[derive(Pausable, PanicOnDefault, BorshDeserialize, BorshSerialize)]
#[pausable(manager_roles(Role::PauseManager))]
pub struct Counter {
counter: u64,
}

#[near_bindgen]
impl Counter {
/// Permissons for `AccessControllable` can be initialized in the constructor. Here we are:
///
/// * Making the contract itself super admin.
/// * Granting `Role::PauseManager` to the account id `pause_manager`.
///
/// For a general overview of access control, please refer to the `AccessControllable` plugin.
#[init]
pub fn new(pause_manager: AccountId) -> Self {
let mut contract = Self {
counter: 0,
__acl: Default::default(),
};

// Make the contract itself super admin. This allows us to grant any role in the
// constructor.
near_sdk::require!(
contract.acl_init_super_admin(env::predecessor_account_id()),
"Failed to initialize super admin",
);

// Grant `Role::PauseManager` to the provided account.
let result = contract.acl_grant_role(Role::PauseManager.into(), pause_manager);
near_sdk::require!(Some(true) == result, "Failed to grant role");

contract
}

pub fn get_counter(&self) -> u64 {
self.counter
}

/// Function can be paused using feature name "increase_1" or "ALL" like:
/// `contract.pa_pause_feature("increase_1")` or `contract.pa_pause_feature("ALL")`
///
/// If the function is paused, all calls to it will fail. Even calls initiated by accounts which
/// are access control super admin or role grantee.
#[pause]
pub fn increase_1(&mut self) {
self.counter += 1;
}

/// Similar to `#[pause]` but use an explicit name for the feature. In this case the feature to
/// be paused is named "Increase by two". Note that trying to pause it using "increase_2" will
/// not have any effect.
///
/// This can be used to pause a subset of the methods at once without requiring to use "ALL".
#[pause(name = "Increase by two")]
pub fn increase_2(&mut self) {
self.counter += 2;
}

/// Similar to `#[pause]` but roles passed as argument may still successfully call this method.
#[pause(except(roles(Role::Unrestricted4Increaser, Role::Unrestricted4Modifier)))]
pub fn increase_4(&mut self) {
self.counter += 4;
}

/// This method can only be called when "increase_1" is paused. Use this macro to create escape
/// hatches when some features are paused. Note that if "ALL" is specified the "increase_1" is
/// considered to be paused.
#[if_paused(name = "increase_1")]
pub fn decrease_1(&mut self) {
self.counter -= 1;
}

/// Similar to `#[if_paused]` but roles passed as argument may successfully call the method even
/// when the feature is _not_ paused.
#[if_paused(name = "increase_4", except(roles(Role::Unrestricted4Decreaser)))]
pub fn decrease_4(&mut self) {
self.counter -= 4;
}

/// Custom use of pause features. Only allow increasing the counter using `careful_increase` if
/// it is below 3.
pub fn careful_increase(&mut self) {
if self.counter >= 3 {
assert!(
!self.pa_is_paused("increase_big".to_string()),
"Method paused for large values of counter"
);
}

self.counter += 1;
}
}
598 changes: 598 additions & 0 deletions near-plugins/tests/pausable.rs

Large diffs are not rendered by default.