Skip to content

Commit 12ee017

Browse files
authored
test: add integration tests for FullAccessKeyFallback (#66)
* Add test contract * Add integration tests * Remove unit tests * Make assert_full_access_keys independen of order * Comment on PublicKey conversion * Fix clippy errors
1 parent 22e97c9 commit 12ee017

File tree

8 files changed

+268
-44
lines changed

8 files changed

+268
-44
lines changed

near-plugins/src/full_access_key_fallback.rs

-44
Original file line numberDiff line numberDiff line change
@@ -42,47 +42,3 @@ impl AsEvent<FullAccessKeyAdded> for FullAccessKeyAdded {
4242
}
4343
}
4444
}
45-
46-
#[cfg(not(target_arch = "wasm32"))]
47-
#[cfg(test)]
48-
mod tests {
49-
// TODO: Make simulation test that verifies key get's added to the account
50-
use crate as near_plugins;
51-
use crate::test_utils::get_context;
52-
use crate::{FullAccessKeyFallback, Ownable};
53-
use near_sdk::{near_bindgen, testing_env, PublicKey};
54-
use std::convert::TryInto;
55-
use std::str::FromStr;
56-
57-
#[near_bindgen]
58-
#[derive(Ownable, FullAccessKeyFallback)]
59-
struct Contract;
60-
61-
fn key() -> PublicKey {
62-
PublicKey::from_str("ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp").unwrap()
63-
}
64-
65-
#[test]
66-
#[should_panic(expected = r#"Ownable: Method must be called from owner"#)]
67-
fn not_owner() {
68-
let ctx = get_context();
69-
testing_env!(ctx);
70-
71-
let mut contract = Contract;
72-
contract.attach_full_access_key(key());
73-
}
74-
75-
#[test]
76-
fn simple() {
77-
let mut ctx = get_context();
78-
testing_env!(ctx.clone());
79-
80-
let mut contract = Contract;
81-
contract.owner_set(Some("carol.test".to_string().try_into().unwrap()));
82-
83-
ctx.predecessor_account_id = "carol.test".to_string().try_into().unwrap();
84-
testing_env!(ctx);
85-
86-
contract.attach_full_access_key(key());
87-
}
88-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use near_sdk::serde_json::json;
2+
use near_sdk::PublicKey;
3+
use workspaces::result::ExecutionFinalResult;
4+
use workspaces::{Account, Contract};
5+
6+
/// Wrapper for a contract that uses `#[full_access_key_fallback]`. It allows implementing helpers
7+
/// for calling contract methods.
8+
pub struct FullAccessKeyFallbackContract {
9+
contract: Contract,
10+
}
11+
12+
impl FullAccessKeyFallbackContract {
13+
pub fn new(contract: Contract) -> Self {
14+
Self { contract }
15+
}
16+
17+
pub fn contract(&self) -> &Contract {
18+
&self.contract
19+
}
20+
21+
/// The `Promise` returned by trait method `attach_full_access_key` is resolved in the
22+
/// workspaces transaction.
23+
pub async fn attach_full_access_key(
24+
&self,
25+
caller: &Account,
26+
public_key: PublicKey,
27+
) -> workspaces::Result<ExecutionFinalResult> {
28+
caller
29+
.call(self.contract.id(), "attach_full_access_key")
30+
.args_json(json!({ "public_key": public_key }))
31+
.max_gas()
32+
.transact()
33+
.await
34+
}
35+
}

near-plugins/tests/common/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod access_controllable_contract;
2+
pub mod full_access_key_fallback_contract;
23
pub mod ownable_contract;
34
pub mod pausable_contract;
45
pub mod repo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "full_access_key_fallback"
3+
version = "0.0.0"
4+
edition = "2018"
5+
6+
[lib]
7+
crate-type = ["cdylib", "rlib"]
8+
9+
[dependencies]
10+
near-plugins = { path = "../../../../near-plugins" }
11+
near-sdk = "4.1.0"
12+
13+
[profile.release]
14+
codegen-units = 1
15+
opt-level = "z"
16+
lto = true
17+
debug = false
18+
panic = "abort"
19+
overflow-checks = true
20+
21+
[workspace]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
build:
2+
cargo build --target wasm32-unknown-unknown --release
3+
4+
# Helpful for debugging. Requires `cargo-expand`.
5+
expand:
6+
cargo expand > expanded.rs
7+
8+
.PHONY: build expand
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[toolchain]
2+
channel = "1.66.1"
3+
components = ["clippy", "rustfmt"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use near_plugins::{FullAccessKeyFallback, Ownable};
2+
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
3+
use near_sdk::{near_bindgen, AccountId, PanicOnDefault};
4+
5+
/// Deriving `FullAccessKeyFallback` requires the contract to be `Ownable.`
6+
#[near_bindgen]
7+
#[derive(Ownable, FullAccessKeyFallback, PanicOnDefault, BorshDeserialize, BorshSerialize)]
8+
pub struct Counter;
9+
10+
#[near_bindgen]
11+
impl Counter {
12+
/// Optionally set the owner in the constructor.
13+
#[init]
14+
pub fn new(owner: Option<AccountId>) -> Self {
15+
let mut contract = Self;
16+
if owner.is_some() {
17+
contract.owner_set(owner);
18+
}
19+
contract
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Using `pub` to avoid invalid `dead_code` warnings, see
2+
// https://users.rust-lang.org/t/invalid-dead-code-warning-for-submodule-in-integration-test/80259
3+
pub mod common;
4+
5+
use anyhow::Ok;
6+
use common::full_access_key_fallback_contract::FullAccessKeyFallbackContract;
7+
use common::utils::{assert_only_owner_permission_failure, assert_success_with_unit_return};
8+
use near_sdk::serde::Deserialize;
9+
use near_sdk::serde_json::{from_value, json};
10+
use std::iter;
11+
use std::path::Path;
12+
use workspaces::network::Sandbox;
13+
use workspaces::types::{AccessKeyPermission, PublicKey};
14+
use workspaces::{Account, AccountId, Contract, Worker};
15+
16+
const PROJECT_PATH: &str = "./tests/contracts/full_access_key_fallback";
17+
18+
/// Returns a new PublicKey that can be used in tests.
19+
///
20+
/// It returns a `near_sdk::PublicKey` since that's the type required for
21+
/// `FullAccessKeyFallback::attach_full_access_key`.
22+
fn new_public_key() -> near_sdk::PublicKey {
23+
"ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
24+
.parse()
25+
.unwrap()
26+
}
27+
28+
/// Converts a `near_sdk::PublicKey` to a `workspaces::types::PublicKey`.
29+
fn pk_sdk_to_workspaces(public_key: near_sdk::PublicKey) -> PublicKey {
30+
// Going via json since there seems to be no direct conversion, see this issue:
31+
// https://github.com/near/workspaces-rs/issues/262
32+
#[derive(Deserialize)]
33+
struct Wrapper {
34+
public_key: PublicKey,
35+
}
36+
37+
let ser = json!({ "public_key": public_key });
38+
from_value::<Wrapper>(ser).unwrap().public_key
39+
}
40+
41+
/// Allows spinning up a setup for testing the contract in [`PROJECT_PATH`] and bundles related
42+
/// resources.
43+
struct Setup {
44+
/// Instance of the deployed contract.
45+
contract: Contract,
46+
/// Wrapper around the deployed contract that facilitates interacting with methods provided by
47+
/// the `FullAccessKeyFallback` plugin.
48+
fa_key_fallback_contract: FullAccessKeyFallbackContract,
49+
/// A newly created account without any `Ownable` permissions.
50+
unauth_account: Account,
51+
}
52+
53+
impl Setup {
54+
/// Deploys and initializes the contract in [`PROJECT_PATH`] and returns a new `Setup`.
55+
///
56+
/// The `owner` parameter is passed on to the contract's constructor, allowing to optionally set
57+
/// the owner during initialization.
58+
async fn new(worker: Worker<Sandbox>, owner: Option<AccountId>) -> anyhow::Result<Self> {
59+
// Compile and deploy the contract.
60+
let wasm =
61+
common::repo::compile_project(Path::new(PROJECT_PATH), "full_access_key_fallback")
62+
.await?;
63+
let contract = worker.dev_deploy(&wasm).await?;
64+
let fa_key_fallback_contract = FullAccessKeyFallbackContract::new(contract.clone());
65+
66+
// Call the contract's constructor.
67+
contract
68+
.call("new")
69+
.args_json(json!({
70+
"owner": owner,
71+
}))
72+
.max_gas()
73+
.transact()
74+
.await?
75+
.into_result()?;
76+
77+
let unauth_account = worker.dev_create_account().await?;
78+
Ok(Self {
79+
contract,
80+
fa_key_fallback_contract,
81+
unauth_account,
82+
})
83+
}
84+
85+
/// Asserts the contract's access keys are:
86+
///
87+
/// - the contracts own key plus
88+
/// - the keys specified in `keys`
89+
///
90+
/// with the order of keys being irrelevant.
91+
///
92+
/// Moreover, it asserts that all access keys have `FullAccess` permission.
93+
///
94+
/// Input parameter `keys` is expected to not contain duplicates.
95+
async fn assert_full_access_keys(&self, keys: &[PublicKey]) {
96+
// Assert the number of keys.
97+
let access_key_infos = self
98+
.contract
99+
.view_access_keys()
100+
.await
101+
.expect("Should view access keys");
102+
assert_eq!(
103+
access_key_infos.len(),
104+
keys.len() + 1, // + 1 for the contract's key
105+
);
106+
107+
// Assert the attached access keys are the ones we expected and all have `FullAccess`.
108+
//
109+
// Since `workspaces::types::PublicKey` doesn't implement `Hash`, it cannot be stored in
110+
// `std::collections::HashSet`. Hence the search in `access_key_infos` with
111+
// `find()`.
112+
let contract_key = self.contract.as_account().secret_key().public_key();
113+
let expected_keys = iter::once(&contract_key).chain(keys.iter());
114+
for expected_key in expected_keys {
115+
let attached_key = access_key_infos
116+
.iter()
117+
.find(|info| &info.public_key == expected_key)
118+
.unwrap_or_else(|| panic!("PublicKey {:?} is not attached", expected_key));
119+
120+
assert!(
121+
matches!(
122+
attached_key.access_key.permission,
123+
AccessKeyPermission::FullAccess,
124+
),
125+
"Unexpected permission of access key {:?}: {:?}",
126+
attached_key,
127+
attached_key.access_key.permission,
128+
);
129+
}
130+
}
131+
}
132+
133+
/// Smoke test of contract setup.
134+
#[tokio::test]
135+
async fn test_setup() -> anyhow::Result<()> {
136+
let worker = workspaces::sandbox().await?;
137+
let _ = Setup::new(worker, None).await?;
138+
139+
Ok(())
140+
}
141+
142+
#[tokio::test]
143+
async fn test_non_owner_cannot_attach_full_access_key() -> anyhow::Result<()> {
144+
let worker = workspaces::sandbox().await?;
145+
let owner = worker.dev_create_account().await?;
146+
let setup = Setup::new(worker, Some(owner.id().clone())).await?;
147+
148+
let new_fak = new_public_key();
149+
let res = setup
150+
.fa_key_fallback_contract
151+
.attach_full_access_key(&setup.unauth_account, new_fak)
152+
.await?;
153+
assert_only_owner_permission_failure(res);
154+
155+
Ok(())
156+
}
157+
158+
#[tokio::test]
159+
async fn test_attach_full_access_key() -> anyhow::Result<()> {
160+
let worker = workspaces::sandbox().await?;
161+
let owner = worker.dev_create_account().await?;
162+
let setup = Setup::new(worker, Some(owner.id().clone())).await?;
163+
164+
// Initially there's just the contract's access key.
165+
setup.assert_full_access_keys(&[]).await;
166+
167+
// Owner may attach a full access key.
168+
let new_fak = new_public_key();
169+
let res = setup
170+
.fa_key_fallback_contract
171+
.attach_full_access_key(&owner, new_fak.clone())
172+
.await?;
173+
assert_success_with_unit_return(res);
174+
setup
175+
.assert_full_access_keys(&[pk_sdk_to_workspaces(new_fak)])
176+
.await;
177+
178+
Ok(())
179+
}

0 commit comments

Comments
 (0)