From 2951d877a23c4cb37cedf8a396037f6c2bdf12e9 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 16 Jan 2023 19:57:28 +0100 Subject: [PATCH 1/6] Add test contract --- .../full_access_key_fallback/Cargo.toml | 21 +++++++++++++++++++ .../full_access_key_fallback/Makefile | 8 +++++++ .../full_access_key_fallback/rust-toolchain | 3 +++ .../full_access_key_fallback/src/lib.rs | 21 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 near-plugins/tests/contracts/full_access_key_fallback/Cargo.toml create mode 100644 near-plugins/tests/contracts/full_access_key_fallback/Makefile create mode 100644 near-plugins/tests/contracts/full_access_key_fallback/rust-toolchain create mode 100644 near-plugins/tests/contracts/full_access_key_fallback/src/lib.rs diff --git a/near-plugins/tests/contracts/full_access_key_fallback/Cargo.toml b/near-plugins/tests/contracts/full_access_key_fallback/Cargo.toml new file mode 100644 index 0000000..e9e34ea --- /dev/null +++ b/near-plugins/tests/contracts/full_access_key_fallback/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "full_access_key_fallback" +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] diff --git a/near-plugins/tests/contracts/full_access_key_fallback/Makefile b/near-plugins/tests/contracts/full_access_key_fallback/Makefile new file mode 100644 index 0000000..aa2cf20 --- /dev/null +++ b/near-plugins/tests/contracts/full_access_key_fallback/Makefile @@ -0,0 +1,8 @@ +build: + cargo build --target wasm32-unknown-unknown --release + +# Helpful for debugging. Requires `cargo-expand`. +expand: + cargo expand > expanded.rs + +.PHONY: build expand diff --git a/near-plugins/tests/contracts/full_access_key_fallback/rust-toolchain b/near-plugins/tests/contracts/full_access_key_fallback/rust-toolchain new file mode 100644 index 0000000..2f3cf78 --- /dev/null +++ b/near-plugins/tests/contracts/full_access_key_fallback/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.66.1" +components = ["clippy", "rustfmt"] diff --git a/near-plugins/tests/contracts/full_access_key_fallback/src/lib.rs b/near-plugins/tests/contracts/full_access_key_fallback/src/lib.rs new file mode 100644 index 0000000..33a4e83 --- /dev/null +++ b/near-plugins/tests/contracts/full_access_key_fallback/src/lib.rs @@ -0,0 +1,21 @@ +use near_plugins::{FullAccessKeyFallback, Ownable}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::{near_bindgen, AccountId, PanicOnDefault}; + +/// Deriving `FullAccessKeyFallback` requires the contract to be `Ownable.` +#[near_bindgen] +#[derive(Ownable, FullAccessKeyFallback, PanicOnDefault, BorshDeserialize, BorshSerialize)] +pub struct Counter; + +#[near_bindgen] +impl Counter { + /// Optionally set the owner in the constructor. + #[init] + pub fn new(owner: Option) -> Self { + let mut contract = Self; + if owner.is_some() { + contract.owner_set(owner); + } + contract + } +} From 1c762cbc388136bb75c3b0b105ec3f15a98f4259 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 16 Jan 2023 20:02:00 +0100 Subject: [PATCH 2/6] Add integration tests --- .../full_access_key_fallback_contract.rs | 35 ++++ near-plugins/tests/common/mod.rs | 1 + .../tests/full_access_key_fallback.rs | 171 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 near-plugins/tests/common/full_access_key_fallback_contract.rs create mode 100644 near-plugins/tests/full_access_key_fallback.rs diff --git a/near-plugins/tests/common/full_access_key_fallback_contract.rs b/near-plugins/tests/common/full_access_key_fallback_contract.rs new file mode 100644 index 0000000..cd076e8 --- /dev/null +++ b/near-plugins/tests/common/full_access_key_fallback_contract.rs @@ -0,0 +1,35 @@ +use near_sdk::serde_json::json; +use near_sdk::PublicKey; +use workspaces::result::ExecutionFinalResult; +use workspaces::{Account, Contract}; + +/// Wrapper for a contract that uses `#[full_access_key_fallback]`. It allows implementing helpers +/// for calling contract methods. +pub struct FullAccessKeyFallbackContract { + contract: Contract, +} + +impl FullAccessKeyFallbackContract { + pub fn new(contract: Contract) -> Self { + Self { contract } + } + + pub fn contract(&self) -> &Contract { + &self.contract + } + + /// The `Promise` returned by trait method `attach_full_access_key` is resolved in the + /// workspaces transaction. + pub async fn attach_full_access_key( + &self, + caller: &Account, + public_key: PublicKey, + ) -> workspaces::Result { + caller + .call(self.contract.id(), "attach_full_access_key") + .args_json(json!({ "public_key": public_key })) + .max_gas() + .transact() + .await + } +} diff --git a/near-plugins/tests/common/mod.rs b/near-plugins/tests/common/mod.rs index 1a3cb9f..4111f35 100644 --- a/near-plugins/tests/common/mod.rs +++ b/near-plugins/tests/common/mod.rs @@ -1,4 +1,5 @@ pub mod access_controllable_contract; +pub mod full_access_key_fallback_contract; pub mod ownable_contract; pub mod pausable_contract; pub mod repo; diff --git a/near-plugins/tests/full_access_key_fallback.rs b/near-plugins/tests/full_access_key_fallback.rs new file mode 100644 index 0000000..f984e5c --- /dev/null +++ b/near-plugins/tests/full_access_key_fallback.rs @@ -0,0 +1,171 @@ +// Using `pub` to avoid invalid `dead_code` warnings, see +// https://users.rust-lang.org/t/invalid-dead-code-warning-for-submodule-in-integration-test/80259 +pub mod common; + +use anyhow::Ok; +use common::full_access_key_fallback_contract::FullAccessKeyFallbackContract; +use common::utils::{assert_only_owner_permission_failure, assert_success_with_unit_return}; +use near_sdk::serde::Deserialize; +use near_sdk::serde_json::{from_value, json}; +use std::iter; +use std::path::Path; +use workspaces::network::Sandbox; +use workspaces::types::{AccessKeyPermission, PublicKey}; +use workspaces::{Account, AccountId, Contract, Worker}; + +const PROJECT_PATH: &str = "./tests/contracts/full_access_key_fallback"; + +/// Returns a new PublicKey that can be used in tests. +/// +/// It returns a `near_sdk::PublicKey` since that's the type required for +/// `FullAccessKeyFallback::attach_full_access_key`. +fn new_public_key() -> near_sdk::PublicKey { + "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp" + .parse() + .unwrap() +} + +/// Converts a `near_sdk::PublicKey` to a `workspaces::types::PublicKey`. +fn pk_sdk_to_workspaces(public_key: near_sdk::PublicKey) -> PublicKey { + #[derive(Deserialize)] + struct Wrapper { + public_key: PublicKey, + } + + let ser = json!({ "public_key": public_key }); + from_value::(ser).unwrap().public_key +} + +/// Allows spinning up a setup for testing the contract in [`PROJECT_PATH`] and bundles related +/// resources. +struct Setup { + /// Instance of the deployed contract. + contract: Contract, + /// Wrapper around the deployed contract that facilitates interacting with methods provided by + /// the `FullAccessKeyFallback` plugin. + fa_key_fallback_contract: FullAccessKeyFallbackContract, + /// A newly created account without any `Ownable` permissions. + unauth_account: Account, +} + +impl Setup { + /// Deploys and initializes the contract in [`PROJECT_PATH`] and returns a new `Setup`. + /// + /// The `owner` parameter is passed on to the contract's constructor, allowing to optionally set + /// the owner during initialization. + async fn new(worker: Worker, owner: Option) -> anyhow::Result { + // Compile and deploy the contract. + let wasm = + common::repo::compile_project(Path::new(PROJECT_PATH), "full_access_key_fallback") + .await?; + let contract = worker.dev_deploy(&wasm).await?; + let fa_key_fallback_contract = FullAccessKeyFallbackContract::new(contract.clone()); + + // Call the contract's constructor. + contract + .call("new") + .args_json(json!({ + "owner": owner, + })) + .max_gas() + .transact() + .await? + .into_result()?; + + let unauth_account = worker.dev_create_account().await?; + Ok(Self { + contract, + fa_key_fallback_contract, + unauth_account, + }) + } + + /// Asserts the contract's access keys are: + /// + /// - the contracts own key followed by + /// - the keys specified in `keys` + /// + /// Moreover, it asserts that all access keys have `FullAccess` permission. + async fn assert_full_access_keys(&self, keys: &[PublicKey]) { + // Assert the number of keys. + let access_keys = self + .contract + .view_access_keys() + .await + .expect("Should view access keys"); + assert_eq!( + access_keys.len(), + keys.len() + 1, // + 1 for the contract's key + ); + + // Assert the `access_keys` are the contract's key followed by `keys` (all full access). + let contract_key = self.contract.as_account().secret_key().public_key(); + let expected_keys = iter::once(&contract_key).chain(keys.iter()); + for (i, expected_key) in expected_keys.into_iter().enumerate() { + let access_key = &access_keys[i]; + assert_eq!( + &access_key.public_key, expected_key, + "Unexpected PublicKey at index {}", + i + ); + println!("looking at {:?}", expected_key); + assert!( + matches!( + access_key.access_key.permission, + AccessKeyPermission::FullAccess, + ), + "Unexpected permission of access key at index {}: {:?}", + i, + access_key.access_key.permission, + ); + } + } +} + +/// Smoke test of contract setup. +#[tokio::test] +async fn test_setup() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let _ = Setup::new(worker, None).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_non_owner_cannot_attach_full_access_key() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let owner = worker.dev_create_account().await?; + let setup = Setup::new(worker, Some(owner.id().clone())).await?; + + let new_fak = new_public_key(); + let res = setup + .fa_key_fallback_contract + .attach_full_access_key(&setup.unauth_account, new_fak) + .await?; + assert_only_owner_permission_failure(res); + + Ok(()) +} + +#[tokio::test] +async fn test_attach_full_access_key() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let owner = worker.dev_create_account().await?; + let setup = Setup::new(worker, Some(owner.id().clone())).await?; + + // Initially there's just the contract's access key. + setup.assert_full_access_keys(&[]).await; + + // Owner may attach a full access key. + let new_fak = new_public_key(); + let res = setup + .fa_key_fallback_contract + .attach_full_access_key(&owner, new_fak.clone()) + .await?; + assert_success_with_unit_return(res); + setup + .assert_full_access_keys(&[pk_sdk_to_workspaces(new_fak)]) + .await; + + Ok(()) +} From 33b4a2cff630be2ebe88e3301b7caf7361c4d6dc Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 16 Jan 2023 20:10:38 +0100 Subject: [PATCH 3/6] Remove unit tests --- near-plugins/src/full_access_key_fallback.rs | 44 -------------------- 1 file changed, 44 deletions(-) diff --git a/near-plugins/src/full_access_key_fallback.rs b/near-plugins/src/full_access_key_fallback.rs index 479031e..70b3f07 100644 --- a/near-plugins/src/full_access_key_fallback.rs +++ b/near-plugins/src/full_access_key_fallback.rs @@ -42,47 +42,3 @@ impl AsEvent for FullAccessKeyAdded { } } } - -#[cfg(not(target_arch = "wasm32"))] -#[cfg(test)] -mod tests { - // TODO: Make simulation test that verifies key get's added to the account - use crate as near_plugins; - use crate::test_utils::get_context; - use crate::{FullAccessKeyFallback, Ownable}; - use near_sdk::{near_bindgen, testing_env, PublicKey}; - use std::convert::TryInto; - use std::str::FromStr; - - #[near_bindgen] - #[derive(Ownable, FullAccessKeyFallback)] - struct Contract; - - fn key() -> PublicKey { - PublicKey::from_str("ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp").unwrap() - } - - #[test] - #[should_panic(expected = r#"Ownable: Method must be called from owner"#)] - fn not_owner() { - let ctx = get_context(); - testing_env!(ctx); - - let mut contract = Contract; - contract.attach_full_access_key(key()); - } - - #[test] - fn simple() { - let mut ctx = get_context(); - testing_env!(ctx.clone()); - - let mut contract = Contract; - contract.owner_set(Some("carol.test".to_string().try_into().unwrap())); - - ctx.predecessor_account_id = "carol.test".to_string().try_into().unwrap(); - testing_env!(ctx); - - contract.attach_full_access_key(key()); - } -} From 118de52bf402b5d7d5106ffe91bdaee56aa8bff2 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 17 Jan 2023 12:21:48 +0100 Subject: [PATCH 4/6] Make assert_full_access_keys independen of order --- .../tests/full_access_key_fallback.rs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/near-plugins/tests/full_access_key_fallback.rs b/near-plugins/tests/full_access_key_fallback.rs index f984e5c..a77e89d 100644 --- a/near-plugins/tests/full_access_key_fallback.rs +++ b/near-plugins/tests/full_access_key_fallback.rs @@ -82,41 +82,47 @@ impl Setup { /// Asserts the contract's access keys are: /// - /// - the contracts own key followed by + /// - the contracts own key plus /// - the keys specified in `keys` /// + /// with the order of keys being irrelevant. + /// /// Moreover, it asserts that all access keys have `FullAccess` permission. + /// + /// Input parameter `keys` is expected to not contain duplicates. async fn assert_full_access_keys(&self, keys: &[PublicKey]) { // Assert the number of keys. - let access_keys = self + let access_key_infos = self .contract .view_access_keys() .await .expect("Should view access keys"); assert_eq!( - access_keys.len(), + access_key_infos.len(), keys.len() + 1, // + 1 for the contract's key ); - // Assert the `access_keys` are the contract's key followed by `keys` (all full access). + // Assert the attached access keys are the ones we expected and all have `FullAccess`. + // + // Since `workspaces::types::PublicKey` doesn't implement `Hash`, it cannot be stored in + // `std::collections::HashSet`. Hence the search in `access_key_infos` with + // `find()`. let contract_key = self.contract.as_account().secret_key().public_key(); let expected_keys = iter::once(&contract_key).chain(keys.iter()); - for (i, expected_key) in expected_keys.into_iter().enumerate() { - let access_key = &access_keys[i]; - assert_eq!( - &access_key.public_key, expected_key, - "Unexpected PublicKey at index {}", - i - ); - println!("looking at {:?}", expected_key); + for expected_key in expected_keys.into_iter() { + let attached_key = access_key_infos + .iter() + .find(|info| &info.public_key == expected_key) + .expect(format!("PublicKey {:?} is not attached", expected_key).as_str()); + assert!( matches!( - access_key.access_key.permission, + attached_key.access_key.permission, AccessKeyPermission::FullAccess, ), - "Unexpected permission of access key at index {}: {:?}", - i, - access_key.access_key.permission, + "Unexpected permission of access key {:?}: {:?}", + attached_key, + attached_key.access_key.permission, ); } } From f513dea5a97c308e423d1e9f1e14b390e8b49c3e Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 17 Jan 2023 12:29:41 +0100 Subject: [PATCH 5/6] Comment on PublicKey conversion --- near-plugins/tests/full_access_key_fallback.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/near-plugins/tests/full_access_key_fallback.rs b/near-plugins/tests/full_access_key_fallback.rs index a77e89d..825ea1a 100644 --- a/near-plugins/tests/full_access_key_fallback.rs +++ b/near-plugins/tests/full_access_key_fallback.rs @@ -27,6 +27,8 @@ fn new_public_key() -> near_sdk::PublicKey { /// Converts a `near_sdk::PublicKey` to a `workspaces::types::PublicKey`. fn pk_sdk_to_workspaces(public_key: near_sdk::PublicKey) -> PublicKey { + // Going via json since there seems to be no direct conversion, see this issue: + // https://github.com/near/workspaces-rs/issues/262 #[derive(Deserialize)] struct Wrapper { public_key: PublicKey, From acb76d6dd57364253e2b72c0caa6b97aeeb5f9c2 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 17 Jan 2023 13:02:41 +0100 Subject: [PATCH 6/6] Fix clippy errors --- near-plugins/tests/full_access_key_fallback.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/near-plugins/tests/full_access_key_fallback.rs b/near-plugins/tests/full_access_key_fallback.rs index 825ea1a..71bc8d2 100644 --- a/near-plugins/tests/full_access_key_fallback.rs +++ b/near-plugins/tests/full_access_key_fallback.rs @@ -111,11 +111,11 @@ impl Setup { // `find()`. let contract_key = self.contract.as_account().secret_key().public_key(); let expected_keys = iter::once(&contract_key).chain(keys.iter()); - for expected_key in expected_keys.into_iter() { + for expected_key in expected_keys { let attached_key = access_key_infos .iter() .find(|info| &info.public_key == expected_key) - .expect(format!("PublicKey {:?} is not attached", expected_key).as_str()); + .unwrap_or_else(|| panic!("PublicKey {:?} is not attached", expected_key)); assert!( matches!(