Browse Source

Merge pull request #1675 from iorveth/forum_v2_migration

Forum v2 migration
shamil-gadelshin 4 years ago
parent
commit
f055772429

+ 1 - 1
Cargo.lock

@@ -3868,7 +3868,7 @@ dependencies = [
 
 [[package]]
 name = "pallet-forum"
-version = "3.1.0"
+version = "4.0.0"
 dependencies = [
  "frame-support",
  "frame-system",

+ 58 - 50
node/src/chain_spec/forum_config.rs

@@ -1,21 +1,30 @@
 use codec::Decode;
 use node_runtime::{
-    common::constraints::InputValidationLengthConstraint,
-    forum::{Category, CategoryId, Post, Thread},
-    AccountId, BlockNumber, ForumConfig, Moment, PostId, ThreadId,
+    forum,
+    forum::{Category, InputValidationLengthConstraint, Post, Thread},
+    AccountId, ForumConfig, Moment, PostId, Runtime, ThreadId,
 };
 use serde::Deserialize;
+use sp_core::H256;
 use std::{fs, path::Path};
 
-fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint {
-    InputValidationLengthConstraint { min, max_min_diff }
-}
+type CategoryId = <Runtime as forum::Trait>::CategoryId;
+type ForumUserId = <Runtime as forum::Trait>::ForumUserId;
+type ModeratorId = <Runtime as forum::Trait>::ModeratorId;
+type ThreadOf = (
+    CategoryId,
+    ThreadId,
+    Thread<ForumUserId, CategoryId, Moment, H256>,
+);
 
 #[derive(Decode)]
 struct ForumData {
-    categories: Vec<Category<BlockNumber, Moment, AccountId>>,
-    posts: Vec<Post<BlockNumber, Moment, AccountId, ThreadId, PostId>>,
-    threads: Vec<Thread<BlockNumber, Moment, AccountId, ThreadId>>,
+    categories: Vec<(CategoryId, Category<CategoryId, ThreadId, H256>)>,
+    posts: Vec<(ThreadId, PostId, Post<ForumUserId, ThreadId, H256>)>,
+    threads: Vec<ThreadOf>,
+    category_by_moderator: Vec<(CategoryId, ModeratorId, ())>,
+    poll_items_constraint: InputValidationLengthConstraint,
+    data_migration_done: bool,
 }
 
 #[derive(Deserialize)]
@@ -26,6 +35,12 @@ struct EncodedForumData {
     posts: Vec<String>,
     /// hex encoded threads
     threads: Vec<String>,
+    /// hex encoded categories by moderator set
+    category_by_moderator: Vec<String>,
+    /// hex encoded poll items input validation constraint
+    poll_items_constraint: String,
+    /// hex encoded data migration done bool flag
+    data_migration_done: String,
 }
 
 impl EncodedForumData {
@@ -58,6 +73,26 @@ impl EncodedForumData {
                     Decode::decode(&mut encoded_thread.as_slice()).unwrap()
                 })
                 .collect(),
+            category_by_moderator: self
+                .category_by_moderator
+                .iter()
+                .map(|category_by_moderator| {
+                    let category_by_moderator = hex::decode(&category_by_moderator[2..].as_bytes())
+                        .expect("failed to parse thread hex string");
+                    Decode::decode(&mut category_by_moderator.as_slice()).unwrap()
+                })
+                .collect(),
+            poll_items_constraint: {
+                let poll_items_constraint =
+                    hex::decode(&self.poll_items_constraint[2..].as_bytes())
+                        .expect("failed to parse thread hex string");
+                Decode::decode(&mut poll_items_constraint.as_slice()).unwrap()
+            },
+            data_migration_done: {
+                let data_migration_done = hex::decode(&self.data_migration_done[2..].as_bytes())
+                    .expect("failed to parse thread hex string");
+                Decode::decode(&mut data_migration_done.as_slice()).unwrap()
+            },
         }
     }
 }
@@ -81,69 +116,42 @@ pub fn empty(forum_sudo: AccountId) -> ForumConfig {
         categories: vec![],
         threads: vec![],
         posts: vec![],
+        category_by_moderator: vec![],
+        poll_items_constraint: String::new(),
+        data_migration_done: String::new(),
     };
     create(forum_sudo, forum_data)
 }
 
