Эх сурвалжийг харах

runtime: proposals: Add tests for discussion modes.

Shamil Gadelshin 4 жил өмнө
parent
commit
a14b60ffe5

+ 1 - 1
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -131,7 +131,7 @@ parameter_types! {
 
 impl proposals_discussion::Trait for Test {
     type Event = ();
-    type PostAuthorOriginValidator = ();
+    type AuthorOriginValidator = ();
     type CouncilOriginValidator = ();
     type ThreadId = u64;
     type PostId = u64;

+ 38 - 7
runtime-modules/proposals/discussion/src/lib.rs

@@ -77,6 +77,9 @@ decl_event!(
 
         /// Emits on post update.
         PostUpdated(PostId, MemberId),
+
+        /// Emits on thread mode change.
+        ThreadModeChanged(ThreadId, ThreadMode<MemberId>),
     }
 );
 
@@ -92,11 +95,7 @@ pub trait Trait: system::Trait + membership::Trait {
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
     /// Validates post author id and origin combination
-    type PostAuthorOriginValidator: ActorOriginValidator<
-        Self::Origin,
-        MemberId<Self>,
-        Self::AccountId,
-    >;
+    type AuthorOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
 
     /// Defines whether the member is an active councilor.
     type CouncilOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
@@ -155,6 +154,9 @@ decl_error! {
 
         /// The thread has Closed mode. And post author doesn't belong to council or allowed members.
         CannotPostOnClosedThread,
+
+        /// Should be thread author or councilor.
+        NotAuthorOrCouncilor,
     }
 }
 
@@ -211,7 +213,7 @@ decl_module! {
             thread_id : T::ThreadId,
             text : Vec<u8>
         ) {
-            T::PostAuthorOriginValidator::ensure_actor_origin(
+            T::AuthorOriginValidator::ensure_actor_origin(
                 origin.clone(),
                 post_author_id,
             )?;
@@ -255,7 +257,7 @@ decl_module! {
             post_id : T::PostId,
             text : Vec<u8>
         ){
-            T::PostAuthorOriginValidator::ensure_actor_origin(
+            T::AuthorOriginValidator::ensure_actor_origin(
                 origin,
                 post_author_id,
             )?;
@@ -287,6 +289,35 @@ decl_module! {
             <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
             Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
        }
+
+        /// Changes thread permission mode.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn change_thread_mode(
+            origin,
+            member_id: MemberId<T>,
+            thread_id : T::ThreadId,
+            mode : ThreadMode<MemberId<T>>
+        ) {
+            T::AuthorOriginValidator::ensure_actor_origin(origin.clone(), member_id)?;
+
+            ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
+
+            let thread = Self::thread_by_id(&thread_id);
+
+            let is_councilor =
+                    T::CouncilOriginValidator::ensure_actor_origin(origin, member_id)
+                        .is_ok();
+            let is_thread_author = thread.author_id == member_id;
+
+            ensure!(is_thread_author || is_councilor, Error::<T>::NotAuthorOrCouncilor);
+
+            // mutation
+
+            <ThreadById<T>>::mutate(thread_id, |thread| {
+                thread.mode = mode.clone();
+            });
+            Self::deposit_event(RawEvent::ThreadModeChanged(thread_id, mode));
+       }
     }
 }
 

+ 26 - 3
runtime-modules/proposals/discussion/src/tests/mock.rs

@@ -81,8 +81,8 @@ impl membership::Trait for Test {
 
 impl crate::Trait for Test {
     type Event = TestEvent;
-    type PostAuthorOriginValidator = ();
-    type CouncilOriginValidator = ();
+    type AuthorOriginValidator = ();
+    type CouncilOriginValidator = CouncilMock;
     type ThreadId = u64;
     type PostId = u64;
     type MaxPostEditionNumber = MaxPostEditionNumber;
@@ -93,7 +93,7 @@ impl crate::Trait for Test {
 
 impl ActorOriginValidator<Origin, u64, u64> for () {
     fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result<u64, &'static str> {
-        if system::ensure_none(origin).is_ok() {
+        if system::ensure_none(origin.clone()).is_ok() {
             return Ok(1);
         }
 
@@ -101,10 +101,33 @@ impl ActorOriginValidator<Origin, u64, u64> for () {
             return Ok(1);
         }
 
+        if actor_id == 2 {
+            return Ok(2);
+        }
+
+        if actor_id == 11 {
+            return Ok(11);
+        }
+
+        if actor_id == 12 && system::ensure_signed(origin).unwrap_or_default() == 12 {
+            return Ok(12);
+        }
+
         Err("Invalid author")
     }
 }
 
+pub struct CouncilMock;
+impl ActorOriginValidator<Origin, u64, u64> for CouncilMock {
+    fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result<u64, &'static str> {
+        if actor_id == 2 && system::ensure_signed(origin).unwrap_or_default() == 2 {
+            return Ok(2);
+        }
+
+        Err("Not a council")
+    }
+}
+
 impl system::Trait for Test {
     type BaseCallFilter = ();
     type Origin = Origin;

+ 205 - 1
runtime-modules/proposals/discussion/src/tests/mod.rs

@@ -85,6 +85,10 @@ impl DiscussionFixture {
         DiscussionFixture { title, ..self }
     }
 
+    fn with_mode(self, mode: ThreadMode<u64>) -> Self {
+        Self { mode, ..self }
+    }
+
     fn create_discussion_and_assert(&self, result: Result<u64, DispatchError>) -> Option<u64> {
         let create_discussion_result =
             Discussions::create_thread(self.author_id, self.title.clone(), self.mode.clone());
@@ -195,6 +199,47 @@ fn create_post_call_succeeds() {
     });
 }
 
+struct ChangeThreadModeFixture {
+    pub origin: RawOrigin<u64>,
+    pub thread_id: u64,
+    pub member_id: u64,
+    pub mode: ThreadMode<u64>,
+}
+
+impl ChangeThreadModeFixture {
+    fn default_for_thread_id(thread_id: u64) -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            thread_id,
+            member_id: 1,
+            mode: ThreadMode::Open,
+        }
+    }
+
+    fn with_mode(self, mode: ThreadMode<u64>) -> Self {
+        Self { mode, ..self }
+    }
+
+    fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Discussions::change_thread_mode(
+            self.origin.clone().into(),
+            self.member_id,
+            self.thread_id,
+            self.mode.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+    }
+}
+
 #[test]
 fn update_post_call_succeeds() {
     initial_test_ext().execute_with(|| {
@@ -258,7 +303,7 @@ fn update_post_call_fails_because_of_the_wrong_author() {
 
         post_fixture.add_post_and_assert(Ok(()));
 
-        post_fixture = post_fixture.with_author(2);
+        post_fixture = post_fixture.with_author(5);
 
         post_fixture.update_post_and_assert(Err(DispatchError::Other("Invalid author")));
 
@@ -428,3 +473,162 @@ fn discussion_thread_and_post_counters_are_valid() {
         assert_eq!(Discussions::post_count(), 1);
     });
 }
+
+#[test]
+fn change_thread_mode_succeeds() {
+    initial_test_ext().execute_with(|| {
+        /*
+           Events are not emitted on block 0.
+           So any dispatchable calls made during genesis block formation will have no events emitted.
+           https://substrate.dev/recipes/2-appetizers/4-events.html
+        */
+        run_to_block(1);
+
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let thread_mode = ThreadMode::Closed(vec![2, 3]);
+        let change_thread_mode_fixture = ChangeThreadModeFixture::default_for_thread_id(thread_id)
+            .with_mode(thread_mode.clone());
+        change_thread_mode_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ThreadCreated(1, 1),
+            RawEvent::ThreadModeChanged(1, thread_mode),
+        ]);
+    });
+}
+
+#[test]
+fn change_mode_failed_with_invalid_origin() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let change_thread_mode_fixture = ChangeThreadModeFixture::default_for_thread_id(thread_id)
+            .with_origin(RawOrigin::Root)
+            .with_member_id(12);
+        change_thread_mode_fixture.call_and_assert(Err(DispatchError::Other("Invalid author")));
+    });
+}
+
+#[test]
+fn change_mode_failed_with_invalid_thread_id() {
+    initial_test_ext().execute_with(|| {
+        let invalid_thread_id = 12;
+
+        let change_thread_mode_fixture =
+            ChangeThreadModeFixture::default_for_thread_id(invalid_thread_id);
+        change_thread_mode_fixture.call_and_assert(Err(Error::<Test>::ThreadDoesntExist.into()));
+    });
+}
+
+#[test]
+fn change_mode_failed_with_not_thread_author_or_councilor() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let change_thread_mode_fixture = ChangeThreadModeFixture::default_for_thread_id(thread_id)
+            .with_origin(RawOrigin::Signed(12))
+            .with_member_id(12);
+        change_thread_mode_fixture.call_and_assert(Err(Error::<Test>::NotAuthorOrCouncilor.into()));
+    });
+}
+
+#[test]
+fn change_thread_mode_succeeds_with_councilor() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let change_thread_mode_fixture = ChangeThreadModeFixture::default_for_thread_id(thread_id)
+            .with_member_id(2)
+            .with_origin(RawOrigin::Signed(2));
+        change_thread_mode_fixture.call_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn create_post_call_succeeds_with_closed_mode_by_author() {
+    initial_test_ext().execute_with(|| {
+        let mode = ThreadMode::Closed(vec![2, 11]);
+        let discussion_fixture = DiscussionFixture::default().with_mode(mode);
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id)
+            .with_origin(RawOrigin::Signed(1))
+            .with_author(1);
+
+        post_fixture.add_post_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn create_post_call_succeeds_with_closed_mode_by_councilor() {
+    initial_test_ext().execute_with(|| {
+        let mode = ThreadMode::Closed(vec![2, 11]);
+        let discussion_fixture = DiscussionFixture::default().with_mode(mode);
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id)
+            .with_origin(RawOrigin::Signed(2))
+            .with_author(2);
+
+        post_fixture.add_post_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn create_post_call_succeeds_with_closed_mode_by_white_listed_member() {
+    initial_test_ext().execute_with(|| {
+        let mode = ThreadMode::Closed(vec![2, 11]);
+        let discussion_fixture = DiscussionFixture::default().with_mode(mode);
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id)
+            .with_origin(RawOrigin::Signed(11))
+            .with_author(11);
+
+        post_fixture.add_post_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn create_post_call_fails_with_closed_mode_by_not_allowed_member() {
+    initial_test_ext().execute_with(|| {
+        let mode = ThreadMode::Closed(vec![2, 10]);
+        let discussion_fixture = DiscussionFixture::default().with_mode(mode);
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id)
+            .with_origin(RawOrigin::Signed(11))
+            .with_author(11);
+
+        post_fixture.add_post_and_assert(Err(Error::<Test>::CannotPostOnClosedThread.into()));
+    });
+}

+ 2 - 2
runtime-modules/proposals/discussion/src/types.rs

@@ -74,8 +74,8 @@ impl<ThreadAuthorId: Clone> ThreadCounter<ThreadAuthorId> {
 }
 
 /// Discussion thread permission modes.
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
 pub enum ThreadMode<MemberId> {
     /// Every member can post on the thread.
     Open,

+ 1 - 1
runtime/src/lib.rs

@@ -585,7 +585,7 @@ parameter_types! {
 
 impl proposals_discussion::Trait for Runtime {
     type Event = Event;
-    type PostAuthorOriginValidator = MembershipOriginValidator<Self>;
+    type AuthorOriginValidator = MembershipOriginValidator<Self>;
     type CouncilOriginValidator = CouncilManager<Self>;
     type ThreadId = ThreadId;
     type PostId = PostId;