// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! The crate's tests. use std::collections::BTreeMap; use frame_support::{ assert_noop, assert_ok, derive_impl, error::BadOrigin, parameter_types, traits::{ConstU16, EitherOf, MapSuccess, Polling}, }; use sp_core::Get; use sp_runtime::{traits::ReduceBy, BuildStorage}; use super::*; use crate as pallet_ranked_collective; type Block = frame_system::mocking::MockBlock; type Class = Rank; frame_support::construct_runtime!( pub enum Test { System: frame_system, Club: pallet_ranked_collective, } ); #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { type Block = Block; } #[derive(Clone, PartialEq, Eq, Debug)] pub enum TestPollState { Ongoing(TallyOf, Rank), Completed(u64, bool), } use TestPollState::*; parameter_types! { pub static Polls: BTreeMap = vec![ (1, Completed(1, true)), (2, Completed(2, false)), (3, Ongoing(Tally::from_parts(0, 0, 0), 1)), ].into_iter().collect(); } pub struct TestPolls; impl Polling> for TestPolls { type Index = u8; type Votes = Votes; type Moment = u64; type Class = Class; fn classes() -> Vec { vec![0, 1, 2] } fn as_ongoing(index: u8) -> Option<(TallyOf, Self::Class)> { Polls::get().remove(&index).and_then(|x| { if let TestPollState::Ongoing(t, c) = x { Some((t, c)) } else { None } }) } fn access_poll( index: Self::Index, f: impl FnOnce(PollStatus<&mut TallyOf, Self::Moment, Self::Class>) -> R, ) -> R { let mut polls = Polls::get(); let entry = polls.get_mut(&index); let r = match entry { Some(Ongoing(ref mut tally_mut_ref, class)) => f(PollStatus::Ongoing(tally_mut_ref, *class)), Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), None => f(PollStatus::None), }; Polls::set(polls); r } fn try_access_poll( index: Self::Index, f: impl FnOnce( PollStatus<&mut TallyOf, Self::Moment, Self::Class>, ) -> Result, ) -> Result { let mut polls = Polls::get(); let entry = polls.get_mut(&index); let r = match entry { Some(Ongoing(ref mut tally_mut_ref, class)) => f(PollStatus::Ongoing(tally_mut_ref, *class)), Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), None => f(PollStatus::None), }?; Polls::set(polls); Ok(r) } #[cfg(feature = "runtime-benchmarks")] fn create_ongoing(class: Self::Class) -> Result { let mut polls = Polls::get(); let i = polls.keys().rev().next().map_or(0, |x| x + 1); polls.insert(i, Ongoing(Tally::new(class), class)); Polls::set(polls); Ok(i) } #[cfg(feature = "runtime-benchmarks")] fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> { let mut polls = Polls::get(); match polls.get(&index) { Some(Ongoing(..)) => {}, _ => return Err(()), } let now = frame_system::Pallet::::block_number(); polls.insert(index, Completed(now, approved)); Polls::set(polls); Ok(()) } } /// Convert the tally class into the minimum rank required to vote on the poll. /// MinRank(Class) = Class - Delta pub struct MinRankOfClass(PhantomData); impl> Convert for MinRankOfClass { fn convert(a: Class) -> Rank { a.saturating_sub(Delta::get()) } } parameter_types! { pub static MinRankOfClassDelta: Rank = 0; } impl Config for Test { type WeightInfo = (); type RuntimeEvent = RuntimeEvent; type PromoteOrigin = EitherOf< // Root can promote arbitrarily. frame_system::EnsureRootWithSuccess>, // Members can promote up to the rank of 2 below them. MapSuccess, ReduceBy>>, >; type DemoteOrigin = EitherOf< // Root can demote arbitrarily. frame_system::EnsureRootWithSuccess>, // Members can demote up to the rank of 3 below them. MapSuccess, ReduceBy>>, >; type ExchangeOrigin = EitherOf< // Root can exchange arbitrarily. frame_system::EnsureRootWithSuccess>, // Members can exchange up to the rank of 2 below them. MapSuccess, ReduceBy>>, >; type Polls = TestPolls; type MinRankOfClass = MinRankOfClass; type MemberSwappedHandler = (); type VoteWeight = Geometric; #[cfg(feature = "runtime-benchmarks")] type BenchmarkSetup = (); } pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext } fn next_block() { System::set_block_number(System::block_number() + 1); } fn member_count(r: Rank) -> MemberIndex { MemberCount::::get(r) } #[allow(dead_code)] fn run_to(n: u64) { while System::block_number() < n { next_block(); } } fn tally(index: u8) -> TallyOf { >>::as_ongoing(index).expect("No poll").0 } #[test] #[ignore] #[should_panic(expected = "No poll")] fn unknown_poll_should_panic() { let _ = tally(0); } #[test] #[ignore] #[should_panic(expected = "No poll")] fn completed_poll_should_panic() { let _ = tally(1); } #[test] fn basic_stuff() { new_test_ext().execute_with(|| { assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); }); } #[test] fn member_lifecycle_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 0); assert_eq!(member_count(1), 0); }); } #[test] fn add_remove_works() { new_test_ext().execute_with(|| { assert_noop!(Club::add_member(RuntimeOrigin::signed(1), 1), DispatchError::BadOrigin); assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 0); assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_eq!(member_count(0), 2); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_eq!(member_count(0), 3); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 3)); assert_eq!(member_count(0), 2); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 2)); assert_eq!(member_count(0), 0); }); } #[test] fn promote_demote_works() { new_test_ext().execute_with(|| { assert_noop!(Club::add_member(RuntimeOrigin::signed(1), 1), DispatchError::BadOrigin); assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_eq!(member_count(1), 0); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 0); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 1); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 2); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 1); assert_noop!(Club::demote_member(RuntimeOrigin::signed(1), 1), DispatchError::BadOrigin); assert_ok!(Club::demote_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_eq!(member_count(1), 1); }); } #[test] fn promote_demote_by_rank_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); for _ in 0..7 { assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); } // #1 can add #2 and promote to rank 1 assert_ok!(Club::add_member(RuntimeOrigin::signed(1), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(1), 2)); // #2 as rank 1 cannot do anything privileged assert_noop!(Club::add_member(RuntimeOrigin::signed(2), 3), BadOrigin); assert_ok!(Club::promote_member(RuntimeOrigin::signed(1), 2)); // #2 as rank 2 can add #3. assert_ok!(Club::add_member(RuntimeOrigin::signed(2), 3)); // #2 as rank 2 cannot promote #3 to rank 1 assert_noop!( Club::promote_member(RuntimeOrigin::signed(2), 3), Error::::NoPermission ); // #1 as rank 7 can promote #2 only up to rank 5 and once there cannot demote them. assert_ok!(Club::promote_member(RuntimeOrigin::signed(1), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(1), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(1), 2)); assert_noop!( Club::promote_member(RuntimeOrigin::signed(1), 2), Error::::NoPermission ); assert_noop!(Club::demote_member(RuntimeOrigin::signed(1), 2), Error::::NoPermission); // #2 as rank 5 can promote #3 only up to rank 3 and once there cannot demote them. assert_ok!(Club::promote_member(RuntimeOrigin::signed(2), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(2), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(2), 3)); assert_noop!( Club::promote_member(RuntimeOrigin::signed(2), 3), Error::::NoPermission ); assert_noop!(Club::demote_member(RuntimeOrigin::signed(2), 3), Error::::NoPermission); // #2 can add #4 & #5 as rank 0 and #6 & #7 as rank 1. assert_ok!(Club::add_member(RuntimeOrigin::signed(2), 4)); assert_ok!(Club::add_member(RuntimeOrigin::signed(2), 5)); assert_ok!(Club::add_member(RuntimeOrigin::signed(2), 6)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(2), 6)); assert_ok!(Club::add_member(RuntimeOrigin::signed(2), 7)); assert_ok!(Club::promote_member(RuntimeOrigin::signed(2), 7)); // #3 as rank 3 can demote/remove #4 & #5 but not #6 & #7 assert_ok!(Club::demote_member(RuntimeOrigin::signed(3), 4)); assert_ok!(Club::remove_member(RuntimeOrigin::signed(3), 5, 0)); assert_noop!(Club::demote_member(RuntimeOrigin::signed(3), 6), Error::::NoPermission); assert_noop!( Club::remove_member(RuntimeOrigin::signed(3), 7, 1), Error::::NoPermission ); // #2 as rank 5 can demote/remove #6 & #7 assert_ok!(Club::demote_member(RuntimeOrigin::signed(2), 6)); assert_ok!(Club::remove_member(RuntimeOrigin::signed(2), 7, 1)); }); } #[test] fn voting_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 0)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_noop!(Club::vote(RuntimeOrigin::signed(0), 3, true), Error::::RankTooLow); assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); assert_ok!(Club::vote(RuntimeOrigin::signed(1), 3, true)); assert_eq!(tally(3), Tally::from_parts(1, 1, 0)); assert_ok!(Club::vote(RuntimeOrigin::signed(1), 3, false)); assert_eq!(tally(3), Tally::from_parts(0, 0, 1)); assert_ok!(Club::vote(RuntimeOrigin::signed(2), 3, true)); assert_eq!(tally(3), Tally::from_parts(1, 3, 1)); assert_ok!(Club::vote(RuntimeOrigin::signed(2), 3, false)); assert_eq!(tally(3), Tally::from_parts(0, 0, 4)); assert_ok!(Club::vote(RuntimeOrigin::signed(3), 3, true)); assert_eq!(tally(3), Tally::from_parts(1, 6, 4)); assert_ok!(Club::vote(RuntimeOrigin::signed(3), 3, false)); assert_eq!(tally(3), Tally::from_parts(0, 0, 10)); }); } #[test] fn cleanup_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::vote(RuntimeOrigin::signed(1), 3, true)); assert_ok!(Club::vote(RuntimeOrigin::signed(2), 3, false)); assert_ok!(Club::vote(RuntimeOrigin::signed(3), 3, true)); assert_noop!(Club::cleanup_poll(RuntimeOrigin::signed(4), 3, 10), Error::::Ongoing); Polls::set( vec![(1, Completed(1, true)), (2, Completed(2, false)), (3, Completed(3, true))] .into_iter() .collect(), ); assert_ok!(Club::cleanup_poll(RuntimeOrigin::signed(4), 3, 10)); // NOTE: This will fail until #10016 is merged. // assert_noop!(Club::cleanup_poll(RuntimeOrigin::signed(4), 3, 10), // Error::::NoneRemaining); }); } #[test] fn remove_member_cleanup_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_eq!(IdToIndex::::get(1, 2), Some(1)); assert_eq!(IndexToId::::get(1, 1), Some(2)); assert_eq!(IdToIndex::::get(1, 3), Some(2)); assert_eq!(IndexToId::::get(1, 2), Some(3)); assert_ok!(Club::remove_member(RuntimeOrigin::root(), 2, 1)); assert_eq!(IdToIndex::::get(1, 2), None); assert_eq!(IndexToId::::get(1, 1), Some(3)); assert_eq!(IdToIndex::::get(1, 3), Some(1)); assert_eq!(IndexToId::::get(1, 2), None); }); } #[test] fn ensure_ranked_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); use frame_support::traits::OriginTrait; type Rank1 = EnsureRanked; type Rank2 = EnsureRanked; type Rank3 = EnsureRanked; type Rank4 = EnsureRanked; assert_eq!(>::try_origin(RuntimeOrigin::signed(1)).unwrap(), 1); assert_eq!(>::try_origin(RuntimeOrigin::signed(2)).unwrap(), 2); assert_eq!(>::try_origin(RuntimeOrigin::signed(3)).unwrap(), 3); assert_eq!( >::try_origin(RuntimeOrigin::signed(1)) .unwrap_err() .into_signer() .unwrap(), 1 ); assert_eq!(>::try_origin(RuntimeOrigin::signed(2)).unwrap(), 2); assert_eq!(>::try_origin(RuntimeOrigin::signed(3)).unwrap(), 3); assert_eq!( >::try_origin(RuntimeOrigin::signed(1)) .unwrap_err() .into_signer() .unwrap(), 1 ); assert_eq!( >::try_origin(RuntimeOrigin::signed(2)) .unwrap_err() .into_signer() .unwrap(), 2 ); assert_eq!(>::try_origin(RuntimeOrigin::signed(3)).unwrap(), 3); assert_eq!( >::try_origin(RuntimeOrigin::signed(1)) .unwrap_err() .into_signer() .unwrap(), 1 ); assert_eq!( >::try_origin(RuntimeOrigin::signed(2)) .unwrap_err() .into_signer() .unwrap(), 2 ); assert_eq!( >::try_origin(RuntimeOrigin::signed(3)) .unwrap_err() .into_signer() .unwrap(), 3 ); }); } #[test] fn do_add_member_to_rank_works() { new_test_ext().execute_with(|| { let max_rank = 9u16; assert_ok!(Club::do_add_member_to_rank(69, max_rank / 2, true)); assert_ok!(Club::do_add_member_to_rank(1337, max_rank, true)); for i in 0..=max_rank { if i <= max_rank / 2 { assert_eq!(member_count(i), 2); } else { assert_eq!(member_count(i), 1); } } assert_eq!(member_count(max_rank + 1), 0); }) } #[test] fn tally_support_correct() { new_test_ext().execute_with(|| { // add members, // rank 1: accounts 1, 2, 3 // rank 2: accounts 2, 3 // rank 3: accounts 3. assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); // init tally with 1 aye vote. let tally: TallyOf = Tally::from_parts(1, 1, 0); // with minRank(Class) = Class // for class 3, 100% support. MinRankOfClassDelta::set(0); assert_eq!(tally.support(3), Perbill::from_rational(1u32, 1)); // with minRank(Class) = (Class - 1) // for class 3, ~50% support. MinRankOfClassDelta::set(1); assert_eq!(tally.support(3), Perbill::from_rational(1u32, 2)); // with minRank(Class) = (Class - 2) // for class 3, ~33% support. MinRankOfClassDelta::set(2); assert_eq!(tally.support(3), Perbill::from_rational(1u32, 3)); // reset back. MinRankOfClassDelta::set(0); }); } #[test] fn exchange_member_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_eq!(member_count(0), 1); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); let member_record = MemberRecord { rank: 1 }; assert_eq!(Members::::get(1), Some(member_record.clone())); assert_eq!(Members::::get(2), None); assert_ok!(Club::exchange_member(RuntimeOrigin::root(), 1, 2)); assert_eq!(member_count(0), 1); assert_eq!(Members::::get(1), None); assert_eq!(Members::::get(2), Some(member_record)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); assert_noop!( Club::exchange_member(RuntimeOrigin::signed(3), 2, 1), DispatchError::BadOrigin ); }); } #[test] fn exchange_member_same_noops() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); // Swapping the same accounts is a noop: assert_noop!(Club::exchange_member(RuntimeOrigin::root(), 1, 1), Error::::SameMember); // Swapping with a different member is a noop: assert_noop!( Club::exchange_member(RuntimeOrigin::root(), 1, 2), Error::::AlreadyMember ); }); }