-fn create(forum_sudo: AccountId, forum_data: EncodedForumData) -> ForumConfig {
+fn create(_forum_sudo: AccountId, forum_data: EncodedForumData) -> ForumConfig {
     let first_id = 1;
     let forum_data = forum_data.decode();
 
-    let next_category_id: CategoryId = forum_data
-        .categories
-        .last()
-        .map_or(first_id, |category| category.id + 1);
+    let next_category_id = first_id + forum_data.categories.len() as CategoryId;
 
     assert_eq!(
         next_category_id,
         (forum_data.categories.len() + 1) as CategoryId
     );
 
-    let next_thread_id: ThreadId = forum_data
-        .threads
-        .last()
-        .map_or(first_id, |thread| thread.id + 1);
+    let next_thread_id = first_id + forum_data.threads.len() as ThreadId;
 
     assert_eq!(next_thread_id, (forum_data.threads.len() + 1) as ThreadId);
 
-    let next_post_id: PostId = forum_data.posts.last().map_or(first_id, |post| post.id + 1);
+    let next_post_id = first_id + forum_data.posts.len() as PostId;
 
     assert_eq!(next_post_id, (forum_data.posts.len() + 1) as PostId);
 
     ForumConfig {
-        category_by_id: forum_data
-            .categories
-            .into_iter()
-            .map(|encoded_category| {
-                let category = encoded_category;
-                (category.id, category)
-            })
-            .collect(),
-        thread_by_id: forum_data
-            .threads
-            .into_iter()
-            .map(|encoded_thread| {
-                let thread = encoded_thread;
-                (thread.id, thread)
-            })
-            .collect(),
-        post_by_id: forum_data
-            .posts
-            .into_iter()
-            .map(|encoded_post| {
-                let post = encoded_post;
-                (post.id, post)
-            })
-            .collect(),
+        category_by_id: forum_data.categories,
+        thread_by_id: forum_data.threads,
+        post_by_id: forum_data.posts,
+        category_by_moderator: forum_data.category_by_moderator,
+        poll_items_constraint: forum_data.poll_items_constraint,
         next_category_id,
         next_thread_id,
         next_post_id,
-        forum_sudo,
-        category_title_constraint: new_validation(10, 90),
-        category_description_constraint: new_validation(10, 490),
-        thread_title_constraint: new_validation(10, 90),
-        post_text_constraint: new_validation(10, 2990),
-        thread_moderation_rationale_constraint: new_validation(10, 290),
-        post_moderation_rationale_constraint: new_validation(10, 290),
+        category_counter: next_category_id - 1,
+        data_migration_done: forum_data.data_migration_done,
     }
 }

+ 11 - 4
node/src/chain_spec/mod.rs

@@ -32,10 +32,10 @@ use node_runtime::{
     membership, wasm_binary_unwrap, AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig,
     ContentDirectoryConfig, ContentDirectoryWorkingGroupConfig, ContentWorkingGroupConfig,
     CouncilConfig, CouncilElectionConfig, DataDirectoryConfig, DataObjectStorageRegistryConfig,
-    DataObjectTypeRegistryConfig, ElectionParameters, ForumConfig, GrandpaConfig, ImOnlineConfig,
-    MembersConfig, Moment, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig,
-    StorageWorkingGroupConfig, SudoConfig, SystemConfig, VersionedStoreConfig,
-    VersionedStorePermissionsConfig, DAYS,
+    DataObjectTypeRegistryConfig, ElectionParameters, ForumConfig, ForumWorkingGroupConfig,
+    GrandpaConfig, ImOnlineConfig, MembersConfig, Moment, SessionConfig, SessionKeys, Signature,
+    StakerStatus, StakingConfig, StorageWorkingGroupConfig, SudoConfig, SystemConfig,
+    VersionedStoreConfig, VersionedStorePermissionsConfig, DAYS,
 };
 
 // Exported to be used by chain-spec-builder
@@ -310,6 +310,13 @@ pub fn testnet_genesis(
         data_object_storage_registry: Some(DataObjectStorageRegistryConfig {
             first_relationship_id: 1,
         }),
+        working_group_Instance1: Some(ForumWorkingGroupConfig {
+            phantom: Default::default(),
+            working_group_mint_capacity: 0,
+            opening_human_readable_text_constraint: default_text_constraint,
+            worker_application_human_readable_text_constraint: default_text_constraint,
+            worker_exit_rationale_text_constraint: default_text_constraint,
+        }),
         working_group_Instance2: Some(StorageWorkingGroupConfig {
             phantom: Default::default(),
             working_group_mint_capacity: 0,

+ 2 - 4
runtime-modules/common/src/working_group.rs

@@ -8,10 +8,8 @@ use strum_macros::EnumIter;
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, EnumIter))]
 #[derive(Encode, Decode, Clone, PartialEq, Eq, Copy, Debug)]
 pub enum WorkingGroup {
-    /* Reserved
-        /// Forum working group: working_group::Instance1.
-        Forum,
-    */
+    /// Forum working group: working_group::Instance1.
+    Forum,
     /// Storage working group: working_group::Instance2.
     Storage,
     /// Storage working group: working_group::Instance3.

+ 3 - 2
runtime-modules/forum/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'pallet-forum'
-version = '3.1.0'
+version = '4.0.0'
 authors = ['Joystream contributors']
 edition = '2018'
 
@@ -14,9 +14,9 @@ sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://
 sp-std = { package = 'sp-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 common = { package = 'pallet-common', default-features = false, path = '../common'}
+sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 
 [dev-dependencies]
-sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 
 [features]
@@ -29,6 +29,7 @@ std = [
 	'sp-std/std',
 	'sp-arithmetic/std',
 	'sp-runtime/std',
+	'sp-io/std',
 	'pallet-timestamp/std',
 	'common/std',
 ]

File diff suppressed because it is too large
+ 1042 - 617
runtime-modules/forum/src/lib.rs


File diff suppressed because it is too large
+ 580 - 353
runtime-modules/forum/src/mock.rs


+ 1653 - 747
runtime-modules/forum/src/tests.rs

@@ -2,980 +2,1886 @@
 
 use super::*;
 use crate::mock::*;
+use frame_support::assert_err;
 
-use frame_support::{assert_err, assert_ok};
+/// test cases are arranged as two layers.
+/// first layer is each method in defined in module.
+/// second layer is each parameter of the specific method.
 
 /*
-* NB!: No test checks for event emission!!!!
-*/
-
-/*
- * set_forum_sudo
- * ==============================================================================
- *
- * Missing cases
- *
- * set_forum_bad_origin
- *
+ * update_category_membership_of_moderator_origin
  */
-
 #[test]
-fn set_forum_sudo_unset() {
-    let config = default_genesis_config();
-
-    build_test_externalities(config).execute_with(|| {
-        // Ensure that forum sudo is default
-        assert_eq!(TestForumModule::forum_sudo(), Some(33));
-
-        // Unset forum sudo
-        assert_ok!(TestForumModule::set_forum_sudo(
-            mock_origin(OriginType::Root),
-            None
-        ));
-
-        // Sudo no longer set
-        assert!(TestForumModule::forum_sudo().is_none());
-
-        // event emitted?!
+// test case for check if origin is forum lead
+fn update_category_membership_of_moderator_origin() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            origin,
+            moderator_id,
+            category_id,
+            true,
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            NOT_FORUM_LEAD_ORIGIN,
+            moderator_id,
+            category_id,
+            true,
+            Err(Error::<Runtime>::OriginNotForumLead.into()),
+        );
     });
 }
 
 #[test]
-fn set_forum_sudo_update() {
-    let config = default_genesis_config();
-
-    build_test_externalities(config).execute_with(|| {
-        // Ensure that forum sudo is default
-        assert_eq!(
-            TestForumModule::forum_sudo(),
-            Some(default_genesis_config().forum_sudo)
+// test case for check whether category is existed.
+fn update_category_membership_of_moderator_category() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
         );
-
-        let new_forum_sudo_account_id = 780;
-
-        // Unset forum sudo
-        assert_ok!(TestForumModule::set_forum_sudo(
-            mock_origin(OriginType::Root),
-            Some(new_forum_sudo_account_id)
-        ));
-
-        // Sudo no longer set
-        assert_eq!(
-            TestForumModule::forum_sudo(),
-            Some(new_forum_sudo_account_id)
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderator_id,
+            category_id,
+            true,
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderator_id,
+            INVLAID_CATEGORY_ID,
+            true,
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
         );
     });
 }
 
-/*
- * create_category
- * ==============================================================================
- *
- * Missing cases
- *
- * create_category_bad_origin
- * create_category_forum_sudo_not_set
- */
-
 #[test]
-fn create_root_category_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        assert_create_category(origin, None, Ok(()));
-    });
+// test case for check if origin is forum lead
+fn create_category_origin() {
+    let origins = vec![FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![Ok(()), Err(Error::<Runtime>::OriginNotForumLead.into())];
+    for index in 0..origins.len() {
+        with_test_externalities(|| {
+            create_category_mock(
+                origins[index].clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                results[index].clone(),
+            );
+        });
+    }
 }
 
 #[test]
-fn create_subcategory_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test case for check if parent category is archived or not existing.
+fn create_category_parent() {
+    let parents = vec![Some(1), Some(2), Some(3)];
+    let results = vec![
+        Ok(()),
+        Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
+        Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+    ];
 
-    build_test_externalities(config).execute_with(|| {
-        let root_category_id = create_root_category(origin.clone());
-        assert_create_category(origin, Some(root_category_id), Ok(()));
-    });
+    for index in 0..parents.len() {
+        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(()),
+            );
+            create_category_mock(
+                origin.clone(),
+                Some(1),
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_archival_status_mock(
+                origin.clone(),
+                PrivilegedActor::Lead,
+                2,
+                true,
+                Ok(()),
+            );
+
+            create_category_mock(
+                origin.clone(),
+                parents[index],
+                good_category_title(),
+                good_category_description(),
+                results[index],
+            );
+        });
+    }
 }
 
 #[test]
-fn create_category_title_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.category_title_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        CreateCategoryFixture {
-            origin,
-            parent: None,
-            title: generate_text(min_len - 1),
-            description: good_category_description(),
-            result: Err(ERROR_CATEGORY_TITLE_TOO_SHORT),
+// test case set category depth
+fn create_category_depth() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let max_depth = <Runtime as Trait>::MaxCategoryDepth::get();
+        for i in 0..(max_depth + 1) {
+            let parent_category_id = match i {
+                0 => None,
+                _ => Some(i),
+            };
+            let expected_result = match i {
+                _ if i >= max_depth => Err(Error::<Runtime>::MaxValidCategoryDepthExceeded.into()),
+                _ => Ok(()),
+            };
+
+            create_category_mock(
+                origin.clone(),
+                parent_category_id,
+                good_category_title(),
+                good_category_description(),
+                expected_result,
+            );
         }
-        .call_and_assert();
     });
 }
 
+/*
+ ** update_category
+ */
 #[test]
-fn create_category_title_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.category_title_constraint.max() as usize;
+// test if category updator is forum lead
+fn update_category_archival_status_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_archival_status_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                true,
+                results[index],
+            );
+        });
+    }
+}
 
-    build_test_externalities(config).execute_with(|| {
-        CreateCategoryFixture {
+#[test]
+// test case for new setting actually not update category status
+fn update_category_archival_status_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_archival_status_mock(
             origin,
-            parent: None,
-            title: generate_text(max_len + 1),
-            description: good_category_description(),
-            result: Err(ERROR_CATEGORY_TITLE_TOO_LONG),
-        }
-        .call_and_assert();
+            PrivilegedActor::Lead,
+            category_id,
+            false,
+            Err(Error::<Runtime>::CategoryNotBeingUpdated.into()),
+        );
     });
 }
 
 #[test]
-fn create_category_description_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.category_description_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        CreateCategoryFixture {
-            origin,
-            parent: None,
-            title: good_category_title(),
-            description: generate_text(min_len - 1),
-            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_SHORT),
-        }
-        .call_and_assert();
+// test case for editing nonexistent category
+fn update_category_archival_status_category_exists() {
+    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_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            1,
+            true,
+            Ok(()),
+        );
+        update_category_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            2,
+            true,
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+        );
     });
 }
 
 #[test]
