浏览代码

Merge branch 'forum_monorepo_migration' into monorepo_external_modules
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

Shamil Gadelshin 5 年之前
父节点
当前提交
33730e4f91
共有 4 个文件被更改,包括 2879 次插入0 次删除
  1. 50 0
      runtime-modules/forum/Cargo.toml
  2. 1318 0
      runtime-modules/forum/src/lib.rs
  3. 530 0
      runtime-modules/forum/src/mock.rs
  4. 981 0
      runtime-modules/forum/src/tests.rs

+ 50 - 0
runtime-modules/forum/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-forum-module'
+version = '1.1.1'
+authors = ['Bedeho Mender <bedeho.mender@protonmail.com>']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0.101', optional = true}
+serde_derive = { version = '1.0.101', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 1318 - 0
runtime-modules/forum/src/lib.rs

@@ -0,0 +1,1318 @@
+// Copyright 2017-2019 Parity Technologies (UK) Ltd.
+
+// This is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+// Copyright 2019 Joystream Contributors
+
+//! # Runtime Example Module
+//!
+//! <!-- Original author of paragraph: @gavofyork -->
+//! The Example: A simple example of a runtime module demonstrating
+//! concepts, APIs and structures common to most runtime modules.
+//!
+//! Run `cargo doc --package runtime-example-module --open` to view this module's documentation.
+//!
+//! ### Documentation Template:<br>
+//! Add heading with custom module name
+//!
+//! # <INSERT_CUSTOM_MODULE_NAME> Module
+//!
+//! Add simple description
+//!
+//! Include the following links that shows what trait needs to be implemented to use the module
+//! and the supported dispatchables that are documented in the Call enum.
+//!
+//! - [`<INSERT_CUSTOM_MODULE_NAME>::Trait`](./trait.Trait.html)
+//! - [`Call`](./enum.Call.html)
+//! - [`Module`](./struct.Module.html)
+//!
+//! ## Overview
+//!
+//! <!-- Original author of paragraph: Various. See https://github.com/paritytech/substrate-developer-hub/issues/44 -->
+//! Short description of module purpose.
+//! Links to Traits that should be implemented.
+//! What this module is for.
+//! What functionality the module provides.
+//! When to use the module (use case examples).
+//! How it is used.
+//! Inputs it uses and the source of each input.
+//! Outputs it produces.
+//!
+//! <!-- Original author of paragraph: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//! <!-- and comment https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
+//!
+//! ## Terminology
+//!
+//! Add terminology used in the custom module. Include concepts, storage items, or actions that you think
+//! deserve to be noted to give context to the rest of the documentation or module usage. The author needs to
+//! use some judgment about what is included. We don't want a list of every storage item nor types - the user
+//! can go to the code for that. For example, "transfer fee" is obvious and should not be included, but
+//! "free balance" and "reserved balance" should be noted to give context to the module.
+//! Please do not link to outside resources. The reference docs should be the ultimate source of truth.
+//!
+//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! ## Goals
+//!
+//! Add goals that the custom module is designed to achieve.
+//!
+//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! ### Scenarios
+//!
+//! <!-- Original author of paragraph: @Kianenigma. Based on PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! #### <INSERT_SCENARIO_NAME>
+//!
+//! Describe requirements prior to interacting with the custom module.
+//! Describe the process of interacting with the custom module for this scenario and public API functions used.
+//!
+//! ## Interface
+//!
+//! ### Supported Origins
+//!
+//! What origins are used and supported in this module (root, signed, inherent)
+//! i.e. root when `ensure_root` used
+//! i.e. inherent when `ensure_inherent` used
+//! i.e. signed when `ensure_signed` used
+//!
+//! `inherent` <INSERT_DESCRIPTION>
+//!
+//! <!-- Original author of paragraph: @Kianenigma in comment -->
+//! <!-- https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
+//!
+//! ### Types
+//!
+//! Type aliases. Include any associated types and where the user would typically define them.
+//!
+//! `ExampleType` <INSERT_DESCRIPTION>
+//!
+//! <!-- Original author of paragraph: ??? -->
+//!
+//!
+//! ### Dispatchable Functions
+//!
+//! <!-- Original author of paragraph: @AmarRSingh & @joepetrowski -->
+//!
+//! // A brief description of dispatchable functions and a link to the rustdoc with their actual documentation.
+//!
+//! <b>MUST</b> have link to Call enum
+//! <b>MUST</b> have origin information included in function doc
+//! <b>CAN</b> have more info up to the user
+//!
+//! ### Public Functions
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! A link to the rustdoc and any notes about usage in the module, not for specific functions.
+//! For example, in the balances module: "Note that when using the publicly exposed functions,
+//! you (the runtime developer) are responsible for implementing any necessary checks
+//! (e.g. that the sender is the signer) before calling a function that will affect storage."
+//!
+//! <!-- Original author of paragraph: @AmarRSingh -->
+//!
+//! It is up to the writer of the respective module (with respect to how much information to provide).
+//!
+//! #### Public Inspection functions - Immutable (getters)
+//!
+//! Insert a subheading for each getter function signature
+//!
+//! ##### `example_getter_name()`
+//!
+//! What it returns
+//! Why, when, and how often to call it
+//! When it could panic or error
+//! When safety issues to consider
+//!
+//! #### Public Mutable functions (changing state)
+//!
+//! Insert a subheading for each setter function signature
+//!
+//! ##### `example_setter_name(origin, parameter_name: T::ExampleType)`
+//!
+//! What state it changes
+//! Why, when, and how often to call it
+//! When it could panic or error
+//! When safety issues to consider
+//! What parameter values are valid and why
+//!
+//! ### Storage Items
+//!
+//! Explain any storage items included in this module
+//!
+//! ### Digest Items
+//!
+//! Explain any digest items included in this module
+//!
+//! ### Inherent Data
+//!
+//! Explain what inherent data (if any) is defined in the module and any other related types
+//!
+//! ### Events:
+//!
+//! Insert events for this module if any
+//!
+//! ### Errors:
+//!
+//! Explain what generates errors
+//!
+//! ## Usage
+//!
+//! Insert 2-3 examples of usage and code snippets that show how to use <INSERT_CUSTOM_MODULE_NAME> module in a custom module.
+//!
+//! ### Prerequisites
+//!
+//! Show how to include necessary imports for <INSERT_CUSTOM_MODULE_NAME> and derive
+//! your module configuration trait with the `INSERT_CUSTOM_MODULE_NAME` trait.
+//!
+//! ```rust
+//! // use <INSERT_CUSTOM_MODULE_NAME>;
+//!
+//! // pub trait Trait: <INSERT_CUSTOM_MODULE_NAME>::Trait { }
+//! ```
+//!
+//! ### Simple Code Snippet
+//!
+//! Show a simple example (e.g. how to query a public getter function of <INSERT_CUSTOM_MODULE_NAME>)
+//!
+//! ## Genesis Config
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! ## Dependencies
+//!
+//! Dependencies on other SRML modules and the genesis config should be mentioned,
+//! but not the Rust Standard Library.
+//! Genesis configuration modifications that may be made to incorporate this module
+//! Interaction with other modules
+//!
+//! <!-- Original author of heading: @AmarRSingh -->
+//!
+//! ## Related Modules
+//!
+//! Interaction with other modules in the form of a bullet point list
+//!
+//! ## References
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! Links to reference material, if applicable. For example, Phragmen, W3F research, etc.
+//! that the implementation is based on.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use serde_derive::{Deserialize, Serialize};
+
+use rstd::prelude::*;
+
+use codec::{Decode, Encode};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+
+mod mock;
+mod tests;
+
+/*
+ * MOVE ALL OF THESE OUT TO COMMON LATER
+ */
+
+/// Length constraint for input validation
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct InputValidationLengthConstraint {
+    /// Minimum length
+    pub min: u16,
+
+    /// Difference between minimum length and max length.
+    /// While having max would have been more direct, this
+    /// way makes max < min unrepresentable semantically,
+    /// which is safer.
+    pub max_min_diff: u16,
+}
+
+impl InputValidationLengthConstraint {
+    /// Helper for computing max
+    pub fn max(&self) -> u16 {
+        self.min + self.max_min_diff
+    }
+
+    pub fn ensure_valid(
+        &self,
+        len: usize,
+        too_short_msg: &'static str,
+        too_long_msg: &'static str,
+    ) -> Result<(), &'static str> {
+        let length = len as u16;
+        if length < self.min {
+            Err(too_short_msg)
+        } else if length > self.max() {
+            Err(too_long_msg)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+/// Constants
+/////////////////////////////////////////////////////////////////
+
+/// The greatest valid depth of a category.
+/// The depth of a root category is 0.
+const MAX_CATEGORY_DEPTH: u16 = 3;
+
+/// Error messages for dispatchables
+const ERROR_FORUM_SUDO_NOT_SET: &str = "Forum sudo not set.";
+const ERROR_ORIGIN_NOT_FORUM_SUDO: &str = "Origin not forum sudo.";
+const ERROR_CATEGORY_TITLE_TOO_SHORT: &str = "Category title too short.";
+const ERROR_CATEGORY_TITLE_TOO_LONG: &str = "Category title too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_SHORT: &str = "Category description too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_LONG: &str = "Category description too long.";
+const ERROR_ANCESTOR_CATEGORY_IMMUTABLE: &str =
+    "Ancestor category immutable, i.e. deleted or archived";
+const ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED: &str = "Maximum valid category depth exceeded.";
+const ERROR_CATEGORY_DOES_NOT_EXIST: &str = "Category does not exist.";
+const ERROR_NOT_FORUM_USER: &str = "Not forum user.";
+const ERROR_THREAD_TITLE_TOO_SHORT: &str = "Thread title too short.";
+const ERROR_THREAD_TITLE_TOO_LONG: &str = "Thread title too long.";
+const ERROR_POST_TEXT_TOO_SHORT: &str = "Post text too short.";
+const ERROR_POST_TEXT_TOO_LONG: &str = "Post too long.";
+const ERROR_THREAD_DOES_NOT_EXIST: &str = "Thread does not exist";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT: &str = "Thread moderation rationale too short.";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG: &str = "Thread moderation rationale too long.";
+const ERROR_THREAD_ALREADY_MODERATED: &str = "Thread already moderated.";
+const ERROR_THREAD_MODERATED: &str = "Thread is moderated.";
+const ERROR_POST_DOES_NOT_EXIST: &str = "Post does not exist.";
+const ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR: &str = "Account does not match post author.";
+const ERROR_POST_MODERATED: &str = "Post is moderated.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_SHORT: &str = "Post moderation rationale too short.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_LONG: &str = "Post moderation rationale too long.";
+const ERROR_CATEGORY_NOT_BEING_UPDATED: &str = "Category not being updated.";
+const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str =
+    "Category cannot be unarchived when deleted.";
+
+//use srml_support::storage::*;
+
+//use sr_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(feature = "std")]
+//use runtime_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(any(feature = "std", test))]
+//use sr_primitives::{StorageOverlay, ChildrenStorageOverlay};
+
+use system;
+use system::{ensure_root, ensure_signed};
+
+/// Represents a user in this forum.
+#[derive(Debug, Copy, Clone)]
+pub struct ForumUser<AccountId> {
+    /// Identifier of user
+    pub id: AccountId, // In the future one could add things like
+                       // - updating post count of a user
+                       // - updating status (e.g. hero, new, etc.)
+                       //
+}
+
+/// Represents a regsitry of `ForumUser` instances.
+pub trait ForumUserRegistry<AccountId> {
+    fn get_forum_user(id: &AccountId) -> Option<ForumUser<AccountId>>;
+}
+
+/// Convenient composite time stamp
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct BlockchainTimestamp<BlockNumber, Moment> {
+    block: BlockNumber,
+    time: Moment,
+}
+
+/// Represents a moderation outcome applied to a post or a thread.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ModerationAction<BlockNumber, Moment, AccountId> {
+    /// When action occured.
+    moderated_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Account forum sudo which acted.
+    moderator_id: AccountId,
+
+    /// Moderation rationale
+    rationale: Vec<u8>,
+}
+
+/// Represents a revision of the text of a Post
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct PostTextChange<BlockNumber, Moment> {
+    /// When this expiration occured
+    expired_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Text that expired
+    text: Vec<u8>,
+}
+
+/// Represents a post identifier
+pub type PostId = u64;
+
+/// Represents a thread post
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Post<BlockNumber, Moment, AccountId> {
+    /// Post identifier
+    id: PostId,
+
+    /// Id of thread to which this post corresponds.
+    thread_id: ThreadId,
+
+    /// The post number of this post in its thread, i.e. total number of posts added (including this)
+    /// to a thread when it was added.
+    /// Is needed to give light clients assurance about getting all posts in a given range,
+    // `created_at` is not sufficient.
+    /// Starts at 1 for first post in thread.
+    nr_in_thread: u32,
+
+    /// Current text of post
+    current_text: Vec<u8>,
+
+    /// Possible moderation of this post
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Edits of post ordered chronologically by edit time.
+    text_change_history: Vec<PostTextChange<BlockNumber, Moment>>,
+
+    /// When post was submitted.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    author_id: AccountId,
+}
+
+/// Represents a thread identifier
+pub type ThreadId = u64;
+
+/// Represents a thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Thread<BlockNumber, Moment, AccountId> {
+    /// Thread identifier
+    id: ThreadId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Category in which this thread lives
+    category_id: CategoryId,
+
+    /// The thread number of this thread in its category, i.e. total number of thread added (including this)
+    /// to a category when it was added.
+    /// Is needed to give light clients assurance about getting all threads in a given range,
+    /// `created_at` is not sufficient.
+    /// Starts at 1 for first thread in category.
+    nr_in_category: u32,
+
+    /// Possible moderation of this thread
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Number of unmoderated and moderated posts in this thread.
+    /// The sum of these two only increases, and former is incremented
+    /// for each new post added to this thread. A new post is added
+    /// with a `nr_in_thread` equal to this sum
+    ///
+    /// When there is a moderation
+    /// of a post, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from posts in a thread.
+    num_unmoderated_posts: u32,
+    num_moderated_posts: u32,
+
+    /// When thread was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    author_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Thread<BlockNumber, Moment, AccountId> {
+    fn num_posts_ever_created(&self) -> u32 {
+        self.num_unmoderated_posts + self.num_moderated_posts
+    }
+}
+
+/// Represents a category identifier
+pub type CategoryId = u64;
+
+/// Represents
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ChildPositionInParentCategory {
+    /// Id of parent category
+    parent_id: CategoryId,
+
+    /// Nr of the child in the parent
+    /// Starts at 1
+    child_nr_in_parent_category: u32,
+}
+
+/// Represents a category
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Category<BlockNumber, Moment, AccountId> {
+    /// Category identifier
+    id: CategoryId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Description
+    description: Vec<u8>,
+
+    /// When category was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Whether category is deleted.
+    deleted: bool,
+
+    /// Whether category is archived.
+    archived: bool,
+
+    /// Number of subcategories (deleted, archived or neither),
+    /// unmoderated threads and moderated threads, _directly_ in this category.
+    ///
+    /// As noted, the first is unaffected by any change in state of direct subcategory.
+    ///
+    /// The sum of the latter two only increases, and former is incremented
+    /// for each new thread added to this category. A new thread is added
+    /// with a `nr_in_category` equal to this sum.
+    ///
+    /// When there is a moderation
+    /// of a thread, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from subcategories or threads in a category.
+    num_direct_subcategories: u32,
+    num_direct_unmoderated_threads: u32,
+    num_direct_moderated_threads: u32,
+
+    /// Position as child in parent, if present, otherwise this category is a root category
+    position_in_parent_category: Option<ChildPositionInParentCategory>,
+
+    /// Account of the moderator which created category.
+    moderator_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Category<BlockNumber, Moment, AccountId> {
+    fn num_threads_created(&self) -> u32 {
+        self.num_direct_unmoderated_threads + self.num_direct_moderated_threads
+    }
+}
+
+/// Represents a sequence of categories which have child-parent relatioonship
+/// where last element is final ancestor, or root, in the context of the category tree.
+type CategoryTreePath<BlockNumber, Moment, AccountId> =
+    Vec<Category<BlockNumber, Moment, AccountId>>;
+
+pub trait Trait: system::Trait + timestamp::Trait + Sized {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Forum {
+
+        /// Map category identifier to corresponding category.
+        pub CategoryById get(category_by_id) config(): map CategoryId => Category<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Category identifier value to be used for the next Category created.
+        pub NextCategoryId get(next_category_id) config(): CategoryId;
+
+        /// Map thread identifier to corresponding thread.
+        pub ThreadById get(thread_by_id) config(): map ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Thread identifier value to be used for next Thread in threadById.
+        pub NextThreadId get(next_thread_id) config(): ThreadId;
+
+        /// Map post identifier to corresponding post.
+        pub PostById get(post_by_id) config(): map PostId => Post<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Post identifier value to be used for for next post created.
+        pub NextPostId get(next_post_id) config(): PostId;
+
+        /// Account of forum sudo.
+        pub ForumSudo get(forum_sudo) config(): Option<T::AccountId>;
+
+        /// Input constraints
+        /// These are all forward looking, that is they are enforced on all
+        /// future calls.
+        pub CategoryTitleConstraint get(category_title_constraint) config(): InputValidationLengthConstraint;
+        pub CategoryDescriptionConstraint get(category_description_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadTitleConstraint get(thread_title_constraint) config(): InputValidationLengthConstraint;
+        pub PostTextConstraint get(post_text_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadModerationRationaleConstraint get(thread_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+        pub PostModerationRationaleConstraint get(post_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+    }
+    /*
+    JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+    Review : https://github.com/paritytech/polkadot/blob/620b8610431e7b5fdd71ce3e94c3ee0177406dcc/runtime/src/parachains.rs#L123-L141
+
+    add_extra_genesis {
+
+        // Explain why we need to put this here.
+        config(initial_forum_sudo) : Option<T::AccountId>;
+
+        build(|
+            storage: &mut generator::StorageOverlay,
+            _: &mut generator::ChildrenStorageOverlay,
+            config: &GenesisConfig<T>
+            | {
+
+
+            if let Some(account_id) = &config.initial_forum_sudo {
+                println!("{}: <ForumSudo<T>>::put(account_id)", account_id);
+                <ForumSudo<T> as generator::StorageValue<_>>::put(&account_id, storage);
+            }
+        })
+    }
+    */
+}
+
+decl_event!(
+    pub enum Event<T>
+    where
+        <T as system::Trait>::AccountId,
+    {
+        /// A category was introduced
+        CategoryCreated(CategoryId),
+
+        /// A category with given id was updated.
+        /// The second argument reflects the new archival status of the category, if changed.
+        /// The third argument reflects the new deletion status of the category, if changed.
+        CategoryUpdated(CategoryId, Option<bool>, Option<bool>),
+
+        /// A thread with given id was created.
+        ThreadCreated(ThreadId),
+
+        /// A thread with given id was moderated.
+        ThreadModerated(ThreadId),
+
+        /// Post with given id was created.
+        PostAdded(PostId),
+
+        /// Post with givne id was moderated.
+        PostModerated(PostId),
+
+        /// Post with given id had its text updated.
+        /// The second argument reflects the number of total edits when the text update occurs.
+        PostTextUpdated(PostId, u64),
+
+        /// Given account was set as forum sudo.
+        ForumSudoSet(Option<AccountId>, Option<AccountId>),
+    }
+);
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn deposit_event() = default;
+
+        /// Set forum sudo.
+        fn set_forum_sudo(origin, new_forum_sudo: Option<T::AccountId>) -> dispatch::Result {
+            ensure_root(origin)?;
+
+            /*
+             * Question: when this routine is called by non sudo or with bad signature, what error is raised?
+             * Update ERror set in spec
+             */
+
+            // Hold on to old value
+            let old_forum_sudo = <ForumSudo<T>>::get().clone();
+
+            // Update forum sudo
+            match new_forum_sudo.clone() {
+                Some(account_id) => <ForumSudo<T>>::put(account_id),
+                None => <ForumSudo<T>>::kill()
+            };
+
+            // Generate event
+            Self::deposit_event(RawEvent::ForumSudoSet(old_forum_sudo, new_forum_sudo));
+
+            // All good.
+            Ok(())
+        }
+
+        /// Add a new category.
+        fn create_category(origin, parent: Option<CategoryId>, title: Vec<u8>, description: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Validate title
+            Self::ensure_category_title_is_valid(&title)?;
+
+            // Validate description
+            Self::ensure_category_description_is_valid(&description)?;
+
+            // Position in parent field value for new category
+            let mut position_in_parent_category_field = None;
+
+            // If not root, then check that we can create in parent category
+            if let Some(parent_category_id) = parent {
+
+                let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(parent_category_id)?;
+
+                // Can we mutate in this category?
+                Self::ensure_can_add_subcategory_path_leaf(&category_tree_path)?;
+
+                /*
+                 * Here we are safe to mutate
+                 */
+
+                // Increment number of subcategories to reflect this new category being
+                // added as a child
+                <CategoryById<T>>::mutate(parent_category_id, |c| {
+                    c.num_direct_subcategories += 1;
+                });
+
+                // Set `position_in_parent_category_field`
+                let parent_category = category_tree_path.first().unwrap();
+
+                position_in_parent_category_field = Some(ChildPositionInParentCategory{
+                    parent_id: parent_category_id,
+                    child_nr_in_parent_category: parent_category.num_direct_subcategories
+                });
+
+            }
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let next_category_id = NextCategoryId::get();
+
+            // Create new category
+            let new_category = Category {
+                id : next_category_id,
+                title : title.clone(),
+                description: description.clone(),
+                created_at : Self::current_block_and_time(),
+                deleted: false,
+                archived: false,
+                num_direct_subcategories: 0,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: position_in_parent_category_field,
+                moderator_id: who
+            };
+
+            // Insert category in map
+            <CategoryById<T>>::insert(new_category.id, new_category);
+
+            // Update other things
+            NextCategoryId::put(next_category_id + 1);
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryCreated(next_category_id));
+
+            Ok(())
+        }
+
+        /// Update category
+        fn update_category(origin, category_id: CategoryId, new_archival_status: Option<bool>, new_deletion_status: Option<bool>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure something is actually being changed
+            ensure!(
+                new_archival_status.is_some() || new_deletion_status.is_some(),
+                ERROR_CATEGORY_NOT_BEING_UPDATED
+            );
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // When we are dealing with a non-root category, we
+            // must ensure mutability of our category by traversing to
+            // root.
+            if category_tree_path.len() > 1  {
+
+                // We must skip checking category itself.
+                // NB: This is kind of hacky way to avoid last element,
+                // something clearn can be done later.
+                let mut path_to_check = category_tree_path.clone();
+                path_to_check.remove(0);
+
+                Self::ensure_can_mutate_in_path_leaf(&path_to_check)?;
+            }
+
+            // If the category itself is already deleted, then this
+            // update *must* simultaneously do an undelete, otherwise it is blocked,
+            // as we do not permit unarchiving a deleted category. Doing
+            // a simultanous undelete and unarchive is accepted.
+
+            let category = <CategoryById<T>>::get(category_id);
+
+            ensure!(
+                !category.deleted || (new_deletion_status == Some(false)),
+                ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED
+            );
+
+            // Mutate category, and set possible new change parameters
+
+            <CategoryById<T>>::mutate(category_id, |c| {
+
+                if let Some(archived) = new_archival_status {
+                    c.archived = archived;
+                }
+
+                if let Some(deleted) = new_deletion_status {
+                    c.deleted = deleted;
+                }
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryUpdated(category_id, new_archival_status, new_deletion_status));
+
+            Ok(())
+        }
+
+        /// Create new thread in category
+        fn create_thread(origin, category_id: CategoryId, title: Vec<u8>, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             * and mutation of Category class,
+             * as well as side effect to update Category::num_threads_created.
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            // Validate title
+            Self::ensure_thread_title_is_valid(&title)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            /*
+             * Here it is safe to mutate state.
+             */
+
+            // Add thread
+            let thread = Self::add_new_thread(category_id, &title, &who);
+
+            // Add inital post to thread
+            Self::add_new_post(thread.id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadCreated(thread.id));
+
+            Ok(())
+        }
+
+        /// Moderate thread
+        fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Get thread
+            let mut thread = Self::ensure_thread_exists(&thread_id)?;
+
+            // Thread is not already moderated
+            ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED);
+
+            // Rationale valid
+            Self::ensure_thread_moderation_rationale_is_valid(&rationale)?;
+
+            // Can mutate in corresponding category
+            let path = Self::build_category_tree_path(thread.category_id);
+
+            // Path must be non-empty, as category id is from thread in state
+            assert!(!path.is_empty());
+
+            Self::ensure_can_mutate_in_path_leaf(&path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Add moderation to thread
+            thread.moderation = Some(ModerationAction {
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            });
+
+            <ThreadById<T>>::insert(thread_id, thread.clone());
+
+            // Update moderation/umoderation count of corresponding category
+            <CategoryById<T>>::mutate(thread.category_id, |category| {
+                category.num_direct_unmoderated_threads -= 1;
+                category.num_direct_moderated_threads += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadModerated(thread_id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn add_post(origin, thread_id: ThreadId, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            // Make sure thread exists and is mutable
+            let thread = Self::ensure_thread_is_mutable(&thread_id)?;
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(thread.category_id)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let post = Self::add_new_post(thread_id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostAdded(post.id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn edit_post_text(origin, post_id: PostId, new_text: Vec<u8>) -> dispatch::Result {
+
+            /* Edit spec.
+              - forum member guard missing
+              - check that both post and thread and category are mutable
+            */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&new_text)?;
+
+            // Make sure there exists a mutable post with post id `post_id`
+            let post = Self::ensure_post_is_mutable(&post_id)?;
+
+            // Signer does not match creator of post with identifier postId
+            ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR);
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            <PostById<T>>::mutate(post_id, |p| {
+
+                let expired_post_text = PostTextChange {
+                    expired_at: Self::current_block_and_time(),
+                    text: post.current_text.clone()
+                };
+
+                // Set current text to new text
+                p.current_text = new_text;
+
+                // Copy current text to history of expired texts
+                p.text_change_history.push(expired_post_text);
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostTextUpdated(post.id, post.text_change_history.len() as u64));
+
+            Ok(())
+        }
+
+        /// Moderate post
+        fn moderate_post(origin, post_id: PostId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure post exists and is mutable
+            let post = Self::ensure_post_is_mutable(&post_id)?;
+
+            Self::ensure_post_moderation_rationale_is_valid(&rationale)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Update moderation action on post
+            let moderation_action = ModerationAction{
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            };
+
+            <PostById<T>>::mutate(post_id, |p| {
+                p.moderation = Some(moderation_action);
+            });
+
+            // Update moderated and unmoderated post count of corresponding thread
+            <ThreadById<T>>::mutate(post.thread_id, |t| {
+                t.num_unmoderated_posts -= 1;
+                t.num_moderated_posts += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostModerated(post.id));
+
+            Ok(())
+        }
+
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_category_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        CategoryTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_CATEGORY_TITLE_TOO_SHORT,
+            ERROR_CATEGORY_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_category_description_is_valid(description: &Vec<u8>) -> dispatch::Result {
+        CategoryDescriptionConstraint::get().ensure_valid(
+            description.len(),
+            ERROR_CATEGORY_DESCRIPTION_TOO_SHORT,
+            ERROR_CATEGORY_DESCRIPTION_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        ThreadModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        ThreadTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_THREAD_TITLE_TOO_SHORT,
+            ERROR_THREAD_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PostTextConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_POST_TEXT_TOO_SHORT,
+            ERROR_POST_TEXT_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        PostModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_POST_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_POST_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn current_block_and_time() -> BlockchainTimestamp<T::BlockNumber, T::Moment> {
+        BlockchainTimestamp {
+            block: <system::Module<T>>::block_number(),
+            time: <timestamp::Module<T>>::now(),
+        }
+    }
+
+    fn ensure_post_is_mutable(
+        post_id: &PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure post exists
+        let post = Self::ensure_post_exists(post_id)?;
+
+        // and is unmoderated
+        ensure!(post.moderation.is_none(), ERROR_POST_MODERATED);
+
+        // and make sure thread is mutable
+        Self::ensure_thread_is_mutable(&post.thread_id)?;
+
+        Ok(post)
+    }
+
+    fn ensure_post_exists(
+        post_id: &PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        if <PostById<T>>::exists(post_id) {
+            Ok(<PostById<T>>::get(post_id))
+        } else {
+            Err(ERROR_POST_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_thread_is_mutable(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure thread exists
+        let thread = Self::ensure_thread_exists(&thread_id)?;
+
+        // and is unmoderated
+        ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED);
+
+        // and corresponding category is mutable
+        Self::ensure_catgory_is_mutable(thread.category_id)?;
+
+        Ok(thread)
+    }
+
+    fn ensure_thread_exists(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        if <ThreadById<T>>::exists(thread_id) {
+            Ok(<ThreadById<T>>::get(thread_id))
+        } else {
+            Err(ERROR_THREAD_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_forum_sudo_set() -> Result<T::AccountId, &'static str> {
+        match <ForumSudo<T>>::get() {
+            Some(account_id) => Ok(account_id),
+            None => Err(ERROR_FORUM_SUDO_NOT_SET),
+        }
+    }
+
+    fn ensure_is_forum_sudo(account_id: &T::AccountId) -> dispatch::Result {
+        let forum_sudo_account = Self::ensure_forum_sudo_set()?;
+
+        ensure!(
+            *account_id == forum_sudo_account,
+            ERROR_ORIGIN_NOT_FORUM_SUDO
+        );
+        Ok(())
+    }
+
+    fn ensure_is_forum_member(
+        account_id: &T::AccountId,
+    ) -> Result<ForumUser<T::AccountId>, &'static str> {
+        let forum_user_query = T::MembershipRegistry::get_forum_user(account_id);
+
+        if let Some(forum_user) = forum_user_query {
+            Ok(forum_user)
+        } else {
+            Err(ERROR_NOT_FORUM_USER)
+        }
+    }
+
+    fn ensure_catgory_is_mutable(category_id: CategoryId) -> dispatch::Result {
+        let category_tree_path = Self::build_category_tree_path(category_id);
+
+        Self::ensure_can_mutate_in_path_leaf(&category_tree_path)
+    }
+
+    fn ensure_can_mutate_in_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        // Is parent category directly or indirectly deleted or archived category
+        ensure!(
+            !category_tree_path.iter().any(
+                |c: &Category<T::BlockNumber, T::Moment, T::AccountId>| c.deleted || c.archived
+            ),
+            ERROR_ANCESTOR_CATEGORY_IMMUTABLE
+        );
+
+        Ok(())
+    }
+
+    fn ensure_can_add_subcategory_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        Self::ensure_can_mutate_in_path_leaf(category_tree_path)?;
+
+        // Does adding a new category exceed maximum depth
+        let depth_of_new_category = 1 + 1 + category_tree_path.len();
+
+        ensure!(
+            depth_of_new_category <= MAX_CATEGORY_DEPTH as usize,
+            ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED
+        );
+
+        Ok(())
+    }
+
+    fn ensure_valid_category_and_build_category_tree_path(
+        category_id: CategoryId,
+    ) -> Result<CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        ensure!(
+            <CategoryById<T>>::exists(&category_id),
+            ERROR_CATEGORY_DOES_NOT_EXIST
+        );
+
+        // Get path from parent to root of category tree.
+        let category_tree_path = Self::build_category_tree_path(category_id);
+
+        assert!(category_tree_path.len() > 0);
+
+        Ok(category_tree_path)
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn build_category_tree_path(
+        category_id: CategoryId,
+    ) -> CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get path from parent to root of category tree.
+        let mut category_tree_path = vec![];
+
+        Self::_build_category_tree_path(category_id, &mut category_tree_path);
+
+        category_tree_path
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn _build_category_tree_path(
+        category_id: CategoryId,
+        path: &mut CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) {
+        // Grab category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Copy out position_in_parent_category
+        let position_in_parent_category_field = category.position_in_parent_category.clone();
+
+        // Add category to path container
+        path.push(category);
+
+        // Make recursive call on parent if we are not at root
+        if let Some(child_position_in_parent) = position_in_parent_category_field {
+            assert!(<CategoryById<T>>::exists(
+                &child_position_in_parent.parent_id
+            ));
+
+            Self::_build_category_tree_path(child_position_in_parent.parent_id, path);
+        }
+    }
+
+    fn add_new_thread(
+        category_id: CategoryId,
+        title: &Vec<u8>,
+        author_id: &T::AccountId,
+    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Create and add new thread
+        let new_thread_id = NextThreadId::get();
+
+        let new_thread = Thread {
+            id: new_thread_id,
+            title: title.clone(),
+            category_id: category_id,
+            nr_in_category: category.num_threads_created() + 1,
+            moderation: None,
+            num_unmoderated_posts: 0,
+            num_moderated_posts: 0,
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store thread
+        <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
+
+        // Update next thread id
+        NextThreadId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated thread count in corresponding category
+        <CategoryById<T>>::mutate(category_id, |c| {
+            c.num_direct_unmoderated_threads += 1;
+        });
+
+        new_thread
+    }
+
+    /// Creates and ads a new post ot the given thread, and makes all required state updates
+    /// `thread_id` must be valid
+    fn add_new_post(
+        thread_id: ThreadId,
+        text: &Vec<u8>,
+        author_id: &T::AccountId,
+    ) -> Post<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get thread
+        let thread = <ThreadById<T>>::get(thread_id);
+
+        // Make and add initial post
+        let new_post_id = NextPostId::get();
+
+        let new_post = Post {
+            id: new_post_id,
+            thread_id: thread_id,
+            nr_in_thread: thread.num_posts_ever_created() + 1,
+            current_text: text.clone(),
+            moderation: None,
+            text_change_history: vec![],
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store post
+        <PostById<T>>::insert(new_post_id, new_post.clone());
+
+        // Update next post id
+        NextPostId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated post count of thread
+        <ThreadById<T>>::mutate(thread_id, |t| {
+            t.num_unmoderated_posts += 1;
+        });
+
+        new_post
+    }
+}

+ 530 - 0
runtime-modules/forum/src/mock.rs

@@ -0,0 +1,530 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{GenesisConfig, Module, Trait};
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+/// Module which has a full Substrate module for
+/// mocking behaviour of MembershipRegistry
+pub mod registry {
+
+    use super::*;
+    // use srml_support::*;
+
+    #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+    pub struct Member<AccountId> {
+        pub id: AccountId,
+    }
+
+    decl_storage! {
+        trait Store for Module<T: Trait> as MockForumUserRegistry {
+
+            pub ForumUserById get(forum_user_by_id) config(): map T::AccountId => Member<T::AccountId>;
+
+        }
+    }
+
+    decl_module! {
+        pub struct Module<T: Trait> for enum Call where origin: T::Origin {}
+    }
+
+    impl<T: Trait> Module<T> {
+        pub fn add_member(member: &Member<T::AccountId>) {
+            <ForumUserById<T>>::insert(member.id.clone(), member.clone());
+        }
+    }
+
+    impl<T: Trait> ForumUserRegistry<T::AccountId> for Module<T> {
+        fn get_forum_user(id: &T::AccountId) -> Option<ForumUser<T::AccountId>> {
+            if <ForumUserById<T>>::exists(id) {
+                let m = <ForumUserById<T>>::get(id);
+
+                Some(ForumUser { id: m.id })
+            } else {
+                None
+            }
+        }
+    }
+
+    pub type TestMembershipRegistryModule = Module<Runtime>;
+}
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    // type WeightMultiplierUpdate = ();
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl Trait for Runtime {
+    type Event = ();
+    type MembershipRegistry = registry::TestMembershipRegistryModule;
+}
+
+#[derive(Clone)]
+pub enum OriginType {
+    Signed(<Runtime as system::Trait>::AccountId),
+    //Inherent, <== did not find how to make such an origin yet
+    Root,
+}
+
+pub fn mock_origin(origin: OriginType) -> mock::Origin {
+    match origin {
+        OriginType::Signed(account_id) => Origin::signed(account_id),
+        //OriginType::Inherent => Origin::inherent,
+        OriginType::Root => system::RawOrigin::Root.into(), //Origin::root
+    }
+}
+
+pub const NOT_FORUM_SUDO_ORIGIN: OriginType = OriginType::Signed(111);
+
+pub const NOT_MEMBER_ORIGIN: OriginType = OriginType::Signed(222);
+
+pub const INVLAID_CATEGORY_ID: CategoryId = 333;
+
+pub const INVLAID_THREAD_ID: ThreadId = 444;
+
+pub const INVLAID_POST_ID: ThreadId = 555;
+
+pub fn generate_text(len: usize) -> Vec<u8> {
+    vec![b'x'; len]
+}
+
+pub fn good_category_title() -> Vec<u8> {
+    b"Great new category".to_vec()
+}
+
+pub fn good_category_description() -> Vec<u8> {
+    b"This is a great new category for the forum".to_vec()
+}
+
+pub fn good_thread_title() -> Vec<u8> {
+    b"Great new thread".to_vec()
+}
+
+pub fn good_thread_text() -> Vec<u8> {
+    b"The first post in this thread".to_vec()
+}
+
+pub fn good_post_text() -> Vec<u8> {
+    b"A response in the thread".to_vec()
+}
+
+pub fn good_rationale() -> Vec<u8> {
+    b"This post violates our community rules".to_vec()
+}
+
+/*
+ * These test fixtures can be heavily refactored to avoid repotition, needs macros, and event
+ * assertions are also missing.
+ */
+
+pub struct CreateCategoryFixture {
+    pub origin: OriginType,
+    pub parent: Option<CategoryId>,
+    pub title: Vec<u8>,
+    pub description: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_category(
+                mock_origin(self.origin.clone()),
+                self.parent,
+                self.title.clone(),
+                self.description.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct UpdateCategoryFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub new_archival_status: Option<bool>,
+    pub new_deletion_status: Option<bool>,
+    pub result: dispatch::Result,
+}
+
+impl UpdateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::update_category(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.new_archival_status.clone(),
+                self.new_deletion_status.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreateThreadFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub title: Vec<u8>,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateThreadFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_thread(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.title.clone(),
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreatePostFixture {
+    pub origin: OriginType,
+    pub thread_id: ThreadId,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreatePostFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::add_post(
+                mock_origin(self.origin.clone()),
+                self.thread_id,
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub fn create_forum_member() -> OriginType {
+    let member_id = 123;
+    let new_member = registry::Member { id: member_id };
+    registry::TestMembershipRegistryModule::add_member(&new_member);
+    OriginType::Signed(member_id)
+}
+
+pub fn assert_create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+    expected_result: dispatch::Result,
+) {
+    CreateCategoryFixture {
+        origin: forum_sudo,
+        parent: parent_category_id,
+        title: good_category_title(),
+        description: good_category_description(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_thread(
+    forum_sudo: OriginType,
+    category_id: CategoryId,
+    expected_result: dispatch::Result,
+) {
+    CreateThreadFixture {
+        origin: forum_sudo,
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_post(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    expected_result: dispatch::Result,
+) {
+    CreatePostFixture {
+        origin: forum_sudo,
+        thread_id,
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+) -> CategoryId {
+    let category_id = TestForumModule::next_category_id();
+    assert_create_category(forum_sudo, parent_category_id, Ok(()));
+    category_id
+}
+
+pub fn create_root_category(forum_sudo: OriginType) -> CategoryId {
+    create_category(forum_sudo, None)
+}
+
+pub fn create_root_category_and_thread(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId) {
+    let member_origin = create_forum_member();
+    let category_id = create_root_category(forum_sudo);
+    let thread_id = TestForumModule::next_thread_id();
+
+    CreateThreadFixture {
+        origin: member_origin.clone(),
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id)
+}
+
+pub fn create_root_category_and_thread_and_post(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId, PostId) {
+    let (member_origin, category_id, thread_id) = create_root_category_and_thread(forum_sudo);
+    let post_id = TestForumModule::next_post_id();
+
+    CreatePostFixture {
+        origin: member_origin.clone(),
+        thread_id: thread_id.clone(),
+        text: good_post_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id, post_id)
+}
+
+pub fn moderate_thread(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_thread(mock_origin(forum_sudo), thread_id, rationale)
+}
+
+pub fn moderate_post(
+    forum_sudo: OriginType,
+    post_id: PostId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_post(mock_origin(forum_sudo), post_id, rationale)
+}
+
+pub fn archive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(true), None)
+}
+
+pub fn unarchive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(false), None)
+}
+
+pub fn delete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(true))
+}
+
+pub fn undelete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(false))
+}
+
+pub fn assert_not_forum_sudo_cannot_update_category(
+    update_operation: fn(OriginType, CategoryId) -> dispatch::Result,
+) {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin.clone());
+        assert_eq!(
+            update_operation(NOT_FORUM_SUDO_ORIGIN, category_id),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+// refactor
+/// - add each config as parameter, then
+///
+
+pub fn default_genesis_config() -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: vec![], // endowed_accounts.iter().cloned().map(|k|(k, 1 << 60)).collect(),
+        next_category_id: 1,
+        thread_by_id: vec![],
+        next_thread_id: 1,
+        post_by_id: vec![],
+        next_post_id: 1,
+
+        forum_sudo: 33,
+
+        category_title_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        category_description_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        thread_title_constraint: InputValidationLengthConstraint {
+            min: 3,
+            max_min_diff: 43,
+        },
+
+        post_text_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 1001,
+        },
+
+        thread_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        },
+
+        post_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        }, // JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+
+           // Extra genesis fields
+           //initial_forum_sudo: Some(143)
+    }
+}
+
+pub type RuntimeMap<K, V> = std::vec::Vec<(K, V)>;
+pub type RuntimeCategory = Category<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeThread = Thread<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimePost = Post<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeBlockchainTimestamp = BlockchainTimestamp<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+>;
+
+pub fn genesis_config(
+    category_by_id: &RuntimeMap<CategoryId, RuntimeCategory>,
+    next_category_id: u64,
+    thread_by_id: &RuntimeMap<ThreadId, RuntimeThread>,
+    next_thread_id: u64,
+    post_by_id: &RuntimeMap<PostId, RuntimePost>,
+    next_post_id: u64,
+    forum_sudo: <Runtime as system::Trait>::AccountId,
+    category_title_constraint: &InputValidationLengthConstraint,
+    category_description_constraint: &InputValidationLengthConstraint,
+    thread_title_constraint: &InputValidationLengthConstraint,
+    post_text_constraint: &InputValidationLengthConstraint,
+    thread_moderation_rationale_constraint: &InputValidationLengthConstraint,
+    post_moderation_rationale_constraint: &InputValidationLengthConstraint,
+) -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: category_by_id.clone(),
+        next_category_id: next_category_id,
+        thread_by_id: thread_by_id.clone(),
+        next_thread_id: next_thread_id,
+        post_by_id: post_by_id.clone(),
+        next_post_id: next_post_id,
+        forum_sudo: forum_sudo,
+        category_title_constraint: category_title_constraint.clone(),
+        category_description_constraint: category_description_constraint.clone(),
+        thread_title_constraint: thread_title_constraint.clone(),
+        post_text_constraint: post_text_constraint.clone(),
+        thread_moderation_rationale_constraint: thread_moderation_rationale_constraint.clone(),
+        post_moderation_rationale_constraint: post_moderation_rationale_constraint.clone(),
+    }
+}
+
+// MockForumUserRegistry
+pub fn default_mock_forum_user_registry_genesis_config() -> registry::GenesisConfig<Runtime> {
+    registry::GenesisConfig::<Runtime> {
+        forum_user_by_id: vec![],
+    }
+}
+
+// NB!:
+// Wanted to have payload: a: &GenesisConfig<Test>
+// but borrow checker made my life miserabl, so giving up for now.
+pub fn build_test_externalities(config: GenesisConfig<Runtime>) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    // Add mock registry configuration
+    default_mock_forum_user_registry_genesis_config()
+        .assimilate_storage(&mut t)
+        .unwrap();
+
+    t.into()
+}
+
+// pub type System = system::Module<Runtime>;
+
+/// Export forum module on a test runtime
+pub type TestForumModule = Module<Runtime>;

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

@@ -0,0 +1,981 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::{assert_err, assert_ok};
+
+/*
+* NB!: No test checks for event emission!!!!
+*/
+
+/*
+ * set_forum_sudo
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * set_forum_bad_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]
+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)
+        );
+
+        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)
+        );
+    });
+}
+
+/*
+ * 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]
+fn create_subcategory_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let root_category_id = create_root_category(origin.clone());
+        assert_create_category(origin, Some(root_category_id), Ok(()));
+    });
+}
+
+#[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),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: generate_text(max_len + 1),
+            description: good_category_description(),
+            result: Err(ERROR_CATEGORY_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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]
+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;
+
+    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();
+    });
+}
+
+/*
+ * 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.
+     */
+
+    let forum_sudo = 32;
+
+    let created_at = RuntimeBlockchainTimestamp { block: 0, time: 0 };
+
+    let category_by_id = vec![
+        // A root category
+        (
+            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();
+    });
+}
+
+/*
+ * create_thread
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_thread_bad_origin
+ * create_thread_forum_sudo_not_set
+ * ...
+ */
+
+#[test]
+fn create_thread_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    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: good_thread_title(),
+            text: good_thread_text(),
+            result: Ok(()),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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]
+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,
+            category_id,
+            title: generate_text(max_len + 1),
+            text: good_thread_text(),
+            result: Err(ERROR_THREAD_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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;
+
+    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: good_thread_title(),
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_post_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, _) = create_root_category_and_thread_and_post(origin);
+    });
+}
+
+#[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);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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;
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+// Test moderation:
+// -----------------------------------------------------------------------------
+
+#[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]
+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(())
+        );
+        assert_eq!(
+            moderate_thread(origin, thread_id, good_rationale()),
+            Err(ERROR_THREAD_ALREADY_MODERATED)
+        );
+    });
+}
+
+#[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]
+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;
+
+    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)
+        );
+    });
+}
+
+#[test]
+fn moderate_post_successfully() {
+    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.clone());
+        assert_eq!(moderate_post(origin, post_id, good_rationale()), 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;
+
+    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)
+        );
+    });
+}
+
+#[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;
+
+    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)
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_already_moderated_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.clone());
+        assert_eq!(
+            moderate_post(origin.clone(), post_id.clone(), good_rationale()),
+            Ok(())
+        );
+        assert_eq!(
+            moderate_post(origin, post_id, good_rationale()),
+            Err(ERROR_POST_MODERATED)
+        );
+    });
+}
+
+// Not a forum sudo:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_forum_sudo_cannot_create_root_category() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(
+            NOT_FORUM_SUDO_ORIGIN,
+            None,
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_create_subcategory() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    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),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_archive_category() {
+    assert_not_forum_sudo_cannot_update_category(archive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_unarchive_category() {
+    assert_not_forum_sudo_cannot_update_category(unarchive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_delete_category() {
+    assert_not_forum_sudo_cannot_update_category(delete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_undelete_category() {
+    assert_not_forum_sudo_cannot_update_category(undelete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_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(NOT_FORUM_SUDO_ORIGIN, thread_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_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.clone());
+        assert_eq!(
+            moderate_post(NOT_FORUM_SUDO_ORIGIN, post_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// Not a member:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_member_cannot_create_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    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();
+    });
+}
+
+#[test]
+fn not_member_cannot_create_post() {
+    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);
+        CreatePostFixture {
+            origin: NOT_MEMBER_ORIGIN,
+            thread_id,
+            text: good_post_text(),
+            result: Err(ERROR_NOT_FORUM_USER),
+        }
+        .call_and_assert();
+    });
+}
+
+#[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
+        );
+    });
+}
+
+// Invalid id passed:
+// -----------------------------------------------------------------------------
+
+#[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(
+            origin,
+            Some(INVLAID_CATEGORY_ID),
+            Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+        );
+    });
+}
+
+#[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();
+    });
+}
+
+#[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]
+fn cannot_moderate_thread_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_thread(origin, INVLAID_THREAD_ID, good_rationale()),
+            ERROR_THREAD_DOES_NOT_EXIST
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_post_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_post(origin, INVLAID_POST_ID, good_rationale()),
+            ERROR_POST_DOES_NOT_EXIST
+        );
+    });
+}
+
+// Successfull extrinsics
+// -----------------------------------------------------------------------------
+
+#[test]
+fn archive_then_unarchive_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!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert archived == true.
+
+        assert_ok!(unarchive_category(forum_sudo, category_id,));
+        // TODO get category by id and assert archived == false.
+    });
+}
+
+#[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),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_subcategory_in_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_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_category(
+            forum_sudo,
+            Some(category_id),
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[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(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_in_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_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_thread(
+            create_forum_member(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_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());
+        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),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_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());
+        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),
+        );
+    });
+}
+
+#[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]
+fn cannot_edit_post_in_moderated_thread() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    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
+        );
+    });
+}
+
+// TODO impl
+// #[test]
+// fn cannot_edit_moderated_post() {}