Skip to content

Commit

Permalink
Merge pull request #2 from bitfinity-network/EPROD-969_inspect_message
Browse files Browse the repository at this point in the history
[EPROD-969] Inspect message check and docs
ufoscout authored Aug 26, 2024
2 parents 6024483 + 3cc9063 commit f030afd
Showing 13 changed files with 504 additions and 74 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# canister-upgrader

## Introduction

The upgrader canister allows the creation of polls to approve upgrade of other canisters.
This is achieved by allowing registered voters to approve or reject upgrades identified by unique hashes.

## Poll types

Thee different types of polls can be created:
1. `ProjectHash`: a poll to approve a specific project hash
1. `AddPermission`: a poll to grant permissions to a Principal
1. `RemovePermission`: a poll to remove permissions from a Principal

For each new poll, the creator has to provide the following informations:
- `description`: The description of the poll,
- `poll_type`: The type of poll as discussed above,
- `start_timestamp_secs`: The timestamp in seconds of when the poll opens
- `end_timestamp_secs`: The timestamp in seconds of when the poll closes

## User Permissions

The access to the canister features is restricted by a set of permissions that allow selected Pricipals to operate on the canister.
The available permissions are:

- `Admin`: this permission grants admin rights to the principal. An admin can directy grant or remove permissions to other principals
- `CreateProject`: Allows calling the endpoints to create a project (e.g. evm, bridge, etc.)
- `CreatePoll`: Allows calling the endpoints to create a poll
- `VotePoll`: Allows calling the endpoints to vote in a poll

28 changes: 28 additions & 0 deletions src/did/src/lib.rs
Original file line number Diff line number Diff line change
@@ -87,6 +87,21 @@ impl Storable for ProjectData {
const BOUND: ic_stable_structures::Bound = ic_stable_structures::Bound::Unbounded;
}

/// Data required to create a poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
)]
pub struct PollCreateData {
/// The description of the poll.
pub description: String,
/// The type of poll.
pub poll_type: PollType,
/// The timestamp when the poll opens.
pub start_timestamp_secs: u64,
/// The timestamp when the poll closes.
pub end_timestamp_secs: u64,
}

/// Describes the type of poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
@@ -106,6 +121,19 @@ pub struct Poll {
pub end_timestamp_secs: u64,
}

impl From<PollCreateData> for Poll {
fn from(value: PollCreateData) -> Self {
Self {
description: value.description,
poll_type: value.poll_type,
no_voters: Vec::new(),
yes_voters: Vec::new(),
start_timestamp_secs: value.start_timestamp_secs,
end_timestamp_secs: value.end_timestamp_secs,
}
}
}

/// Describes the type of poll.
#[derive(
Debug, Clone, CandidType, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize,
1 change: 1 addition & 0 deletions src/upgrader_canister/Cargo.toml
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ ic-log = { workspace = true }
ic-storage = { workspace = true }
ic-stable-structures = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
upgrader_canister_did = { workspace = true }

[dev-dependencies]
72 changes: 58 additions & 14 deletions src/upgrader_canister/src/canister.rs
Original file line number Diff line number Diff line change
@@ -3,12 +3,15 @@ use std::collections::BTreeMap;
use candid::Principal;
use ic_canister::{init, post_upgrade, query, update, Canister, MethodType, PreUpdate};
use ic_exports::ic_kit::ic;
use ic_stable_structures::stable_structures::Memory;
use upgrader_canister_did::error::Result;
use upgrader_canister_did::{
BuildData, Permission, PermissionList, Poll, ProjectData, UpgraderCanisterInitData,
BuildData, Permission, PermissionList, Poll, PollCreateData, PollType, ProjectData,
UpgraderCanisterInitData, UpgraderError,
};

use crate::build_data::canister_build_data;
use crate::state::permission::Permissions;
use crate::state::UpgraderCanisterState;

thread_local! {
@@ -87,6 +90,22 @@ impl UpgraderCanister {
})
}

/// Disable/Enable the inspect message
#[update]
pub fn admin_disable_inspect_message(&mut self, value: bool) -> Result<()> {
STATE.with(|state| {
state.permissions.borrow().check_admin(&ic::caller())?;
state.settings.borrow_mut().disable_inspect_message(value);
Ok(())
})
}