-fn create_category_description_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.category_description_constraint.max() as usize;
+// test if moderator can archive category
+fn update_category_archival_status_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(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        CreateCategoryFixture {
-            origin,
-            parent: None,
-            title: good_category_title(),
-            description: generate_text(max_len + 1),
-            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_LONG),
-        }
-        .call_and_assert();
+        // unprivileged moderator will fail to update category
+        update_category_archival_status_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            true,
+            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_archival_status_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            true,
+            Ok(()),
+        );
     });
 }
 
-/*
- * update_category
- * ==============================================================================
- *
- * Missing cases
- *
- * create_category_bad_origin
- * create_category_forum_sudo_not_set
- * create_category_origin_not_forum_sudo
- * create_category_immutable_ancestor_category
- */
-
 #[test]
-fn update_category_undelete_and_unarchive() {
-    /*
-     * Create an initial state with two levels of categories, where
-     * leaf category is deleted, and then try to undelete.
-     */
+// test if moderator can archive category
+fn update_category_archival_status_lock_works() {
+    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(()),
+        );
 
-    let forum_sudo = 32;
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-    let created_at = RuntimeBlockchainTimestamp { block: 0, time: 0 };
+        let post_id = create_post_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_post_text(),
+            Ok(()),
+        );
 
-    let category_by_id = vec![
-        // A root category
-        (
+        update_category_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
             1,
-            Category {
-                id: 1,
-                title: "New root".as_bytes().to_vec(),
-                description: "This is a new root category".as_bytes().to_vec(),
-                created_at: created_at.clone(),
-                deleted: false,
-                archived: false,
-                num_direct_subcategories: 1,
-                num_direct_unmoderated_threads: 0,
-                num_direct_moderated_threads: 0,
-                position_in_parent_category: None,
-                moderator_id: forum_sudo,
-            },
-        ),
-        // A subcategory of the one above
-        (
-            2,
-            Category {
-                id: 2,
-                title: "New subcategory".as_bytes().to_vec(),
-                description: "This is a new subcategory to root category"
-                    .as_bytes()
-                    .to_vec(),
-                created_at: created_at.clone(),
-                deleted: true,
-                archived: false,
-                num_direct_subcategories: 0,
-                num_direct_unmoderated_threads: 0,
-                num_direct_moderated_threads: 0,
-                position_in_parent_category: Some(ChildPositionInParentCategory {
-                    parent_id: 1,
-                    child_nr_in_parent_category: 1,
-                }),
-                moderator_id: forum_sudo,
-            },
-        ),
-    ];
-
-    // Set constraints to be sloppy, we don't care about enforcing them.
-    let sloppy_constraint = InputValidationLengthConstraint {
-        min: 0,
-        max_min_diff: 1000,
-    };
-
-    let config = genesis_config(
-        &category_by_id,             // category_by_id
-        category_by_id.len() as u64, // next_category_id
-        &vec![],                     // thread_by_id
-        1,                           // next_thread_id
-        &vec![],                     // post_by_id
-        1,                           // next_post_id
-        forum_sudo,
-        &sloppy_constraint,
-        &sloppy_constraint,
-        &sloppy_constraint,
-        &sloppy_constraint,
-        &sloppy_constraint,
-        &sloppy_constraint,
-    );
-
-    build_test_externalities(config).execute_with(|| {
-        UpdateCategoryFixture {
-            origin: OriginType::Signed(forum_sudo),
-            category_id: 2,
-            new_archival_status: None,        // same as before
-            new_deletion_status: Some(false), // undelete
-            result: Ok(()),
-        }
-        .call_and_assert();
-    });
-}
+            true,
+            Ok(()),
+        );
 
-/*
- * create_thread
- * ==============================================================================
- *
- * Missing cases
- *
- * create_thread_bad_origin
- * create_thread_forum_sudo_not_set
- * ...
- */
+        // can't add more threads
+        create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
+        );
 
-#[test]
-fn create_thread_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // can't add more posts to thread inside category
+        create_post_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_post_text(),
+            Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(origin);
-        let member_origin = create_forum_member();
+        // can't update post
+        edit_post_text_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            post_id,
+            good_post_new_text(),
+            Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
+        );
 
-        CreateThreadFixture {
-            origin: member_origin,
+        // can't update thread
+        edit_thread_title_mock(
+            origin.clone(),
+            forum_lead,
             category_id,
-            title: good_thread_title(),
-            text: good_thread_text(),
-            result: Ok(()),
-        }
-        .call_and_assert();
+            thread_id,
+            good_thread_new_title(),
+            Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
+        );
     });
 }
 
 #[test]
-fn create_thread_title_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.thread_title_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(origin);
-        let member_origin = create_forum_member();
-
-        CreateThreadFixture {
-            origin: member_origin,
-            category_id,
-            title: generate_text(min_len - 1),
-            text: good_thread_text(),
-            result: Err(ERROR_THREAD_TITLE_TOO_SHORT),
-        }
-        .call_and_assert();
+// test category can be deleted
+fn delete_category() {
+    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(()),
+        );
+        assert!(<CategoryById<Runtime>>::contains_key(category_id));
+        delete_category_mock(origin.clone(), PrivilegedActor::Lead, category_id, Ok(()));
+        assert!(!<CategoryById<Runtime>>::contains_key(category_id));
     });
 }
 
 #[test]
-fn create_thread_title_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.thread_title_constraint.max() as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(origin);
-        let member_origin = create_forum_member();
-
-        CreateThreadFixture {
-            origin: member_origin,
+// test category can't be deleted when it has subcategories
+fn delete_category_non_empty_subcategories() {
+    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(()),
+        );
+        create_category_mock(
+            origin.clone(),
+            Some(category_id),
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        delete_category_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
             category_id,
-            title: generate_text(max_len + 1),
-            text: good_thread_text(),
-            result: Err(ERROR_THREAD_TITLE_TOO_LONG),
-        }
-        .call_and_assert();
+            Err(Error::<Runtime>::CategoryNotEmptyCategories.into()),
+        );
     });
 }
 
 #[test]
-fn create_thread_text_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.post_text_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(origin);
-        let member_origin = create_forum_member();
+// test category can't be deleted when it contains threads
+fn delete_category_non_empty_threads() {
+    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(()),
+        );
+        create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-        CreateThreadFixture {
-            origin: member_origin,
+        delete_category_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
             category_id,
-            title: good_thread_title(),
-            text: generate_text(min_len - 1),
-            result: Err(ERROR_POST_TEXT_TOO_SHORT),
-        }
-        .call_and_assert();
+            Err(Error::<Runtime>::CategoryNotEmptyThreads.into()),
+        );
     });
 }
 
 #[test]
-fn create_thread_text_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.post_text_constraint.max() as usize;
+// test category can't be deleted by moderator only if he is moderating one of parent categories
+fn delete_category_need_ancestor_moderation() {
+    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_1 = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        let category_id_2 = create_category_mock(
+            origin.clone(),
+            Some(category_id_1),
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(origin);
-        let member_origin = create_forum_member();
+        // without any permissions, moderator can't delete category
+        delete_category_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id_2,
+            Err(Error::<Runtime>::ModeratorCantDeleteCategory.into()),
+        );
 
-        CreateThreadFixture {
-            origin: member_origin,
-            category_id,
-            title: good_thread_title(),
-            text: generate_text(max_len + 1),
-            result: Err(ERROR_POST_TEXT_TOO_LONG),
-        }
-        .call_and_assert();
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id_2,
+            true,
+            Ok(()),
+        );
+
+        // without permissions to moderate only category itself, moderator can't delete category
+        delete_category_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id_2,
+            Err(Error::<Runtime>::ModeratorCantDeleteCategory.into()),
+        );
+
+        // give permision to moderate parent category
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id_1,
+            true,
+            Ok(()),
+        );
+
+        // check number of subcategories is correct
+        assert_eq!(
+            <CategoryById<Runtime>>::get(category_id_1).num_direct_subcategories,
+            1,
+        );
+
+        // with permissions to moderate parent category, delete will work
+        delete_category_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id_2,
+            Ok(()),
+        );
+
+        // check that subcategory count was decreased
+        assert_eq!(
+            <CategoryById<Runtime>>::get(category_id_1).num_direct_subcategories,
+            0,
+        );
     });
 }
 
 #[test]
