Browse Source

Merge branch 'olympia' into olympia-staging

Leszek Wiesner 3 years ago
parent
commit
05bdeefd30

+ 0 - 2
Cargo.lock

@@ -1,7 +1,5 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
-
 [[package]]
 name = "Inflector"
 version = "0.11.4"

+ 214 - 2
runtime-modules/forum/src/benchmarking.rs

@@ -591,7 +591,7 @@ benchmarks! {
 
         assert_eq!(Module::<T>::category_by_id(category_id), new_category);
         assert_last_event::<T>(
-            RawEvent::CategoryUpdated(
+            RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 PrivilegedActor::Lead
@@ -636,7 +636,7 @@ benchmarks! {
 
         assert_eq!(Module::<T>::category_by_id(category_id), new_category);
         assert_last_event::<T>(
-            RawEvent::CategoryUpdated(
+            RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 PrivilegedActor::Moderator(moderator_id)
@@ -644,6 +644,188 @@ benchmarks! {
         );
     }
 
+    update_category_title_lead{
+        let lead_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(lead_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_title = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+
+    }: update_category_title(RawOrigin::Signed(caller_id), PrivilegedActor::Lead, category_id, new_title.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_title_hash = T::calculate_hash(new_title.as_slice());
+
+        let new_category = Category {
+            title_hash: new_title_hash,
+            description_hash: T::calculate_hash(text.as_slice()),
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 0,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                PrivilegedActor::Lead
+            ).into()
+        );
+    }
+
+    update_category_title_moderator{
+        let moderator_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(moderator_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_title = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+        let moderator_id = ModeratorId::<T>::from(moderator_id.try_into().unwrap());
+
+        // Set up category membership of moderator.
+        Module::<T>::update_category_membership_of_moderator(
+            RawOrigin::Signed(caller_id.clone()).into(), moderator_id, category_id, true
+        ).unwrap();
+
+    }: update_category_title(RawOrigin::Signed(caller_id), PrivilegedActor::Moderator(moderator_id), category_id, new_title.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_title_hash = T::calculate_hash(new_title.as_slice());
+
+        let new_category = Category {
+            title_hash: new_title_hash,
+            description_hash: T::calculate_hash(text.as_slice()),
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 1,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                PrivilegedActor::Moderator(moderator_id)
+            ).into()
+        );
+    }
+
+    update_category_description_lead{
+        let lead_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(lead_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_description = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+
+    }: update_category_description(RawOrigin::Signed(caller_id), PrivilegedActor::Lead, category_id, new_description.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_description_hash = T::calculate_hash(new_description.as_slice());
+
+        let new_category = Category {
+            title_hash: T::calculate_hash(text.as_slice()),
+            description_hash: new_description_hash,
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 0,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                PrivilegedActor::Lead
+            ).into()
+        );
+    }
+
+    update_category_description_moderator{
+        let moderator_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(moderator_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_description = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+        let moderator_id = ModeratorId::<T>::from(moderator_id.try_into().unwrap());
+
+        // Set up category membership of moderator.
+        Module::<T>::update_category_membership_of_moderator(
+            RawOrigin::Signed(caller_id.clone()).into(), moderator_id, category_id, true
+        ).unwrap();
+
+    }: update_category_description(RawOrigin::Signed(caller_id), PrivilegedActor::Moderator(moderator_id), category_id, new_description.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_description_hash = T::calculate_hash(new_description.as_slice());
+
+        let new_category = Category {
+            title_hash: T::calculate_hash(text.as_slice()),
+            description_hash: new_description_hash,
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 1,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                PrivilegedActor::Moderator(moderator_id)
+            ).into()
+        );
+    }
+
     delete_category_lead {
 
         let lead_id = 0;
@@ -1833,6 +2015,36 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_update_category_title_lead() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_title_lead::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_title_moderator() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_title_moderator::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_description_lead() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_description_lead::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_description_moderator() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_description_moderator::<
+                Runtime,
+            >());
+        });
+    }
+
     #[test]
     fn test_delete_posts() {
         with_test_externalities(|| {

+ 115 - 3
runtime-modules/forum/src/lib.rs

@@ -61,6 +61,10 @@ pub trait WeightInfo {
     fn update_category_membership_of_moderator_old() -> Weight;
     fn update_category_archival_status_lead(i: u32) -> Weight;
     fn update_category_archival_status_moderator(i: u32) -> Weight;
+    fn update_category_title_lead(i: u32, j: u32) -> Weight;
+    fn update_category_title_moderator(i: u32, j: u32) -> Weight;
+    fn update_category_description_lead(i: u32, j: u32) -> Weight;
+    fn update_category_description_moderator(i: u32, j: u32) -> Weight;
     fn delete_category_lead(i: u32) -> Weight;
     fn delete_category_moderator(i: u32) -> Weight;
     fn create_thread(j: u32, k: u32, i: u32) -> Weight;
@@ -469,6 +473,7 @@ decl_event!(
         ModeratorId = ModeratorId<T>,
         <T as Trait>::ThreadId,
         <T as Trait>::PostId,
+        <T as frame_system::Trait>::Hash,
         ForumUserId = ForumUserId<T>,
         <T as Trait>::PostReactionId,
         PrivilegedActor = PrivilegedActor<T>,
@@ -477,9 +482,17 @@ decl_event!(
         /// A category was introduced
         CategoryCreated(CategoryId, Option<CategoryId>, Vec<u8>, Vec<u8>),
 
-        /// A category with given id was updated.
+        /// An arhical status of category with given id was updated.
         /// The second argument reflects the new archival status of the category.
-        CategoryUpdated(CategoryId, bool, PrivilegedActor),
+        CategoryArchivalStatusUpdated(CategoryId, bool, PrivilegedActor),
+
+        /// A title of category with given id was updated.
+        /// The second argument reflects the new title hash of the category.
+        CategoryTitleUpdated(CategoryId, Hash, PrivilegedActor),
+
+        /// A discription of category with given id was updated.
+        /// The second argument reflects the new description hash of the category.
+        CategoryDescriptionUpdated(CategoryId, Hash, PrivilegedActor),
 
         /// A category was deleted
         CategoryDeleted(CategoryId, PrivilegedActor),
@@ -578,6 +591,7 @@ decl_module! {
             Ok(())
         }
 
+
         /// Add a new category.
         ///
         /// <weight>
@@ -687,7 +701,105 @@ decl_module! {
 
             // Generate event
             Self::deposit_event(
-                RawEvent::CategoryUpdated(category_id, new_archival_status, actor)
+                RawEvent::CategoryArchivalStatusUpdated(category_id, new_archival_status, actor)
+            );
+
+            Ok(())
+        }
+
+        /// Update category title
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (W + V)` where:
+        /// - `W` is the category depth
+        /// - `V` is the length of the category title.
+        /// - DB:
+        ///    - O(W)
+        /// # </weight>
+        #[weight = WeightInfoForum::<T>::update_category_title_lead(
+            T::MaxCategoryDepth::get() as u32,
+            title.len().saturated_into(),
+        ).max(WeightInfoForum::<T>::update_category_title_moderator(
+            T::MaxCategoryDepth::get() as u32,
+            title.len().saturated_into(),
+        ))]
+        fn update_category_title(origin, actor: PrivilegedActor<T>, category_id: T::CategoryId, title: Vec<u8>) -> DispatchResult {
+            // Ensure data migration is done
+            Self::ensure_data_migration_done()?;
+
+            let account_id = ensure_signed(origin)?;
+
+            // Ensure actor can update category
+            let category = Self::ensure_can_moderate_category(&account_id, &actor, &category_id)?;
+
+            let title_hash = T::calculate_hash(title.as_slice());
+
+            // No change, invalid transaction
+            if title_hash == category.title_hash {
+                return Err(Error::<T>::CategoryNotBeingUpdated.into())
+            }
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Mutate category, and set possible new change parameters
+            <CategoryById<T>>::mutate(category_id, |c| c.title_hash = title_hash);
+
+            // Generate event
+            Self::deposit_event(
+                RawEvent::CategoryTitleUpdated(category_id, title_hash, actor)
+            );
+
+            Ok(())
+        }
+
+        /// Update category description
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (W)` where:
+        /// - `W` is the category depth
+        /// - `V` is the length of the category description.
+        /// - DB:
+        ///    - O(W)
+        /// # </weight>
+        #[weight = WeightInfoForum::<T>::update_category_description_lead(
+            T::MaxCategoryDepth::get() as u32,
+            description.len().saturated_into(),
+        ).max(WeightInfoForum::<T>::update_category_description_moderator(
+            T::MaxCategoryDepth::get() as u32,
+            description.len().saturated_into(),
+        ))]
+        fn update_category_description(origin, actor: PrivilegedActor<T>, category_id: T::CategoryId, description: Vec<u8>) -> DispatchResult {
+            // Ensure data migration is done
+            Self::ensure_data_migration_done()?;
+
+            let account_id = ensure_signed(origin)?;
+
+            // Ensure actor can update category
+            let category = Self::ensure_can_moderate_category(&account_id, &actor, &category_id)?;
+
+            let description_hash = T::calculate_hash(description.as_slice());
+
+            // No change, invalid transaction
+            if description_hash == category.description_hash {
+                return Err(Error::<T>::CategoryNotBeingUpdated.into())
+            }
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Mutate category, and set possible new change parameters
+            <CategoryById<T>>::mutate(category_id, |c| c.description_hash = description_hash);
+
+            // Generate event
+            Self::deposit_event(
+                RawEvent::CategoryDescriptionUpdated(category_id, description_hash, actor)
             );
 
             Ok(())

+ 81 - 3
runtime-modules/forum/src/mock.rs

@@ -435,6 +435,18 @@ impl WeightInfo for () {
     fn update_category_archival_status_moderator(_: u32) -> Weight {
         0
     }
+    fn update_category_title_lead(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_title_moderator(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_description_lead(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_description_moderator(_: u32, _: u32) -> Weight {
+        0
+    }
     fn delete_category_lead(_: u32) -> Weight {
         0
     }
@@ -540,10 +552,18 @@ pub fn good_category_title() -> Vec<u8> {
     b"Great new category".to_vec()
 }
 
+pub fn good_category_title_new() -> Vec<u8> {
+    b"Great new category title".to_vec()
+}
+
 pub fn good_category_description() -> Vec<u8> {
     b"This is a great new category for the forum".to_vec()
 }
 
+pub fn good_category_description_new() -> Vec<u8> {
+    b"This is a great new category description for the forum".to_vec()
+}
+
 pub fn good_thread_title() -> Vec<u8> {
     b"Great new thread".to_vec()
 }
@@ -1072,7 +1092,7 @@ pub fn update_category_archival_status_mock(
     if result.is_ok() {
         assert_eq!(
             System::events().last().unwrap().event,
-            TestEvent::forum_mod(RawEvent::CategoryUpdated(
+            TestEvent::forum_mod(RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 actor
@@ -1081,12 +1101,70 @@ pub fn update_category_archival_status_mock(
     }
 }
 
+pub fn update_category_title_mock(
+    origin: OriginType,
+    actor: PrivilegedActor<Runtime>,
+    category_id: <Runtime as Trait>::CategoryId,
+    new_title: Vec<u8>,
+    result: DispatchResult,
+) {
+    let new_title_hash = Runtime::calculate_hash(new_title.as_slice());
+    assert_eq!(
+        TestForumModule::update_category_title(
+            mock_origin(origin),
+            actor.clone(),
+            category_id,
+            new_title
+        ),
+        result
+    );
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            TestEvent::forum_mod(RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                actor
+            ))
+        );
+    }
+}
+
+pub fn update_category_description_mock(
+    origin: OriginType,
+    actor: PrivilegedActor<Runtime>,
+    category_id: <Runtime as Trait>::CategoryId,
+    new_description: Vec<u8>,
+    result: DispatchResult,
+) {
+    let new_description_hash = Runtime::calculate_hash(new_description.as_slice());
+    assert_eq!(
+        TestForumModule::update_category_description(
+            mock_origin(origin),
+            actor.clone(),
+            category_id,
+            new_description
+        ),
+        result
+    );
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            TestEvent::forum_mod(RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                actor
+            ))
+        );
+    }
+}
+
 pub fn delete_category_mock(
     origin: OriginType,
     moderator_id: PrivilegedActor<Runtime>,
     category_id: <Runtime as Trait>::CategoryId,
     result: DispatchResult,
-) -> () {
+) {
     assert_eq!(
         TestForumModule::delete_category(mock_origin(origin), moderator_id.clone(), category_id),
         result,
@@ -1097,7 +1175,7 @@ pub fn delete_category_mock(
             System::events().last().unwrap().event,
             TestEvent::forum_mod(RawEvent::CategoryDeleted(category_id, moderator_id))
         );
-    };
+    }
 }
 
 pub fn moderate_thread_mock(

+ 254 - 0
runtime-modules/forum/src/tests.rs

@@ -415,6 +415,260 @@ fn update_category_archival_status_lock_works() {
     });
 }
 
+#[test]
+// test if category updator is forum lead
+fn update_category_description_origin() {
+    let origins = [FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![Ok(()), Err(Error::<Runtime>::OriginNotForumLead.into())];
+
+    for index in 0..origins.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+        with_test_externalities(|| {
+            let category_id = create_category_mock(
+                origin,
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_description_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                good_category_description_new(),
+                results[index],
+            );
+        });
+    }
+}
+
+#[test]
+// test case for new setting actually not update category description
+fn update_category_description_no_change() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin,
+            PrivilegedActor::Lead,
+            category_id,
+            good_category_description(),
+            Err(Error::<Runtime>::CategoryNotBeingUpdated.into()),
+        );
+    });
+}
+
+#[test]
+// test case for editing nonexistent category
+fn update_category_description_does_not_exist() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            1,
+            good_category_description_new(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            2,
+            good_category_description_new(),
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+        );
+    });
+}
+
+#[test]
+// test if moderator can update category description
+fn update_category_description_moderator() {
+    let moderators = [FORUM_MODERATOR_ORIGIN_ID];
+    let origins = [FORUM_MODERATOR_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+
+        // unprivileged moderator will fail to update category
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_description_new(),
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
+        );
+
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
+
+        // moderator associated with category will succeed
+        update_category_description_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_description_new(),
+            Ok(()),
+        );
+    });
+}
+
+#[test]
+// test if category updator is forum lead
+fn update_category_title_origin() {
+    let origins = [FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![Ok(()), Err(Error::<Runtime>::OriginNotForumLead.into())];
+
+    for index in 0..origins.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+        with_test_externalities(|| {
+            let category_id = create_category_mock(
+                origin,
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_title_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                good_category_title_new(),
+                results[index],
+            );
+        });
+    }
+}
+
+#[test]
+// test case for new setting actually not update category title
+fn update_category_title_no_change() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin,
+            PrivilegedActor::Lead,
+            category_id,
+            good_category_title(),
+            Err(Error::<Runtime>::CategoryNotBeingUpdated.into()),
+        );
+    });
+}
+
+#[test]
+// test case for editing nonexistent category
+fn update_category_title_does_not_exist() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            1,
+            good_category_title_new(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            2,
+            good_category_title_new(),
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+        );
+    });
+}
+
+#[test]
+// test if moderator can update category title
+fn update_category_title_moderator() {
+    let moderators = [FORUM_MODERATOR_ORIGIN_ID];
+    let origins = [FORUM_MODERATOR_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+
+        // unprivileged moderator will fail to update category
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_title_new(),
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
+        );
+
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
+
+        // moderator associated with category will succeed
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_title_new(),
+            Ok(()),
+        );
+    });
+}
+
 #[test]
 // test category can be deleted
 fn delete_category() {

+ 36 - 0
runtime/src/weights/forum.rs

@@ -183,4 +183,40 @@ impl forum::WeightInfo for WeightInfo {
             .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
             .saturating_add(DbWeight::get().writes(1 as Weight))
     }
+    fn update_category_title_lead(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_title_moderator(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_description_lead(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_description_moderator(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
 }