/// Returns whether the inspect message is disabled.
#[query]
pub fn is_inspect_message_disabled(&self) -> bool {
STATE.with(|state| state.settings.borrow().is_inspect_message_disabled())
}

/// Returns the permissions of the caller
#[query]
pub fn caller_permissions_get(&self) -> Result<PermissionList> {
@@ -108,14 +127,19 @@ impl UpgraderCanister {
STATE.with(|state| state.projects.borrow().get(&key))
}

/// Inspects permissions for the project_create method
pub fn project_create_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::CreateProject])
}

/// Creates a new project
#[update]
pub fn project_create(&mut self, project: ProjectData) -> Result<()> {
STATE.with(|state| {
state
.permissions
.borrow()
.check_has_all_permissions(&ic::caller(), &[Permission::CreateProject])?;
Self::project_create_inspect(&state.permissions.borrow(), &ic::caller())?;
state.projects.borrow_mut().insert(project)
})
}
@@ -132,27 +156,47 @@ impl UpgraderCanister {
STATE.with(|state| state.polls.borrow().get(&id))
}

/// Inspects permissions for the poll_create method
pub fn poll_create_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::CreatePoll])
}

/// Creates a new poll and returns the generated poll id
#[update]
pub fn poll_create(&mut self, poll: Poll) -> Result<u64> {
pub fn poll_create(&mut self, poll: PollCreateData) -> Result<u64> {
STATE.with(|state| {
state
.permissions
.borrow()
.check_has_all_permissions(&ic::caller(), &[Permission::CreatePoll])?;
Self::poll_create_inspect(&state.permissions.borrow(), &ic::caller())?;

if let PollType::ProjectHash { project, hash: _ } = &poll.poll_type {
state.projects.borrow().get(project).ok_or_else(|| {
UpgraderError::BadRequest(format!(
"Cannot create poll, project [{}] does not exist",
project
))
})?;
}

Ok(state.polls.borrow_mut().insert(poll))
})
}

/// Inspects permissions for the poll_vote method
pub fn poll_vote_inspect<M: Memory>(
permissions: &Permissions<M>,
caller: &Principal,
) -> Result<()> {
permissions.check_has_all_permissions(caller, &[Permission::VotePoll])
}

/// Votes for a poll. If the voter has already voted, the previous vote is replaced.
#[update]
pub fn poll_vote(&mut self, poll_id: u64, approved: bool) -> Result<()> {
STATE.with(|state| {
let caller = ic::caller();
state
.permissions
.borrow()
.check_has_all_permissions(&caller, &[Permission::VotePoll])?;
Self::poll_vote_inspect(&state.permissions.borrow(), &caller)?;
state
.polls
.borrow_mut()
1 change: 1 addition & 0 deletions src/upgrader_canister/src/constant.rs
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@ pub(crate) const PERMISSIONS_MAP_MEMORY_ID: u8 = 1;
pub(crate) const PROJECTS_MAP_MEMORY_ID: u8 = 2;
pub(crate) const POLLS_MAP_MEMORY_ID: u8 = 3;
pub(crate) const POLLS_ID_SEQUENCE_MEMORY_ID: u8 = 4;
pub(crate) const SETTINGS_MAP_MEMORY_ID: u8 = 5;
42 changes: 42 additions & 0 deletions src/upgrader_canister/src/inspect_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// required by the inspect_message macro
#[allow(unused_imports)]
use ic_exports::ic_cdk::{self, api};
use ic_exports::ic_kit::ic;

use crate::canister::UpgraderCanister;
use crate::state::UpgraderCanisterState;

/// NOTE: inspect is disabled for non-wasm targets because without it we are getting a weird compilation error
/// in CI:
/// > multiple definition of `canister_inspect_message'
#[cfg(target_family = "wasm")]
#[ic_exports::ic_cdk_macros::inspect_message]
fn inspect_messages() {
crate::canister::STATE.with(|state| inspect_message_impl(state))
}

#[allow(dead_code)]
fn inspect_message_impl(state: &UpgraderCanisterState) {
// If inspect message is disabled, accept the message
if state.settings.borrow().is_inspect_message_disabled() {
api::call::accept_message();
return;
}

let permissions = state.permissions.borrow();
let method = api::call::method_name();

let check_result = match method.as_str() {
method if method.starts_with("admin_") => permissions.check_admin(&ic::caller()),
"project_create" => UpgraderCanister::project_create_inspect(&permissions, &ic::caller()),
"poll_create" => UpgraderCanister::poll_create_inspect(&permissions, &ic::caller()),
"poll_vote" => UpgraderCanister::poll_vote_inspect(&permissions, &ic::caller()),
_ => Ok(()),
};

if let Err(e) = check_result {
ic::trap(&format!("Call rejected by inspect check: {e:?}"));
} else {
api::call::accept_message();
}
}
1 change: 1 addition & 0 deletions src/upgrader_canister/src/lib.rs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ use ic_canister::generate_idl;
pub mod build_data;
pub mod canister;
pub mod constant;
pub mod inspect_message;
pub mod state;

pub fn idl() -> String {
4 changes: 4 additions & 0 deletions src/upgrader_canister/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -5,16 +5,19 @@ use ic_stable_structures::stable_structures::DefaultMemoryImpl;
use ic_stable_structures::{default_ic_memory_manager, VirtualMemory};
use permission::Permissions;
use polls::Polls;
use settings::Settings;

pub mod permission;
pub mod polls;
pub mod projects;
pub mod settings;

/// State of the upgrader canister
pub struct UpgraderCanisterState {
pub permissions: Rc<RefCell<Permissions<VirtualMemory<DefaultMemoryImpl>>>>,
pub polls: Rc<RefCell<Polls<VirtualMemory<DefaultMemoryImpl>>>>,
pub projects: Rc<RefCell<projects::Projects<VirtualMemory<DefaultMemoryImpl>>>>,
pub settings: Rc<RefCell<Settings<VirtualMemory<DefaultMemoryImpl>>>>,
}

impl Default for UpgraderCanisterState {
@@ -25,6 +28,7 @@ impl Default for UpgraderCanisterState {
permissions: Rc::new(RefCell::new(Permissions::new(&memory_manager))),
polls: Rc::new(RefCell::new(Polls::new(&memory_manager))),
projects: Rc::new(RefCell::new(projects::Projects::new(&memory_manager))),
settings: Rc::new(RefCell::new(Settings::new(&memory_manager))),
}
}
}
30 changes: 9 additions & 21 deletions src/upgrader_canister/src/state/polls.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ use ic_stable_structures::{
BTreeMapStructure, CellStructure, MemoryManager, StableBTreeMap, StableCell,
};
use upgrader_canister_did::error::{Result, UpgraderError};
use upgrader_canister_did::Poll;
use upgrader_canister_did::{Poll, PollCreateData};

use crate::constant::{POLLS_ID_SEQUENCE_MEMORY_ID, POLLS_MAP_MEMORY_ID};

@@ -36,9 +36,9 @@ impl<M: Memory> Polls<M> {
}

/// Inserts a new poll and returns the generated key
pub fn insert(&mut self, poll: Poll) -> u64 {
pub fn insert(&mut self, poll: PollCreateData) -> u64 {
let id = self.next_id();
self.polls.insert(id, poll);
self.polls.insert(id, poll.into());
id
}

@@ -115,10 +115,8 @@ mod test {
let mut polls = super::Polls::new(&memory_manager);

// Act
let poll_0_id = polls.insert(upgrader_canister_did::Poll {
let poll_0_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
@@ -127,10 +125,8 @@ mod test {
end_timestamp_secs: 234567,
});

let poll_1_id = polls.insert(upgrader_canister_did::Poll {
let poll_1_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_1".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
@@ -165,10 +161,8 @@ mod test {
// Arrange
let memory_manager = ic_stable_structures::default_ic_memory_manager();
let mut polls = super::Polls::new(&memory_manager);
let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
@@ -202,10 +196,8 @@ mod test {
// Arrange
let memory_manager = ic_stable_structures::default_ic_memory_manager();
let mut polls = super::Polls::new(&memory_manager);
let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
@@ -247,10 +239,8 @@ mod test {

let end_ts = 100;

let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
@@ -278,10 +268,8 @@ mod test {

let start_ts = 100;

let poll_id = polls.insert(upgrader_canister_did::Poll {
let poll_id = polls.insert(upgrader_canister_did::PollCreateData {
description: "poll_0".to_string(),
yes_voters: vec![],
no_voters: vec![],
poll_type: PollType::ProjectHash {
project: "project".to_owned(),
hash: "hash".to_owned(),
104 changes: 104 additions & 0 deletions src/upgrader_canister/src/state/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::borrow::Cow;

use candid::{CandidType, Deserialize};
use ic_stable_structures::stable_structures::Memory;
use ic_stable_structures::{Bound, CellStructure, MemoryManager, StableCell, Storable};
use serde::Serialize;
use upgrader_canister_did::codec;

use crate::constant::SETTINGS_MAP_MEMORY_ID;

pub struct Settings<M: Memory> {
settings: StableCell<SettingsData, M>,
}

impl<M: Memory> Settings<M> {
/// Create new settings
pub fn new(memory_manager: &dyn MemoryManager<M, u8>) -> Self {
let settings = StableCell::new(
memory_manager.get(SETTINGS_MAP_MEMORY_ID),
Default::default(),
)
.expect("failed to initialize settings in stable memory");

Self { settings }
}

/// Disable the inspect message
pub fn disable_inspect_message(&mut self, disable: bool) {
self.update(|s| {
s.disable_inspect_message = disable;
});
}

/// Returns true if the inspect message is disabled
pub fn is_inspect_message_disabled(&self) -> bool {
self.read(|s| s.disable_inspect_message)
}

fn read<F, T>(&self, f: F) -> T
where
for<'a> F: FnOnce(&'a SettingsData) -> T,
{
f(self.settings.get())
}

fn update<F, T>(&mut self, f: F) -> T
where
for<'a> F: FnOnce(&'a mut SettingsData) -> T,
{
let cell = &mut self.settings;
let mut new_settings = cell.get().clone();
let result = f(&mut new_settings);
cell.set(new_settings).expect("failed to set evm settings");
result
}
}

#[derive(Debug, Default, Deserialize, CandidType, Clone, PartialEq, Eq, Serialize)]
pub struct SettingsData {
disable_inspect_message: bool,
}

impl Storable for SettingsData {
fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
codec::encode(self).into()
}

fn from_bytes(bytes: Cow<[u8]>) -> Self {
codec::decode(&bytes)
}

const BOUND: Bound = Bound::Unbounded;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_storable_settings_data() {
let settings = SettingsData::default();

let serialized = settings.to_bytes();
let deserialized = SettingsData::from_bytes(serialized);

assert_eq!(settings, deserialized);
}

/// Test inspect message is not disabled by default
#[test]
fn test_default_inspect_message_disabled() {
let settings = SettingsData::default();
assert!(!settings.disable_inspect_message);
}

/// Test disabling the inspect message
#[test]
fn test_disable_inspect_message() {
let mut settings = Settings::new(&ic_stable_structures::default_ic_memory_manager());
assert!(!settings.is_inspect_message_disabled());
settings.disable_inspect_message(true);
assert!(settings.is_inspect_message_disabled());
}
}
244 changes: 207 additions & 37 deletions src/upgrader_canister/tests/canister/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::sync::Arc;

use candid::Principal;
use upgrader_canister_did::{Permission, Poll, PollType, ProjectData};
use ic_canister_client::CanisterClientResult;
use ic_exports::pocket_ic::PocketIc;
use upgrader_canister_did::{Permission, PollCreateData, PollType, ProjectData};

use crate::pocket_ic::{build_client, deploy_canister, ADMIN};

@@ -110,24 +114,88 @@ async fn test_only_admin_can_manage_permissions() {
// Arrange
let (pocket, canister_principal) = deploy_canister(None).await;
let caller_principal = Principal::from_slice(&[1u8; 29]);
let client = build_client(pocket, canister_principal, caller_principal);
let client = build_client(pocket.clone(), canister_principal, caller_principal);

// Act & Assert
assert!(client
.admin_permissions_get(caller_principal)
.await
.unwrap()
.is_err());
assert!(client
.admin_permissions_add(caller_principal, &[Permission::CreateProject])
.await
.unwrap()
.is_err());
assert!(client
.admin_permissions_remove(caller_principal, &[Permission::CreateProject])

assert_inspect_message_error(
&client
.admin_permissions_add(caller_principal, &[Permission::CreateProject])
.await,
);
assert_inspect_message_error(
&client
.admin_permissions_remove(caller_principal, &[Permission::CreateProject])
.await,
);

// Permission check should fail even if the inspect message is disabled
{
disable_inspect_message(pocket, canister_principal).await;

// Act & Assert
assert!(client
.admin_permissions_add(caller_principal, &[Permission::CreateProject])
.await
.unwrap()
.is_err());
assert!(client
.admin_permissions_remove(caller_principal, &[Permission::CreateProject])
.await
.unwrap()
.is_err());
}
}

/// Test that the admin can disable/enable the inspect message
#[tokio::test]
async fn test_admin_can_disable_inspect_message() {
// Arrange
let (pocket, canister_principal) = deploy_canister(None).await;
let caller_principal = ADMIN;
let client = build_client(pocket, canister_principal, caller_principal);

// Act
let inspect_message_disabled_before = client.is_inspect_message_disabled().await.unwrap();
client
.admin_disable_inspect_message(true)
.await
.unwrap()
.is_err());
.unwrap();
let inspect_message_disabled_after = client.is_inspect_message_disabled().await.unwrap();

// Assert
assert!(!inspect_message_disabled_before);
assert!(inspect_message_disabled_after);
}

/// Test that only the admin can disable/enable the inspect message
#[tokio::test]
async fn test_only_admin_can_disable_inspect_message() {
// Arrange
let (pocket, canister_principal) = deploy_canister(None).await;
let caller_principal = Principal::from_slice(&[1u8; 29]);
let client = build_client(pocket.clone(), canister_principal, caller_principal);

// Act & Assert
assert_inspect_message_error(&client.admin_disable_inspect_message(true).await);

// Permission check should fail even if the inspect message is disabled
{
disable_inspect_message(pocket, canister_principal).await;

// Act & Assert
assert!(client
.admin_disable_inspect_message(false)
.await
.unwrap()
.is_err());
}
}

/// Test that the caller can get their own permissions
@@ -218,11 +286,21 @@ async fn test_caller_cant_create_projects_if_not_allowed() {
name: "Project".to_string(),
description: "Description".to_string(),
};
let result = user_1_client.project_create(&project).await.unwrap();
assert_inspect_message_error(&user_1_client.project_create(&project).await);

// Assert
assert!(result.is_err());
// Permission check should fail even if the inspect message is disabled
{
disable_inspect_message(pocket, canister_principal).await;

// Act
assert!(user_1_client
.project_create(&project)
.await
.unwrap()
.is_err());
}

// Assert
let projects = user_1_client.project_get_all().await.unwrap();
assert!(projects.is_empty());
}
@@ -234,6 +312,8 @@ async fn test_caller_can_create_and_get_polls() {
let (pocket, canister_principal) = deploy_canister(None).await;

let admin_client = build_client(pocket.clone(), canister_principal, ADMIN);
let project_key = "project-0";
create_project(pocket.clone(), canister_principal, project_key).await;

// User with permission to create polls
let user_1_principal = Principal::from_slice(&[1u8; 29]);
@@ -249,14 +329,12 @@ async fn test_caller_can_create_and_get_polls() {
let user_2_client = build_client(pocket, canister_principal, user_2_principal);

// Act
let poll = Poll {
let poll = PollCreateData {
description: "Description".to_string(),
poll_type: PollType::ProjectHash {
project: "project".to_string(),
project: project_key.to_string(),
hash: "hash".to_string(),
},
no_voters: vec![Principal::from_slice(&[1u8; 29])],
yes_voters: vec![Principal::from_slice(&[2u8; 29])],
start_timestamp_secs: 0,
end_timestamp_secs: 1,
};
@@ -265,10 +343,44 @@ async fn test_caller_can_create_and_get_polls() {
// Assert
let polls = user_2_client.poll_get_all().await.unwrap();
assert_eq!(polls.len(), 1);
assert_eq!(polls[&poll_id], poll);
assert_eq!(polls[&poll_id], poll.clone().into());

let poll_from_get = user_2_client.poll_get(poll_id).await.unwrap().unwrap();
assert_eq!(poll_from_get, poll);
assert_eq!(poll_from_get, poll.into());
}

/// Test that the caller cannot create a poll for a not existing project
#[tokio::test]
async fn test_caller_cant_create_poll_for_not_existing_project() {
// Arrange
let (pocket, canister_principal) = deploy_canister(None).await;

let admin_client = build_client(pocket.clone(), canister_principal, ADMIN);

// User with permission to create polls
let user_1_principal = Principal::from_slice(&[1u8; 29]);
let user_1_client = build_client(pocket.clone(), canister_principal, user_1_principal);
admin_client
.admin_permissions_add(
user_1_principal,
&[Permission::CreatePoll, Permission::VotePoll],
)
.await
.unwrap()
.unwrap();

let poll = PollCreateData {
description: "Description".to_string(),
poll_type: PollType::ProjectHash {
project: "project".to_string(),
hash: "hash".to_string(),
},
start_timestamp_secs: 0,
end_timestamp_secs: 1,
};

// Act & Assert
assert!(user_1_client.poll_create(&poll).await.unwrap().is_err());
}

/// Test that the caller can't create polls if not allowed
@@ -279,23 +391,30 @@ async fn test_caller_cant_create_polls_if_not_allowed() {
let user_1_principal = Principal::from_slice(&[1u8; 29]);
let user_1_client = build_client(pocket.clone(), canister_principal, user_1_principal);

let project_key = "project-1";
create_project(pocket.clone(), canister_principal, project_key).await;

// Act
let poll = Poll {
let poll = PollCreateData {
description: "Description".to_string(),
poll_type: PollType::ProjectHash {
project: "project".to_string(),
project: project_key.to_string(),
hash: "hash".to_string(),
},
no_voters: vec![Principal::from_slice(&[1u8; 29])],
yes_voters: vec![Principal::from_slice(&[2u8; 29])],
start_timestamp_secs: 0,
end_timestamp_secs: 1,
};
let result = user_1_client.poll_create(&poll).await.unwrap();
assert_inspect_message_error(&user_1_client.poll_create(&poll).await);

// Assert
assert!(result.is_err());
// Permission check should fail even if the inspect message is disabled
{
disable_inspect_message(pocket, canister_principal).await;

// Act
assert!(user_1_client.poll_create(&poll).await.unwrap().is_err());
}

// Assert
let polls = user_1_client.poll_get_all().await.unwrap();
assert!(polls.is_empty());
}
@@ -311,6 +430,9 @@ async fn test_caller_can_vote_in_poll() {
let user_2_client = build_client(pocket.clone(), canister_principal, user_2_principal);
let admin_client = build_client(pocket.clone(), canister_principal, ADMIN);

let project_key = "project-10";
create_project(pocket.clone(), canister_principal, project_key).await;

admin_client
.admin_permissions_add(
user_1_principal,
@@ -325,14 +447,12 @@ async fn test_caller_can_vote_in_poll() {
.unwrap()
.unwrap();

let poll = Poll {
let poll = PollCreateData {
description: "Description".to_string(),
poll_type: PollType::ProjectHash {
project: "project".to_string(),
project: project_key.to_string(),
hash: "hash".to_string(),
},
no_voters: vec![],
yes_voters: vec![],
start_timestamp_secs: 0,
end_timestamp_secs: u64::MAX,
};
@@ -369,32 +489,82 @@ async fn test_caller_cant_vote_in_poll_if_not_allowed() {
let user_2_client = build_client(pocket.clone(), canister_principal, user_2_principal);
let admin_client = build_client(pocket.clone(), canister_principal, ADMIN);

let project_key = "project-10";
create_project(pocket.clone(), canister_principal, project_key).await;

admin_client
.admin_permissions_add(user_1_principal, &[Permission::CreatePoll])
.await
.unwrap()
.unwrap();

let poll = Poll {
let poll = PollCreateData {
description: "Description".to_string(),
poll_type: PollType::ProjectHash {
project: "project".to_string(),
project: project_key.to_string(),
hash: "hash".to_string(),
},
no_voters: vec![],
yes_voters: vec![],
start_timestamp_secs: 0,
end_timestamp_secs: u64::MAX,
};
let poll_id = user_1_client.poll_create(&poll).await.unwrap().unwrap();

// Act
let result = user_2_client.poll_vote(poll_id, true).await.unwrap();
assert_inspect_message_error(&user_2_client.poll_vote(poll_id, true).await);

// Assert
assert!(result.is_err());
// Permission check should fail even if the inspect message is disabled
{
disable_inspect_message(pocket, canister_principal).await;

// Act
assert!(user_2_client
.poll_vote(poll_id, true)
.await
.unwrap()
.is_err());
}

// Assert
let poll = user_1_client.poll_get(poll_id).await.unwrap().unwrap();
assert!(poll.yes_voters.is_empty());
assert!(poll.no_voters.is_empty());
}

fn assert_inspect_message_error<T: std::fmt::Debug>(result: &CanisterClientResult<T>) {
assert!(result.is_err());
let error = result.as_ref().unwrap_err();
assert!(error.to_string().contains("Call rejected by inspect check"));
}

async fn disable_inspect_message(pocket: Arc<PocketIc>, canister_principal: Principal) {
let admin_client = build_client(pocket, canister_principal, ADMIN);
admin_client
.admin_disable_inspect_message(true)
.await
.unwrap()
.unwrap();
}

async fn create_project(pocket: Arc<PocketIc>, canister_principal: Principal, project_key: &str) {
let user_1_principal = Principal::from_slice(&[199u8; 29]);
let user_1_client = build_client(pocket.clone(), canister_principal, user_1_principal);
let admin_client = build_client(pocket.clone(), canister_principal, ADMIN);

admin_client
.admin_permissions_add(user_1_principal, &[Permission::CreateProject])
.await
.unwrap()
.unwrap();

// Act
let project = ProjectData {
key: project_key.to_string(),
name: format!("Project {}", project_key),
description: format!("Description {}", project_key),
};
user_1_client
.project_create(&project)
.await
.unwrap()
.unwrap();
}
21 changes: 19 additions & 2 deletions src/upgrader_canister_client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,9 @@ use std::collections::BTreeMap;
use candid::Principal;
use ic_canister_client::{CanisterClient, CanisterClientResult};
use upgrader_canister_did::error::Result;
use upgrader_canister_did::{BuildData, Permission, PermissionList, Poll, ProjectData};
use upgrader_canister_did::{
BuildData, Permission, PermissionList, Poll, PollCreateData, ProjectData,
};

/// An upgrader canister client.
#[derive(Debug, Clone)]
@@ -61,6 +63,21 @@ impl<C: CanisterClient> UpgraderCanisterClient<C> {
.await
}

/// Disable/Enable the inspect message
pub async fn admin_disable_inspect_message(
&self,
value: bool,
) -> CanisterClientResult<Result<()>> {
self.client
.update("admin_disable_inspect_message", (value,))
.await
}

/// Returns whether the inspect message is disabled.
pub async fn is_inspect_message_disabled(&self) -> CanisterClientResult<bool> {
self.client.query("is_inspect_message_disabled", ()).await
}

/// Returns the permissions of the caller
pub async fn caller_permissions_get(&self) -> CanisterClientResult<Result<PermissionList>> {
self.client.query("caller_permissions_get", ()).await
@@ -92,7 +109,7 @@ impl<C: CanisterClient> UpgraderCanisterClient<C> {
}

/// Creates a new poll and returns the generated poll id
pub async fn poll_create(&self, poll: &Poll) -> CanisterClientResult<Result<u64>> {
pub async fn poll_create(&self, poll: &PollCreateData) -> CanisterClientResult<Result<u64>> {
self.client.update("poll_create", (poll,)).await
}

0 comments on commit f030afd

Please sign in to comment.