-fn create_post_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test if lead can delete root category
+fn delete_category_root_by_lead() {
+    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(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, _) = create_root_category_and_thread_and_post(origin);
+        delete_category_mock(origin.clone(), PrivilegedActor::Lead, category_id, Ok(()));
     });
 }
 
+/*
+ ** create_thread
+ */
 #[test]
-fn create_post_text_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.post_text_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+// test if thread creator is valid forum user
+fn create_thread_origin() {
+    let origins = [NOT_FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_2_ORIGIN];
+    let forum_user_id = NOT_FORUM_LEAD_ORIGIN_ID;
+    let results = vec![
+        Ok(()),
+        Err(Error::<Runtime>::ForumUserIdNotMatchAccount.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(()),
+            );
+            create_thread_mock(
+                origins[index].clone(),
+                forum_user_id,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                results[index],
+            );
+        });
+    }
+}
 
-        CreatePostFixture {
-            origin: member_origin,
-            thread_id,
-            text: generate_text(min_len - 1),
-            result: Err(ERROR_POST_TEXT_TOO_SHORT),
-        }
-        .call_and_assert();
-    });
+#[test]
+// test if timestamp of poll start time and end time are valid
+fn create_thread_poll_timestamp() {
+    let expiration_diff = 10;
+    let results = vec![Ok(()), Err(Error::<Runtime>::PollTimeSetting.into())];
+
+    for index in 0..results.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+
+        with_test_externalities(|| {
+            change_current_time(1);
+            let poll = generate_poll_timestamp_cases(index, expiration_diff);
+            change_current_time(index as u64 * expiration_diff + 1);
+
+            let category_id = create_category_mock(
+                origin.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+
+            create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                Some(poll),
+                results[index],
+            );
+        });
+    }
 }
 
 #[test]
-fn create_post_text_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.post_text_constraint.max() as usize;
+// test if author can edit thread's title
+fn edit_thread_title() {
+    let forum_users = [NOT_FORUM_LEAD_ORIGIN_ID, NOT_FORUM_LEAD_2_ORIGIN_ID];
+    let origins = [NOT_FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_2_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(()),
+        );
+        // create thread by author
+        let thread_id = create_thread_mock(
+            origins[0].clone(),
+            forum_users[0],
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+        // check author can edit text
+        edit_thread_title_mock(
+            origins[0].clone(),
+            forum_users[0],
+            category_id,
+            thread_id,
+            good_thread_new_title(),
+            Ok(()),
+        );
 
-        CreatePostFixture {
-            origin: member_origin,
+        // check non-author is forbidden from editing text
+        edit_thread_title_mock(
+            origins[1].clone(),
+            forum_users[1],
+            category_id,
             thread_id,
-            text: generate_text(max_len + 1),
-            result: Err(ERROR_POST_TEXT_TOO_LONG),
-        }
-        .call_and_assert();
+            good_thread_new_title(),
+            Err(Error::<Runtime>::AccountDoesNotMatchThreadAuthor.into()),
+        );
     });
 }
 
-// Test moderation:
-// -----------------------------------------------------------------------------
-
+/*
+ ** update_category
+ */
 #[test]
-fn moderate_thread_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
-        assert_eq!(moderate_thread(origin, thread_id, good_rationale()), Ok(()));
-    });
+// test if category updator is forum lead
+fn update_thread_archival_status_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(()),
+            );
+
+            let thread_id = create_thread_mock(
+                origins[0].clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                Ok(()),
+            );
+            update_thread_archival_status_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                thread_id,
+                true,
+                results[index],
+            );
+        });
+    }
 }
 
 #[test]
-fn cannot_moderate_already_moderated_thread() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
-        assert_eq!(
-            moderate_thread(origin.clone(), thread_id.clone(), good_rationale()),
-            Ok(())
+// test case for new setting actually not update thread status
+fn update_thread_archival_status_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(()),
         );
-        assert_eq!(
-            moderate_thread(origin, thread_id, good_rationale()),
-            Err(ERROR_THREAD_ALREADY_MODERATED)
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        update_thread_archival_status_mock(
+            origin,
+            PrivilegedActor::Lead,
+            category_id,
+            thread_id,
+            false,
+            Err(Error::<Runtime>::ThreadNotBeingUpdated.into()),
         );
     });
 }
 
 #[test]
-fn moderate_thread_rationale_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.thread_moderation_rationale_constraint.min as usize;
-
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
-        let bad_rationale = generate_text(min_len - 1);
-        assert_eq!(
-            moderate_thread(origin, thread_id, bad_rationale),
-            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT)
+// test case for editing nonexistent thread
+fn update_thread_archival_status_thread_exists() {
+    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(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        update_thread_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            category_id,
+            thread_id,
+            true,
+            Ok(()),
+        );
+        update_thread_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            category_id,
+            thread_id + 1,
+            true,
+            Err(Error::<Runtime>::ThreadDoesNotExist.into()),
         );
     });
 }
 
 #[test]
-fn moderate_thread_rationale_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.thread_moderation_rationale_constraint.max() as usize;
+// test if moderator can archive thread
+fn update_thread_archival_status_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(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
-        let bad_rationale = generate_text(max_len + 1);
-        assert_eq!(
-            moderate_thread(origin, thread_id, bad_rationale),
-            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG)
+        // unprivileged moderator will fail to update category
+        update_thread_archival_status_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            thread_id,
+            true,
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
         );
-    });
-}
 
-#[test]
-fn moderate_post_successfully() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
-        assert_eq!(moderate_post(origin, post_id, good_rationale()), Ok(()));
+        // moderator associated with category will succeed
+        update_thread_archival_status_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            thread_id,
+            true,
+            Ok(()),
+        );
     });
 }
 
 #[test]
-fn moderate_post_rationale_too_short() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let min_len = config.post_moderation_rationale_constraint.min as usize;
+// test if moderator can archive thread
+fn update_thread_archival_status_lock_works() {
+    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(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
-        let bad_rationale = generate_text(min_len - 1);
-        assert_eq!(
-            moderate_post(origin, post_id, bad_rationale),
-            Err(ERROR_POST_MODERATION_RATIONALE_TOO_SHORT)
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
         );
-    });
-}
 
-#[test]
-fn moderate_post_rationale_too_long() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-    let max_len = config.post_moderation_rationale_constraint.max() as usize;
+        let post_id = create_post_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_post_text(),
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
-        let bad_rationale = generate_text(max_len + 1);
-        assert_eq!(
-            moderate_post(origin, post_id, bad_rationale),
-            Err(ERROR_POST_MODERATION_RATIONALE_TOO_LONG)
+        update_thread_archival_status_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            category_id,
+            thread_id,
+            true,
+            Ok(()),
+        );
+
+        // can't add more posts to thread inside category
+        create_post_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_post_text(),
+            Err(Error::<Runtime>::ThreadImmutable.into()),
+        );
+
+        // can't update post
+        edit_post_text_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            post_id,
+            good_post_new_text(),
+            Err(Error::<Runtime>::ThreadImmutable.into()),
+        );
+
+        // can't update thread
+        edit_thread_title_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_thread_new_title(),
+            Err(Error::<Runtime>::ThreadImmutable.into()),
         );
     });
 }
 
 #[test]
-fn cannot_moderate_already_moderated_post() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test if moderator can delete thread
+fn delete_thread() {
+    let moderators = [
+        FORUM_MODERATOR_ORIGIN_ID,
+        FORUM_MODERATOR_2_ORIGIN_ID,
+        NOT_FORUM_LEAD_ORIGIN_ID,
+    ];
+    let origins = [
+        FORUM_MODERATOR_ORIGIN,
+        FORUM_MODERATOR_2_ORIGIN,
+        NOT_FORUM_LEAD_ORIGIN,
+    ];
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
-        assert_eq!(
-            moderate_post(origin.clone(), post_id.clone(), good_rationale()),
-            Ok(())
+    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(()),
+        );
+
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+
+        let post_id = create_post_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            good_post_text(),
+            Ok(()),
+        );
+
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
         );
+
+        // check number of category's threads match before delete
         assert_eq!(
-            moderate_post(origin, post_id, good_rationale()),
-            Err(ERROR_POST_MODERATED)
+            <CategoryById<Runtime>>::get(category_id).num_direct_threads,
+            1
         );
-    });
-}
 
-// Not a forum sudo:
-// -----------------------------------------------------------------------------
+        // regular user will fail to delete the thread
+        delete_thread_mock(
+            origins[2].clone(),
+            moderators[2],
+            category_id,
+            thread_id,
+            Err(Error::<Runtime>::ModeratorIdNotMatchAccount.into()),
+        );
 
-#[test]
-fn not_forum_sudo_cannot_create_root_category() {
-    let config = default_genesis_config();
+        // moderator not associated with thread will fail to delete it
+        delete_thread_mock(
+            origins[1].clone(),
+            moderators[1],
+            category_id,
+            thread_id,
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        assert_create_category(
-            NOT_FORUM_SUDO_ORIGIN,
-            None,
-            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        // moderator will delete thread
+        delete_thread_mock(
+            origins[0].clone(),
+            moderators[0],
+            category_id,
+            thread_id,
+            Ok(()),
         );
-    });
-}
 
-#[test]
-fn not_forum_sudo_cannot_create_subcategory() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // check thread's post was deleted
+        assert!(!<PostById<Runtime>>::contains_key(thread_id, post_id));
 
-    build_test_externalities(config).execute_with(|| {
-        let root_category_id = create_root_category(origin);
-        assert_create_category(
-            NOT_FORUM_SUDO_ORIGIN,
-            Some(root_category_id),
-            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        // check category's thread count was decreased
+        assert_eq!(
+            <CategoryById<Runtime>>::get(category_id).num_direct_threads,
+            0
         );
     });
 }
 
 #[test]
-fn not_forum_sudo_cannot_archive_category() {
-    assert_not_forum_sudo_cannot_update_category(archive_category);
-}
+// test if moderator can move thread between two categories he moderates
+fn move_thread_moderator_permissions() {
+    let moderators = [FORUM_MODERATOR_ORIGIN_ID, FORUM_MODERATOR_2_ORIGIN_ID];
+    let origins = [FORUM_MODERATOR_ORIGIN, FORUM_MODERATOR_2_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id_1 = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        let category_id_2 = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
 
-#[test]
-fn not_forum_sudo_cannot_unarchive_category() {
-    assert_not_forum_sudo_cannot_update_category(unarchive_category);
-}
+        // sanity check
+        assert_ne!(category_id_1, category_id_2);
 
-#[test]
-fn not_forum_sudo_cannot_delete_category() {
-    assert_not_forum_sudo_cannot_update_category(delete_category);
-}
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id_1,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-#[test]
-fn not_forum_sudo_cannot_undelete_category() {
-    assert_not_forum_sudo_cannot_update_category(undelete_category);
-}
+        // moderator not associated with any category will fail to move thread
+        move_thread_mock(
+            origins[0].clone(),
+            moderators[0],
+            category_id_1,
+            thread_id,
+            category_id_2,
+            Err(Error::<Runtime>::ModeratorModerateOriginCategory.into()),
+        );
 
-#[test]
-fn not_forum_sudo_cannot_moderate_thread() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // set incomplete permissions for first user (only category 1)
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id_1,
+            true,
+            Ok(()),
+        );
+        // set incomplete permissions for second user (only category 2)
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[1],
+            category_id_2,
+            true,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        // moderator associated only with the first category will fail to move thread
+        move_thread_mock(
+            origins[1].clone(),
+            moderators[1],
+            category_id_1,
+            thread_id,
+            category_id_2,
+            Err(Error::<Runtime>::ModeratorModerateOriginCategory.into()),
+        );
+
+        // moderator associated only with the second category will fail to move thread
+        move_thread_mock(
+            origins[0].clone(),
+            moderators[0],
+            category_id_1,
+            thread_id,
+            category_id_2,
+            Err(Error::<Runtime>::ModeratorModerateDestinationCategory.into()),
+        );
+
+        // give the rest of necessary permissions to the first moderator
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id_2,
+            true,
+            Ok(()),
+        );
+
+        // check counters of threads in category
+        assert_eq!(
+            <CategoryById<Runtime>>::get(category_id_1).num_direct_threads,
+            1,
+        );
         assert_eq!(
-            moderate_thread(NOT_FORUM_SUDO_ORIGIN, thread_id, good_rationale()),
-            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+            <CategoryById<Runtime>>::get(category_id_2).num_direct_threads,
+            0,
         );
-    });
-}
 
-#[test]
-fn not_forum_sudo_cannot_moderate_post() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // moderator associated with both categories will succeed to move thread
+        move_thread_mock(
+            origins[0].clone(),
+            moderators[0],
+            category_id_1,
+            thread_id,
+            category_id_2,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        // check counters of threads in category
         assert_eq!(
-            moderate_post(NOT_FORUM_SUDO_ORIGIN, post_id, good_rationale()),
-            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+            <CategoryById<Runtime>>::get(category_id_1).num_direct_threads,
+            0,
+        );
+        assert_eq!(
+            <CategoryById<Runtime>>::get(category_id_2).num_direct_threads,
+            1,
         );
     });
 }
 
-// Not a member:
-// -----------------------------------------------------------------------------
-
 #[test]
-fn not_member_cannot_create_thread() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test if error is thrown when origin and destination category is the same
+fn move_thread_invalid_move() {
+    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(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        CreateThreadFixture {
-            origin: NOT_MEMBER_ORIGIN,
-            category_id: create_root_category(origin),
-            title: good_thread_title(),
-            text: good_thread_text(),
-            result: Err(ERROR_NOT_FORUM_USER),
-        }
-        .call_and_assert();
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+
+        // set permissions
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
+
+        move_thread_mock(
+            origins[0].clone(),
+            moderators[0],
+            category_id,
+            thread_id,
+            category_id,
+            Err(Error::<Runtime>::ThreadMoveInvalid.into()),
+        );
     });
 }
 
+/*
+ ** vote_on_poll
+ */
 #[test]
-fn not_member_cannot_create_post() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test if poll submitter is a forum user
+fn vote_on_poll_origin() {
+    let origins = vec![FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![
+        Ok(()),
+        Err(Error::<Runtime>::ForumUserIdNotMatchAccount.into()),
+    ];
+    let expiration_diff = 10;
+
+    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.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            let thread_id = create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                Some(generate_poll(expiration_diff)),
+                Ok(()),
+            );
+
+            vote_on_poll_mock(
+                origins[index].clone(),
+                forum_lead,
+                thread_id,
+                category_id,
+                1,
+                results[index],
+            );
+        });
+    }
+}
 
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(origin);
-        CreatePostFixture {
-            origin: NOT_MEMBER_ORIGIN,
+#[test]
+// test if poll metadata created
+fn vote_on_poll_exists() {
+    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(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        vote_on_poll_mock(
+            origin.clone(),
+            forum_lead,
             thread_id,
-            text: good_post_text(),
-            result: Err(ERROR_NOT_FORUM_USER),
-        }
-        .call_and_assert();
+            category_id,
+            1,
+            Err(Error::<Runtime>::PollNotExist.into()),
+        );
     });
 }
 
 #[test]
-fn not_member_cannot_edit_post() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin);
-        assert_err!(
-            TestForumModule::edit_post_text(
-                mock_origin(NOT_MEMBER_ORIGIN),
-                post_id,
-                good_rationale()
-            ),
-            ERROR_NOT_FORUM_USER
+// test if forum reject poll submit after expiration
+fn vote_on_poll_expired() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    let expiration_diff = 10;
+
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            Some(generate_poll(expiration_diff)),
+            Ok(()),
+        );
+        change_current_time(expiration_diff + 1);
+        vote_on_poll_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            thread_id,
+            1,
+            Err(Error::<Runtime>::PollCommitExpired.into()),
         );
     });
 }
 
-// Invalid id passed:
-// -----------------------------------------------------------------------------
+/*
+ ** moderate_thread
+ */
 
 #[test]
-fn cannot_create_subcategory_with_invalid_parent_category_id() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        assert_create_category(
+// test if thread moderator registered as valid moderator
+fn moderate_thread_origin_ok() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderator_id,
+            category_id,
+            true,
+            Ok(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        moderate_thread_mock(
             origin,
-            Some(INVLAID_CATEGORY_ID),
-            Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+            moderator_id,
+            category_id,
+            thread_id,
+            good_moderation_rationale(),
+            Ok(()),
         );
     });
 }
 
-#[test]
-fn cannot_create_thread_with_invalid_category_id() {
-    let config = default_genesis_config();
-
-    build_test_externalities(config).execute_with(|| {
-        CreateThreadFixture {
-            origin: create_forum_member(),
-            category_id: INVLAID_CATEGORY_ID,
-            title: good_thread_title(),
-            text: good_thread_text(),
-            result: Err(ERROR_CATEGORY_DOES_NOT_EXIST),
-        }
-        .call_and_assert();
-    });
-}
+/*
+ ** add_post
+ */
 
 #[test]
-fn cannot_create_post_with_invalid_thread_id() {
-    let config = default_genesis_config();
-
-    build_test_externalities(config).execute_with(|| {
-        CreatePostFixture {
-            origin: create_forum_member(),
-            thread_id: INVLAID_THREAD_ID,
-            text: good_post_text(),
-            result: Err(ERROR_THREAD_DOES_NOT_EXIST),
-        }
-        .call_and_assert();
-    });
+// test if post origin registered as forum user
+fn add_post_origin() {
+    let origins = vec![FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![
+        Ok(()),
+        Err(Error::<Runtime>::ForumUserIdNotMatchAccount.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.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+
+            let thread_id = create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                Ok(()),
+            );
+            create_post_mock(
+                origins[index].clone(),
+                forum_lead,
+                category_id,
+                thread_id,
+                good_post_text(),
+                results[index],
+            );
+        });
+    }
 }
 
 #[test]
-fn cannot_moderate_thread_with_invalid_id() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+// test if post text can be edited by author
+fn edit_post_text() {
+    let forum_users = [NOT_FORUM_LEAD_ORIGIN_ID, NOT_FORUM_LEAD_2_ORIGIN_ID];
+    let origins = [NOT_FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_2_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+
+    with_test_externalities(|| {
+        // prepare category and thread
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        assert_err!(
-            moderate_thread(origin, INVLAID_THREAD_ID, good_rationale()),
-            ERROR_THREAD_DOES_NOT_EXIST
+        // create post by author
+        let post_id = create_post_mock(
+            origins[0].clone(),
+            forum_users[0],
+            category_id,
+            thread_id,
+            good_post_text(),
+            Ok(()),
         );
-    });
-}
 
-#[test]
-fn cannot_moderate_post_with_invalid_id() {
-    let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+        // check author can edit text
+        edit_post_text_mock(
+            origins[0].clone(),
+            forum_users[0],
+            category_id,
+            thread_id,
+            post_id,
+            good_post_new_text(),
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        assert_err!(
-            moderate_post(origin, INVLAID_POST_ID, good_rationale()),
-            ERROR_POST_DOES_NOT_EXIST
+        // check non-author is forbidden from editing text
+        edit_post_text_mock(
+            origins[1].clone(),
+            forum_users[1],
+            category_id,
+            thread_id,
+            post_id,
+            good_post_new_text(),
+            Err(Error::<Runtime>::AccountDoesNotMatchPostAuthor.into()),
         );
     });
 }
 
-// Successfull extrinsics
-// -----------------------------------------------------------------------------
-
+/*
+ ** react_post
+ */
 #[test]
-fn archive_then_unarchive_category_successfully() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+// test if post react take effect
+fn react_post() {
+    // three reations to post, test them one by one.
+    let reactions = vec![0, 1, 2];
+    for index in 0..reactions.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = FORUM_LEAD_ORIGIN;
+
+        with_test_externalities(|| {
+            let category_id = create_category_mock(
+                origin.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+
+            let thread_id = create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                Ok(()),
+            );
+            let post_id = create_post_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                thread_id,
+                good_post_text(),
+                Ok(()),
+            );
+            react_post_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                thread_id,
+                post_id,
+                reactions[index],
+                Ok(()),
+            );
+        });
+    }
+}
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
-        // TODO get category by id and assert archived == true.
+/*
+ ** moderate_post
+ */
 
-        assert_ok!(unarchive_category(forum_sudo, category_id,));
-        // TODO get category by id and assert archived == false.
-    });
+#[test]
+// test if post moderator registered
+fn moderate_post_origin() {
+    let origins = vec![FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![
+        Ok(()),
+        Err(Error::<Runtime>::ModeratorIdNotMatchAccount.into()),
+    ];
+    for index in 0..origins.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+        with_test_externalities(|| {
+            let moderator_id = forum_lead;
+
+            let category_id = create_category_mock(
+                origin.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_membership_of_moderator_mock(
+                origin.clone(),
+                moderator_id,
+                category_id,
+                true,
+                Ok(()),
+            );
+
+            let thread_id = create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                Ok(()),
+            );
+            let post_id = create_post_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                thread_id,
+                good_post_text(),
+                Ok(()),
+            );
+            moderate_post_mock(
+                origins[index].clone(),
+                moderator_id,
+                category_id,
+                thread_id,
+                post_id,
+                good_moderation_rationale(),
+                results[index],
+            );
+        });
+    }
 }
 
 #[test]
-fn delete_then_undelete_category_successfully() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
-        // TODO get category by id and assert deleted == true.
-
-        assert_ok!(undelete_category(forum_sudo.clone(), category_id.clone(),));
-        // TODO get category by id and assert deleted == false.
-    });
-}
-
-// TODO Consider to fix the logic of the forum module
-// #[test]
-// fn cannot_unarchive_not_archived_category() {
-//     let config = default_genesis_config();
-//     let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-//     build_test_externalities(config).execute_with(|| {
-//         let category_id = create_root_category(forum_sudo.clone());
-
-//         // TODO bug in a logic! it should not be possible. !!!
-
-//         assert_err!(
-//             archive_category(
-//                 forum_sudo.clone(),
-//                 category_id.clone(),
-//             ),
-//             "... TODO expect error ..."
-//         );
-//     });
-// }
-
-// TODO Consider to fix the logic of the forum module
-// #[test]
-// fn cannot_undelete_not_deleted_category() {
-//     let config = default_genesis_config();
-//     let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-//     build_test_externalities(config).execute_with(|| {
-//         let category_id = create_root_category(forum_sudo.clone());
-//         assert_err!(
-//             delete_category(
-//                 forum_sudo.clone(),
-//                 category_id.clone(),
-//             ),
-//             "... TODO expect error ..."
-//         );
-//     });
-// }
-
-// With archived / deleted category, moderated thread
-// -----------------------------------------------------------------------------
-
-#[test]
-fn cannot_create_subcategory_in_archived_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_category(
-            forum_sudo,
-            Some(category_id),
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+fn set_stickied_threads_ok() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderator_id,
+            category_id,
+            true,
+            Ok(()),
         );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        set_stickied_threads_mock(origin, moderator_id, category_id, vec![thread_id], Ok(()));
     });
 }
 
 #[test]
-fn cannot_create_subcategory_in_deleted_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+fn set_stickied_threads_wrong_moderator() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_category(
-            forum_sudo,
-            Some(category_id),
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        set_stickied_threads_mock(
+            origin,
+            moderator_id,
+            category_id,
+            vec![thread_id],
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
         );
     });
 }
 
 #[test]
-fn cannot_create_thread_in_archived_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_thread(
-            create_forum_member(),
+fn set_stickied_threads_thread_not_exists() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let moderator_id = forum_lead;
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderator_id,
+            category_id,
+            true,
+            Ok(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
+        let wrong_thread_id = thread_id + 1;
+        set_stickied_threads_mock(
+            origin,
+            moderator_id,
             category_id,
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+            vec![wrong_thread_id],
+            Err(Error::<Runtime>::ThreadDoesNotExist.into()),
         );
     });
 }
 
 #[test]
-fn cannot_create_thread_in_deleted_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
-
+fn test_migration_not_done() {
+    let config = migration_not_done_config();
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
     build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_thread(
-            create_forum_member(),
-            category_id,
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        let forum_user_id = 1;
+        let moderator_id = 1;
+        let category_id = 1;
+        let thread_id = 1;
+        let post_id = 1;
+
+        assert_err!(
+            TestForumModule::create_category(
+                mock_origin(origin.clone()),
+                None,
+                good_category_title(),
+                good_category_description()
+            ),
+            Error::<Runtime>::DataMigrationNotDone,
         );
-    });
-}
 
-#[test]
-fn cannot_create_post_in_thread_of_archived_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+        assert_err!(
+            TestForumModule::create_thread(
+                mock_origin(origin.clone()),
+                forum_user_id,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+            ),
+            Error::<Runtime>::DataMigrationNotDone,
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        let thread_id = TestForumModule::next_thread_id();
-        assert_create_thread(create_forum_member(), category_id, Ok(()));
-        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_post(
-            create_forum_member(),
-            thread_id,
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        assert_err!(
+            TestForumModule::add_post(
+                mock_origin(origin.clone()),
+                forum_user_id,
+                category_id,
+                thread_id,
+                good_post_text(),
+            ),
+            Error::<Runtime>::DataMigrationNotDone,
         );
-    });
-}
 
-#[test]
-fn cannot_create_post_in_thread_of_deleted_category() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+        assert_err!(
+            TestForumModule::moderate_thread(
+                mock_origin(origin.clone()),
+                PrivilegedActor::Moderator(moderator_id),
+                category_id,
+                thread_id,
+                good_moderation_rationale(),
+            ),
+            Error::<Runtime>::DataMigrationNotDone,
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let category_id = create_root_category(forum_sudo.clone());
-        let thread_id = TestForumModule::next_thread_id();
-        assert_create_thread(create_forum_member(), category_id, Ok(()));
-        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
-        assert_create_post(
-            create_forum_member(),
-            thread_id,
-            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        assert_err!(
+            TestForumModule::moderate_post(
+                mock_origin(origin.clone()),
+                PrivilegedActor::Moderator(moderator_id),
+                category_id,
+                thread_id,
+                post_id,
+                good_moderation_rationale(),
+            ),
+            Error::<Runtime>::DataMigrationNotDone,
         );
     });
 }
 
 #[test]
-fn cannot_create_post_in_moderated_thread() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
-
-    build_test_externalities(config).execute_with(|| {
-        let (_, _, thread_id) = create_root_category_and_thread(forum_sudo.clone());
-        assert_ok!(moderate_thread(
-            forum_sudo,
-            thread_id.clone(),
-            good_rationale()
-        ));
-        assert_create_post(
-            create_forum_member(),
-            thread_id,
-            Err(ERROR_THREAD_MODERATED),
+// test storage limits are enforced
+fn storage_limit_checks() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+
+    // test MaxSubcategories and MaxThreadsInCategory
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
         );
+
+        // test max subcategories limit
+        let max = <<<Runtime as Trait>::MapLimits as StorageLimits>::MaxSubcategories>::get();
+        for i in 0..max {
+            create_category_mock(
+                origin.clone(),
+                Some(category_id),
+                good_category_title(),
+                good_category_description(),
+                match i {
+                    _ if i == max => Err(Error::<Runtime>::MapSizeLimit.into()),
+                    _ => Ok(()),
+                },
+            );
+        }
+
+        // test max threads in category
+        let max = <<<Runtime as Trait>::MapLimits as StorageLimits>::MaxThreadsInCategory>::get();
+        for i in 0..max {
+            create_thread_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                good_thread_title(),
+                good_thread_text(),
+                None,
+                match i {
+                    _ if i == max => Err(Error::<Runtime>::MapSizeLimit.into()),
+                    _ => Ok(()),
+                },
+            );
+        }
     });
-}
 
-#[test]
-fn cannot_edit_post_in_moderated_thread() {
-    let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    // test MaxPostsInThread
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        let thread_id = create_thread_mock(
+            origin.clone(),
+            forum_lead,
+            category_id,
+            good_thread_title(),
+            good_thread_text(),
+            None,
+            Ok(()),
+        );
 
-    build_test_externalities(config).execute_with(|| {
-        let (member_origin, _, thread_id, post_id) =
-            create_root_category_and_thread_and_post(forum_sudo.clone());
-        assert_ok!(moderate_thread(forum_sudo, thread_id, good_rationale()));
-        assert_err!(
-            TestForumModule::edit_post_text(mock_origin(member_origin), post_id, good_rationale()),
-            ERROR_THREAD_MODERATED
+        // test max posts in thread
+        let max = <<<Runtime as Trait>::MapLimits as StorageLimits>::MaxPostsInThread>::get();
+        // starting from 1 because create_thread_mock creates one post by itself
+        for i in 1..max {
+            create_post_mock(
+                origin.clone(),
+                forum_lead,
+                category_id,
+                thread_id,
+                good_post_text(),
+                match i {
+                    _ if i == max => Err(Error::<Runtime>::MapSizeLimit.into()),
+                    _ => Ok(()),
+                },
+            );
+        }
+    });
+
+    // test MaxModeratorsForCategory
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
         );
+
+        let max: usize =
+            <<<Runtime as Trait>::MapLimits as StorageLimits>::MaxModeratorsForCategory>::get()
+                as usize;
+        for i in 0..max {
+            let moderator_id = EXTRA_MODERATORS[i];
+            update_category_membership_of_moderator_mock(
+                origin.clone(),
+                moderator_id,
+                category_id,
+                true,
+                match i {
+                    _ if i == max => Err(Error::<Runtime>::MapSizeLimit.into()),
+                    _ => Ok(()),
+                },
+            );
+        }
     });
-}
 
-// TODO impl
-// #[test]
-// fn cannot_edit_moderated_post() {}
+    // test MaxCategories
+    with_test_externalities(|| {
+        let max: usize =
+            <<<Runtime as Trait>::MapLimits as StorageLimits>::MaxPostsInThread>::get() as usize;
+        for i in 0..max {
+            create_category_mock(
+                origin.clone(),
+                None,
+                good_category_title(),
+                good_category_description(),
+                match i {
+                    _ if i == max => Err(Error::<Runtime>::MapSizeLimit.into()),
+                    _ => Ok(()),
+                },
+            );
+        }
+    });
+}

+ 0 - 31
runtime/src/integration/forum.rs

@@ -1,31 +0,0 @@
-/*
- * Forum module integration
- *
- * ForumUserRegistry could have been implemented directly on
- * the membership module, and likewise ForumUser on Profile,
- * however this approach is more loosely coupled.
- *
- * Further exploration required to decide what the long
- * run convention should be.
- */
-
-use crate::{AccountId, Runtime};
-
-/// Shim registry which will proxy ForumUserRegistry behaviour to the members module
-pub struct ShimMembershipRegistry {}
-
-impl forum::ForumUserRegistry<AccountId> for ShimMembershipRegistry {
-    fn get_forum_user(id: &AccountId) -> Option<forum::ForumUser<AccountId>> {
-        if membership::Module::<Runtime>::is_member_account(id) {
-            // For now we don't retrieve the members profile since it is not used for anything,
-            // but in the future we may need it to read out more
-            // information possibly required to construct a
-            // ForumUser.
-
-            // Now convert member profile to a forum user
-            Some(forum::ForumUser { id: id.clone() })
-        } else {
-            None
-        }
-    }
-}

+ 0 - 1
runtime/src/integration/mod.rs

@@ -1,6 +1,5 @@
 pub mod content_directory;
 pub mod content_working_group;
-pub mod forum;
 pub mod proposals;
 pub mod staking_handler;
 pub mod storage;

+ 1 - 0
runtime/src/integration/proposals/proposal_encoder.rs

@@ -22,6 +22,7 @@ macro_rules! wrap_working_group_call {
                 Call::ContentDirectoryWorkingGroup($working_group_instance_call)
             }
             WorkingGroup::Storage => Call::StorageWorkingGroup($working_group_instance_call),
+            WorkingGroup::Forum => Call::ForumWorkingGroup($working_group_instance_call),
         }
     }};
 }

+ 79 - 2
runtime/src/lib.rs

@@ -552,13 +552,80 @@ impl membership::Trait for Runtime {
     type ActorId = ActorId;
 }
 
+parameter_types! {
+    pub const MaxCategoryDepth: u64 = 5;
+
+    pub const MaxSubcategories: u64 = 20;
+    pub const MaxThreadsInCategory: u64 = 20;
+    pub const MaxPostsInThread: u64 = 20;
+    pub const MaxModeratorsForCategory: u64 = 20;
+    pub const MaxCategories: u64 = 20;
+}
+
+pub struct MapLimits;
+
+impl forum::StorageLimits for MapLimits {
+    type MaxSubcategories = MaxSubcategories;
+    type MaxThreadsInCategory = MaxThreadsInCategory;
+    type MaxPostsInThread = MaxPostsInThread;
+    type MaxModeratorsForCategory = MaxModeratorsForCategory;
+    type MaxCategories = MaxCategories;
+}
+
+// Alias for forum working group
+type ForumGroup<T> = working_group::Module<T, ForumWorkingGroupInstance>;
+
 impl forum::Trait for Runtime {
     type Event = Event;
-    type MembershipRegistry = integration::forum::ShimMembershipRegistry;
+    //type MembershipRegistry = ShimMembershipRegistry;
     type ThreadId = ThreadId;
     type PostId = PostId;
+    type ForumUserId = ForumUserId;
+    type ModeratorId = ModeratorId;
+    type CategoryId = u64;
+    type PostReactionId = u64;
+    type MaxCategoryDepth = MaxCategoryDepth;
+
+    type MapLimits = MapLimits;
+
+    fn is_lead(_account_id: &AccountId) -> bool {
+        // get current lead id
+        let maybe_current_lead_id = ForumGroup::<Runtime>::current_lead();
+        if let Some(ref current_lead_id) = maybe_current_lead_id {
+            if let Ok(worker) = ForumGroup::<Runtime>::ensure_worker_exists(current_lead_id) {
+                *_account_id == worker.role_account_id
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }
+
+    fn is_forum_member(_account_id: &Self::AccountId, _forum_user_id: &Self::ForumUserId) -> bool {
+        membership::Module::<Runtime>::ensure_is_controller_account_for_member(
+            _forum_user_id,
+            _account_id,
+        )
+        .is_ok()
+    }
+
+    fn is_moderator(_account_id: &Self::AccountId, _moderator_id: &Self::ModeratorId) -> bool {
+        if let Ok(worker) = ForumGroup::<Runtime>::ensure_worker_exists(_moderator_id) {
+            *_account_id == worker.role_account_id
+        } else {
+            false
+        }
+    }
+
+    fn calculate_hash(text: &[u8]) -> Self::Hash {
+        Self::Hash::from_slice(text)
+    }
 }
 
+// The forum working group instance alias.
+pub type ForumWorkingGroupInstance = working_group::Instance1;
+
 // The storage working group instance alias.
 pub type StorageWorkingGroupInstance = working_group::Instance2;
 
@@ -569,6 +636,11 @@ parameter_types! {
     pub const MaxWorkerNumberLimit: u32 = 100;
 }
 
+impl working_group::Trait<ForumWorkingGroupInstance> for Runtime {
+    type Event = Event;
+    type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
+}
+
 impl working_group::Trait<StorageWorkingGroupInstance> for Runtime {
     type Event = Event;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
@@ -669,6 +741,11 @@ parameter_types! {
     pub const SurchargeReward: Balance = 0; // no reward
 }
 
+/// Forum identifiers for user, moderator and category
+pub type ForumUserId = u64;
+pub type ModeratorId = u64;
+pub type CategoryId = u64;
+
 /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know
 /// the specifics of the runtime. They can then be made to be agnostic over specific formats
 /// of data like extrinsics, allowing for them to continue syncing the network through upgrades
@@ -735,7 +812,7 @@ construct_runtime!(
         ProposalsDiscussion: proposals_discussion::{Module, Call, Storage, Event<T>},
         ProposalsCodex: proposals_codex::{Module, Call, Storage},
         // --- Working groups
-        // reserved for the future use: ForumWorkingGroup: working_group::<Instance1>::{Module, Call, Storage, Event<T>},
+        ForumWorkingGroup: working_group::<Instance1>::{Module, Call, Storage, Config<T>, Event<T>},
         StorageWorkingGroup: working_group::<Instance2>::{Module, Call, Storage, Config<T>, Event<T>},
         ContentDirectoryWorkingGroup: working_group::<Instance3>::{Module, Call, Storage, Config<T>, Event<T>},
     }

+ 60 - 1
runtime/src/tests/proposals_integration/working_group_proposals.rs

@@ -12,7 +12,7 @@ use working_group::{OpeningPolicyCommitment, RewardPolicy};
 
 use crate::{
     Balance, BlockNumber, ContentDirectoryWorkingGroup, ContentDirectoryWorkingGroupInstance,
-    StorageWorkingGroup, StorageWorkingGroupInstance,
+    ForumWorkingGroup, ForumWorkingGroupInstance, StorageWorkingGroup, StorageWorkingGroupInstance,
 };
 use sp_std::collections::btree_set::BTreeSet;
 
@@ -52,6 +52,14 @@ fn add_opening(
             >>::contains_key(opening_id));
             opening_id
         }
+        WorkingGroup::Forum => {
+            let opening_id = ForumWorkingGroup::next_opening_id();
+            assert!(!<working_group::OpeningById<
+                Runtime,
+                ForumWorkingGroupInstance,
+            >>::contains_key(opening_id));
+            opening_id
+        }
     };
 
     let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
@@ -338,6 +346,12 @@ fn create_add_working_group_leader_opening_proposal_execution_succeeds() {
                     StorageWorkingGroupInstance,
                 >(group);
             }
+            WorkingGroup::Forum => {
+                run_create_add_working_group_leader_opening_proposal_execution_succeeds::<
+                    Runtime,
+                    ForumWorkingGroupInstance,
+                >(group);
+            }
         }
     }
 }
@@ -396,6 +410,12 @@ fn create_begin_review_working_group_leader_applications_proposal_execution_succ
                 StorageWorkingGroupInstance,
             >(group);
             }
+            WorkingGroup::Forum => {
+                run_create_begin_review_working_group_leader_applications_proposal_execution_succeeds::<
+                Runtime,
+                ForumWorkingGroupInstance,
+            >(group);
+            }
         }
     }
 }
@@ -478,6 +498,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                     StorageWorkingGroupInstance,
                 >(group);
             }
+            WorkingGroup::Forum => {
+                run_create_fill_working_group_leader_opening_proposal_execution_succeeds::<
+                    Runtime,
+                    ForumWorkingGroupInstance,
+                >(group);
+            }
         }
     }
 
@@ -560,6 +586,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                         StorageWorkingGroupInstance,
                     >(group);
                 }
+                WorkingGroup::Forum => {
+                    run_create_decrease_group_leader_stake_proposal_execution_succeeds::<
+                        Runtime,
+                        ForumWorkingGroupInstance,
+                    >(group);
+                }
             }
         }
     }
@@ -677,6 +709,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                         StorageWorkingGroupInstance,
                     >(group)
                 }
+                WorkingGroup::Forum => {
+                    run_create_slash_group_leader_stake_proposal_execution_succeeds::<
+                        Runtime,
+                        ForumWorkingGroupInstance,
+                    >(group)
+                }
             }
         }
     }
@@ -795,6 +833,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                         StorageWorkingGroupInstance,
                     >(group);
                 }
+                WorkingGroup::Forum => {
+                    run_create_set_working_group_mint_capacity_proposal_execution_succeeds::<
+                        Runtime,
+                        ForumWorkingGroupInstance,
+                    >(group);
+                }
             }
         }
 
@@ -851,6 +895,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                             StorageWorkingGroupInstance,
                         >(group);
                     }
+                    WorkingGroup::Forum => {
+                        run_create_set_working_group_mint_capacity_proposal_execution_succeeds::<
+                            Runtime,
+                            ForumWorkingGroupInstance,
+                        >(group);
+                    }
                 }
             }
         }
@@ -974,6 +1024,12 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                             StorageWorkingGroupInstance,
                         >(group);
                     }
+                    WorkingGroup::Forum => {
+                        run_create_terminate_group_leader_role_proposal_execution_succeeds::<
+                            Runtime,
+                            ForumWorkingGroupInstance,
+                        >(group);
+                    }
                 }
             }
         }
@@ -1093,6 +1149,9 @@ fn create_fill_working_group_leader_opening_proposal_execution_succeeds() {
                     WorkingGroup::Storage => {
                         run_create_terminate_group_leader_role_proposal_with_slashing_execution_succeeds::<Runtime, StorageWorkingGroupInstance>(group);
                     }
+                    WorkingGroup::Forum => {
+                        run_create_terminate_group_leader_role_proposal_with_slashing_execution_succeeds::<Runtime, ForumWorkingGroupInstance>(group);
+                    }
                 }
             }
         }

Some files were not shown because too many files changed in this diff