فهرست منبع

Merge pull request #405 from shamil-gadelshin/bureaucracy_iteration2

Bureaucracy module. Iteration 2. Hiring flow migration.
Bedeho Mender 4 سال پیش
والد
کامیت
c6f45e5814

+ 5 - 0
Cargo.lock

@@ -4479,6 +4479,7 @@ dependencies = [
  "substrate-hiring-module",
  "substrate-membership-module",
  "substrate-primitives",
+ "substrate-recurring-reward-module",
  "substrate-stake-module",
  "substrate-token-mint-module",
 ]
@@ -4603,6 +4604,8 @@ dependencies = [
 name = "substrate-common-module"
 version = "1.0.0"
 dependencies = [
+ "parity-scale-codec",
+ "serde",
  "sr-primitives",
  "srml-support",
  "srml-system",
@@ -4842,9 +4845,11 @@ dependencies = [
  "srml-timestamp",
  "substrate-bureaucracy-module",
  "substrate-common-module",
+ "substrate-hiring-module",
  "substrate-membership-module",
  "substrate-primitives",
  "substrate-recurring-reward-module",
+ "substrate-stake-module",
  "substrate-token-mint-module",
 ]
 

+ 12 - 5
runtime-modules/bureaucracy/Cargo.toml

@@ -17,6 +17,8 @@ std = [
     'stake/std',
     'membership/std',
     'minting/std',
+    'recurringrewards/std',
+    'common/std',
 ]
 
 [dependencies.primitives]
@@ -80,6 +82,16 @@ default_features = false
 package = 'substrate-token-mint-module'
 path = '../token-minting'
 
+[dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'
+
 [dev-dependencies.runtime-io]
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'
@@ -92,11 +104,6 @@ git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-balances'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 
-[dev-dependencies.common]
-default_features = false
-package = 'substrate-common-module'
-path = '../common'
-
 [dev-dependencies.timestamp]
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'

+ 289 - 0
runtime-modules/bureaucracy/src/errors.rs

@@ -0,0 +1,289 @@
+use membership::members;
+
+//TODO: convert messages to the decl_error! entries
+pub mod bureaucracy_errors {
+    pub static MSG_CURRENT_LEAD_NOT_SET: &str = "Current lead is not set";
+    pub static MSG_IS_NOT_LEAD_ACCOUNT: &str = "Not a lead account";
+    pub static MSG_OPENING_TEXT_TOO_SHORT: &str = "Opening text too short";
+    pub static MSG_OPENING_TEXT_TOO_LONG: &str = "Opening text too long";
+    pub static MSG_WORKER_OPENING_DOES_NOT_EXIST: &str = "Worker opening does not exist";
+    pub static MSG_INSUFFICIENT_BALANCE_TO_APPLY: &str = "Insufficient balance to apply";
+    pub static MSG_APPLY_ON_WORKER_OPENING_UNSIGNED_ORIGIN: &str = "Unsigned origin";
+    pub static MSG_APPLY_ON_WORKER_OPENING_MEMBER_ID_INVALID: &str = "Member id is invalid";
+    pub static MSG_APPLY_ON_WORKER_OPENING_SIGNER_NOT_CONTROLLER_ACCOUNT: &str =
+        "Signer does not match controller account";
+    pub static MSG_ORIGIN_IS_NEITHER_MEMBER_CONTROLLER_OR_ROOT: &str =
+        "Origin must be controller or root account of member";
+    pub static MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING: &str =
+        "Member already has an active application on the opening";
+    pub static MSG_WORKER_APPLICATION_TEXT_TOO_LONG: &str = "Worker application text too long";
+    pub static MSG_WORKER_APPLICATION_TEXT_TOO_SHORT: &str = "Worker application text too short";
+    pub static MSG_INSUFFICIENT_BALANCE_TO_COVER_STAKE: &str =
+        "Insuffieicnt balance to cover stake";
+    pub static MSG_ORIGIN_IS_NOT_APPLICANT: &str = "Origin is not applicant";
+    pub static MSG_WORKER_APPLICATION_DOES_NOT_EXIST: &str = "Worker application does not exist";
+    pub static MSG_SUCCESSFUL_WORKER_APPLICATION_DOES_NOT_EXIST: &str =
+        "Successful worker application does not exist";
+    pub static MSG_FILL_WORKER_OPENING_INVALID_NEXT_PAYMENT_BLOCK: &str =
+        "Reward policy has invalid next payment block number";
+    pub static MSG_FILL_WORKER_OPENING_MINT_DOES_NOT_EXIST: &str =
+        "Working group mint does not exist";
+}
+/*
+ * The errors below, while in many cases encoding similar outcomes,
+ * are scoped to the specific extrinsic for which they are used.
+ * The reason for this is that it will later to easier to convert this
+ * representation into into the type safe error encoding coming in
+ * later versions of Substrate.
+ */
+
+// Errors for `accept_worker_applications`
+pub static MSG_ACCEPT_WORKER_APPLICATIONS_OPENING_DOES_NOT_EXIST: &str = "Opening does not exist";
+pub static MSG_ACCEPT_WORKER_APPLICATIONS_OPENING_IS_NOT_WAITING_TO_BEGIN: &str =
+    "Opening Is Not in Waiting to begin";
+
+// Errors for `begin_worker_applicant_review`
+pub static MSG_BEGIN_WORKER_APPLICANT_REVIEW_OPENING_DOES_NOT_EXIST: &str =
+    "Opening does not exist";
+pub static MSG_BEGIN_WORKER_APPLICANT_REVIEW_OPENING_OPENING_IS_NOT_WAITING_TO_BEGIN: &str =
+    "Opening Is Not in Waiting";
+
+// Errors for `fill_worker_opening`
+pub static MSG_FULL_WORKER_OPENING_OPENING_DOES_NOT_EXIST: &str = "OpeningDoesNotExist";
+pub static MSG_FULL_WORKER_OPENING_OPENING_NOT_IN_REVIEW_PERIOD_STAGE: &str =
+    "OpeningNotInReviewPeriodStage";
+pub static MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_TOO_SHORT: &str =
+    "Application stake unstaking period for successful applicants too short";
+pub static MSG_FULL_WORKER_OPENING_SUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_TOO_SHORT: &str =
+    "Application stake unstaking period for failed applicants too short";
+pub static MSG_FULL_WORKER_OPENING_SUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_TOO_SHORT: &str =
+    "Role stake unstaking period for successful applicants too short";
+pub static MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_TOO_SHORT: &str =
+    "Role stake unstaking period for failed applicants too short";
+pub static MSG_FULL_WORKER_OPENING_SUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_REDUNDANT: &str =
+    "Application stake unstaking period for successful applicants redundant";
+pub static MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_REDUNDANT: &str =
+    "Application stake unstaking period for failed applicants redundant";
+pub static MSG_FULL_WORKER_OPENING_SUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_REDUNDANT: &str =
+    "Role stake unstaking period for successful applicants redundant";
+pub static MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_REDUNDANT: &str =
+    "Role stake unstaking period for failed applicants redundant";
+pub static MSG_FULL_WORKER_OPENING_APPLICATION_DOES_NOT_EXIST: &str = "ApplicationDoesNotExist";
+pub static MSG_FULL_WORKER_OPENING_APPLICATION_NOT_ACTIVE: &str = "ApplicationNotInActiveStage";
+pub static MSG_FILL_WORKER_OPENING_APPLICATION_FOR_WRONG_OPENING: &str =
+    "Applications not for opening";
+// Errors for `withdraw_worker_application`
+pub static MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_DOES_NOT_EXIST: &str =
+    "ApplicationDoesNotExist";
+pub static MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_NOT_ACTIVE: &str = "ApplicationNotActive";
+pub static MSG_WITHDRAW_WORKER_APPLICATION_OPENING_NOT_ACCEPTING_APPLICATIONS: &str =
+    "OpeningNotAcceptingApplications";
+pub static MSG_WITHDRAW_WORKER_APPLICATION_UNSTAKING_PERIOD_TOO_SHORT: &str =
+    "UnstakingPeriodTooShort ..."; // <== SHOULD REALLY BE TWO SEPARATE, ONE FOR EACH STAKING PURPOSE
+pub static MSG_WITHDRAW_WORKER_APPLICATION_REDUNDANT_UNSTAKING_PERIOD: &str =
+    "RedundantUnstakingPeriodProvided ...";
+
+// Errors for `create_channel`
+pub static MSG_CREATE_CHANNEL_IS_NOT_MEMBER: &str = "Is not a member";
+pub static MSG_CREATE_CHANNEL_NOT_CONTROLLER_ACCOUNT: &str =
+    "Account is not controller account of member";
+
+// Errors for `add_worker_opening`
+pub static MSG_ADD_WORKER_OPENING_ACTIVATES_IN_THE_PAST: &str =
+    "Opening does not activate in the future";
+pub static MSG_ADD_WORKER_OPENING_ROLE_STAKE_LESS_THAN_MINIMUM: &str =
+    "Role stake amount less than minimum currency balance";
+pub static MSG_ADD_WORKER_OPENING_APPLIICATION_STAKE_LESS_THAN_MINIMUM: &str =
+    "Application stake amount less than minimum currency balance";
+pub static MSG_ADD_WORKER_OPENING_OPENING_DOES_NOT_EXIST: &str = "OpeningDoesNotExist";
+pub static MSG_ADD_WORKER_OPENING_STAKE_PROVIDED_WHEN_REDUNDANT: &str =
+    "StakeProvidedWhenRedundant ..."; // <== SHOULD REALLY BE TWO SEPARATE, ONE FOR EACH STAKING PURPOSE
+pub static MSG_ADD_WORKER_OPENING_STAKE_MISSING_WHEN_REQUIRED: &str =
+    "StakeMissingWhenRequired ..."; // <== SHOULD REALLY BE TWO SEPARATE, ONE FOR EACH STAKING PURPOSE
+pub static MSG_ADD_WORKER_OPENING_STAKE_AMOUNT_TOO_LOW: &str = "StakeAmountTooLow ..."; // <== SHOULD REALLY BE TWO SEPARATE, ONE FOR EACH STAKING PURPOSE
+pub static MSG_ADD_WORKER_OPENING_OPENING_NOT_IN_ACCEPTING_APPLICATION_STAGE: &str =
+    "OpeningNotInAcceptingApplicationsStage";
+pub static MSG_ADD_WORKER_OPENING_NEW_APPLICATION_WAS_CROWDED_OUT: &str =
+    "NewApplicationWasCrowdedOut";
+pub static MSG_ADD_WORKER_OPENING_ZERO_MAX_APPLICANT_COUNT: &str =
+    "Application rationing has zero max active applicants";
+
+/// Error wrapper for external module error conversions
+pub struct WrappedError<E> {
+    // can this be made generic, or does that undermine the whole orhpan rule spirit?
+    pub error: E,
+}
+
+#[macro_export]
+macro_rules! ensure_on_wrapped_error {
+    ($call:expr) => {{
+        { $call }.map_err(|err| crate::WrappedError { error: err })
+    }};
+}
+
+// Add macro here to make this
+//derive_from_impl(hiring::BeginAcceptingApplicationsError)
+//derive_from_impl(hiring::BeginAcceptingApplicationsError)
+
+impl rstd::convert::From<WrappedError<hiring::BeginAcceptingApplicationsError>> for &str {
+    fn from(wrapper: WrappedError<hiring::BeginAcceptingApplicationsError>) -> Self {
+        match wrapper.error {
+            hiring::BeginAcceptingApplicationsError::OpeningDoesNotExist => {
+                MSG_ACCEPT_WORKER_APPLICATIONS_OPENING_DOES_NOT_EXIST
+            }
+            hiring::BeginAcceptingApplicationsError::OpeningIsNotInWaitingToBeginStage => {
+                MSG_ACCEPT_WORKER_APPLICATIONS_OPENING_IS_NOT_WAITING_TO_BEGIN
+            }
+        }
+    }
+}
+
+impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for &str {
+    fn from(wrapper: WrappedError<hiring::AddOpeningError>) -> Self {
+        match wrapper.error {
+            hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
+                MSG_ADD_WORKER_OPENING_ACTIVATES_IN_THE_PAST
+            }
+            hiring::AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(purpose) => {
+                match purpose {
+                    hiring::StakePurpose::Role => {
+                        MSG_ADD_WORKER_OPENING_ROLE_STAKE_LESS_THAN_MINIMUM
+                    }
+                    hiring::StakePurpose::Application => {
+                        MSG_ADD_WORKER_OPENING_APPLIICATION_STAKE_LESS_THAN_MINIMUM
+                    }
+                }
+            }
+            hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
+                MSG_ADD_WORKER_OPENING_ZERO_MAX_APPLICANT_COUNT
+            }
+        }
+    }
+}
+
+impl rstd::convert::From<WrappedError<hiring::BeginReviewError>> for &str {
+    fn from(wrapper: WrappedError<hiring::BeginReviewError>) -> Self {
+        match wrapper.error {
+            hiring::BeginReviewError::OpeningDoesNotExist => {
+                MSG_BEGIN_WORKER_APPLICANT_REVIEW_OPENING_DOES_NOT_EXIST
+            }
+            hiring::BeginReviewError::OpeningNotInAcceptingApplicationsStage => {
+                MSG_BEGIN_WORKER_APPLICANT_REVIEW_OPENING_OPENING_IS_NOT_WAITING_TO_BEGIN
+            }
+        }
+    }
+}
+
+impl<T: hiring::Trait> rstd::convert::From<WrappedError<hiring::FillOpeningError<T>>> for &str {
+    fn from(wrapper: WrappedError<hiring::FillOpeningError<T>>) -> Self {
+        match wrapper.error {
+			hiring::FillOpeningError::<T>::OpeningDoesNotExist => MSG_FULL_WORKER_OPENING_OPENING_DOES_NOT_EXIST,
+			hiring::FillOpeningError::<T>::OpeningNotInReviewPeriodStage => MSG_FULL_WORKER_OPENING_OPENING_NOT_IN_REVIEW_PERIOD_STAGE,
+			hiring::FillOpeningError::<T>::UnstakingPeriodTooShort(
+				stake_purpose,
+				outcome_in_filled_opening,
+			) => match stake_purpose {
+				hiring::StakePurpose::Application => match outcome_in_filled_opening {
+					hiring::ApplicationOutcomeInFilledOpening::Success => MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_TOO_SHORT,
+					hiring::ApplicationOutcomeInFilledOpening::Failure => MSG_FULL_WORKER_OPENING_SUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_TOO_SHORT
+				},
+				hiring::StakePurpose::Role => match outcome_in_filled_opening {
+					hiring::ApplicationOutcomeInFilledOpening::Success => MSG_FULL_WORKER_OPENING_SUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_TOO_SHORT,
+					hiring::ApplicationOutcomeInFilledOpening::Failure => MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_TOO_SHORT
+				},
+			},
+			hiring::FillOpeningError::<T>::RedundantUnstakingPeriodProvided(
+				stake_purpose,
+				outcome_in_filled_opening,
+			) => match stake_purpose {
+				hiring::StakePurpose::Application => match outcome_in_filled_opening {
+					hiring::ApplicationOutcomeInFilledOpening::Success => MSG_FULL_WORKER_OPENING_SUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_REDUNDANT,
+					hiring::ApplicationOutcomeInFilledOpening::Failure => MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_APPLICATION_STAKE_UNSTAKING_PERIOD_REDUNDANT
+				},
+				hiring::StakePurpose::Role => match outcome_in_filled_opening {
+					hiring::ApplicationOutcomeInFilledOpening::Success => MSG_FULL_WORKER_OPENING_SUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_REDUNDANT,
+					hiring::ApplicationOutcomeInFilledOpening::Failure => MSG_FULL_WORKER_OPENING_UNSUCCESSFUL_ROLE_STAKE_UNSTAKING_PERIOD_REDUNDANT
+				},
+			},
+			hiring::FillOpeningError::<T>::ApplicationDoesNotExist(_application_id) => MSG_FULL_WORKER_OPENING_APPLICATION_DOES_NOT_EXIST,
+			hiring::FillOpeningError::<T>::ApplicationNotInActiveStage(_application_id) => MSG_FULL_WORKER_OPENING_APPLICATION_NOT_ACTIVE,
+			hiring::FillOpeningError::<T>::ApplicationForWrongOpening(_application_id) => MSG_FILL_WORKER_OPENING_APPLICATION_FOR_WRONG_OPENING,
+		}
+    }
+}
+
+impl rstd::convert::From<WrappedError<hiring::DeactivateApplicationError>> for &str {
+    fn from(wrapper: WrappedError<hiring::DeactivateApplicationError>) -> Self {
+        match wrapper.error {
+            hiring::DeactivateApplicationError::ApplicationDoesNotExist => {
+                MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_DOES_NOT_EXIST
+            }
+            hiring::DeactivateApplicationError::ApplicationNotActive => {
+                MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_NOT_ACTIVE
+            }
+            hiring::DeactivateApplicationError::OpeningNotAcceptingApplications => {
+                MSG_WITHDRAW_WORKER_APPLICATION_OPENING_NOT_ACCEPTING_APPLICATIONS
+            }
+            hiring::DeactivateApplicationError::UnstakingPeriodTooShort(_stake_purpose) => {
+                MSG_WITHDRAW_WORKER_APPLICATION_UNSTAKING_PERIOD_TOO_SHORT
+            }
+            hiring::DeactivateApplicationError::RedundantUnstakingPeriodProvided(
+                _stake_purpose,
+            ) => MSG_WITHDRAW_WORKER_APPLICATION_REDUNDANT_UNSTAKING_PERIOD,
+        }
+    }
+}
+
+impl rstd::convert::From<WrappedError<members::ControllerAccountForMemberCheckFailed>> for &str {
+    fn from(wrapper: WrappedError<members::ControllerAccountForMemberCheckFailed>) -> Self {
+        match wrapper.error {
+            members::ControllerAccountForMemberCheckFailed::NotMember => {
+                MSG_CREATE_CHANNEL_IS_NOT_MEMBER
+            }
+            members::ControllerAccountForMemberCheckFailed::NotControllerAccount => {
+                MSG_CREATE_CHANNEL_NOT_CONTROLLER_ACCOUNT
+            }
+        }
+    }
+}
+
+impl rstd::convert::From<WrappedError<hiring::AddApplicationError>> for &str {
+    fn from(wrapper: WrappedError<hiring::AddApplicationError>) -> Self {
+        match wrapper.error {
+            hiring::AddApplicationError::OpeningDoesNotExist => {
+                MSG_ADD_WORKER_OPENING_OPENING_DOES_NOT_EXIST
+            }
+            hiring::AddApplicationError::StakeProvidedWhenRedundant(_stake_purpose) => {
+                MSG_ADD_WORKER_OPENING_STAKE_PROVIDED_WHEN_REDUNDANT
+            }
+            hiring::AddApplicationError::StakeMissingWhenRequired(_stake_purpose) => {
+                MSG_ADD_WORKER_OPENING_STAKE_MISSING_WHEN_REQUIRED
+            }
+            hiring::AddApplicationError::StakeAmountTooLow(_stake_purpose) => {
+                MSG_ADD_WORKER_OPENING_STAKE_AMOUNT_TOO_LOW
+            }
+            hiring::AddApplicationError::OpeningNotInAcceptingApplicationsStage => {
+                MSG_ADD_WORKER_OPENING_OPENING_NOT_IN_ACCEPTING_APPLICATION_STAGE
+            }
+            hiring::AddApplicationError::NewApplicationWasCrowdedOut => {
+                MSG_ADD_WORKER_OPENING_NEW_APPLICATION_WAS_CROWDED_OUT
+            }
+        }
+    }
+}
+
+impl rstd::convert::From<WrappedError<members::MemberControllerAccountDidNotSign>> for &str {
+    fn from(wrapper: WrappedError<members::MemberControllerAccountDidNotSign>) -> Self {
+        match wrapper.error {
+            members::MemberControllerAccountDidNotSign::UnsignedOrigin => {
+                bureaucracy_errors::MSG_APPLY_ON_WORKER_OPENING_UNSIGNED_ORIGIN
+            }
+            members::MemberControllerAccountDidNotSign::MemberIdInvalid => {
+                bureaucracy_errors::MSG_APPLY_ON_WORKER_OPENING_MEMBER_ID_INVALID
+            }
+            members::MemberControllerAccountDidNotSign::SignerControllerAccountMismatch => {
+                bureaucracy_errors::MSG_APPLY_ON_WORKER_OPENING_SIGNER_NOT_CONTROLLER_ACCOUNT
+            }
+        }
+    }
+}

+ 702 - 25
runtime-modules/bureaucracy/src/lib.rs

@@ -1,27 +1,124 @@
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
+#[cfg(test)]
+mod tests;
 mod types;
+#[macro_use]
+mod errors;
 
-use sr_primitives::traits::EnsureOrigin;
-use srml_support::{decl_event, decl_module, decl_storage, dispatch};
-use system::{ensure_root, RawOrigin};
+use rstd::collections::btree_map::BTreeMap;
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+use rstd::vec::Vec;
+use sr_primitives::traits::{EnsureOrigin, One, Zero};
+use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+use system::{ensure_root, ensure_signed, RawOrigin};
 
-use types::Lead;
+use common::constraints::InputValidationLengthConstraint;
+use errors::bureaucracy_errors::*;
+use errors::WrappedError;
+use types::{
+    Lead, OpeningPolicyCommitment, RewardPolicy, Worker, WorkerApplication, WorkerOpening,
+    WorkerRoleStage, WorkerRoleStakeProfile,
+};
 
-//TODO: convert messages to the decl_error! entries
-pub static MSG_ORIGIN_IS_NOT_LEAD: &str = "Origin is not lead";
-pub static MSG_CURRENT_LEAD_NOT_SET: &str = "Current lead is not set";
-pub static MSG_CURRENT_LEAD_ALREADY_SET: &str = "Current lead is already set";
-pub static MSG_IS_NOT_LEAD_ACCOUNT: &str = "Not a lead account";
+//TODO: docs
+//TODO: migrate to decl_error
+//TODO: 'roles' extrinsics
+//TODO: initialize a mint!
 
 /// Alias for the _Lead_ type
 pub type LeadOf<T> =
     Lead<<T as membership::members::Trait>::MemberId, <T as system::Trait>::AccountId>;
 
+/*
++ add_worker_opening
++ accept_worker_applications
++ begin_worker_applicant_review
++ fill_worker_opening
++ withdraw_worker_application
++ terminate_worker_application
++ apply_on_worker_opening
+*/
+
+/// Workaround for BTreeSet type
+pub type WorkerApplicationIdSet<T> = BTreeSet<WorkerApplicationId<T>>;
+
+/// Type for the identifier for an opening for a worker.
+pub type WorkerOpeningId<T> = <T as hiring::Trait>::OpeningId;
+
+/// Type for the identifier for an application as a worker.
+pub type WorkerApplicationId<T> = <T as hiring::Trait>::ApplicationId;
+
+/// Balance type of runtime
+pub type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Balance type of runtime
+pub type CurrencyOf<T> = <T as stake::Trait>::Currency;
+
+/// Negative imbalance of runtime.
+pub type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+/// Alias for the worker application id to the worker id dictionary
+pub type WorkerApplicationIdToWorkerIdMap<T> = BTreeMap<WorkerApplicationId<T>, WorkerId<T>>;
+
+/// Type identifier for worker role, which must be same as membership actor identifier
+pub type WorkerId<T> = <T as membership::members::Trait>::ActorId;
+
+// Type simplification
+type WorkerOpeningInfo<T> = (
+    WorkerOpening<
+        <T as hiring::Trait>::OpeningId,
+        <T as system::Trait>::BlockNumber,
+        BalanceOf<T>,
+        WorkerApplicationId<T>,
+    >,
+    hiring::Opening<
+        BalanceOf<T>,
+        <T as system::Trait>::BlockNumber,
+        <T as hiring::Trait>::ApplicationId,
+    >,
+);
+
+// Type simplification
+type WorkerApplicationInfo<T> = (
+    WorkerApplication<
+        <T as system::Trait>::AccountId,
+        WorkerOpeningId<T>,
+        <T as membership::members::Trait>::MemberId,
+        <T as hiring::Trait>::ApplicationId,
+    >,
+    WorkerApplicationId<T>,
+    WorkerOpening<
+        <T as hiring::Trait>::OpeningId,
+        <T as system::Trait>::BlockNumber,
+        BalanceOf<T>,
+        WorkerApplicationId<T>,
+    >,
+);
+
+// Type simplification
+type WorkerOf<T> = Worker<
+    <T as system::Trait>::AccountId,
+    <T as recurringrewards::Trait>::RewardRelationshipId,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::BlockNumber,
+>;
+
 /// The bureaucracy main _Trait_
-pub trait Trait<I: Instance>: system::Trait + membership::members::Trait {
-    /// Bureaucracy event type.
+pub trait Trait<I: Instance>:
+    system::Trait
+    + membership::members::Trait
+    + hiring::Trait
+    + minting::Trait
+    + stake::Trait
+    + recurringrewards::Trait
+{
+    /// Engine event type.
     type Event: From<Event<Self, I>> + Into<<Self as system::Trait>::Event>;
 }
 
@@ -30,20 +127,80 @@ decl_event!(
     pub enum Event<T, I>
     where
         <T as membership::members::Trait>::MemberId,
-        <T as system::Trait>::AccountId
+        <T as system::Trait>::AccountId,
+        WorkerOpeningId = WorkerOpeningId<T>,
+        WorkerApplicationId = WorkerApplicationId<T>,
+        WorkerApplicationIdToWorkerIdMap = WorkerApplicationIdToWorkerIdMap<T>,
     {
         /// Emits on setting the leader.
         /// Params:
         /// - Member id of the leader.
         /// - Role account id of the leader.
         LeaderSet(MemberId, AccountId),
+        /// Emits on adding new worker opening.
+        /// Params:
+        /// - Worker opening id
+        WorkerOpeningAdded(WorkerOpeningId),
+        /// Emits on accepting application for the worker opening.
+        /// Params:
+        /// - Worker opening id
+        AcceptedWorkerApplications(WorkerOpeningId),
+        /// Emits on adding the application for the worker opening.
+        /// Params:
+        /// - Worker opening id
+        /// - Worker application id
+        AppliedOnWorkerOpening(WorkerOpeningId, WorkerApplicationId),
+        /// Emits on withdrawing the application for the worker opening.
+        /// Params:
+        /// - Worker application id
+        WorkerApplicationWithdrawn(WorkerApplicationId),
+        /// Emits on terminating the application for the worker opening.
+        /// Params:
+        /// - Worker application id
+        WorkerApplicationTerminated(WorkerApplicationId),
+        /// Emits on beginning the application review for the worker opening.
+        /// Params:
+        /// - Worker opening id
+        BeganWorkerApplicationReview(WorkerOpeningId),
+        /// Emits on filling the worker opening.
+        /// Params:
+        /// - Worker opening id
+        /// - Worker application id to the worker id dictionary
+        WorkerOpeningFilled(WorkerOpeningId, WorkerApplicationIdToWorkerIdMap),
     }
 );
 
 decl_storage! {
     trait Store for Module<T: Trait<I>, I: Instance> as Bureaucracy {
+        /// The mint currently funding the rewards for this module.
+        pub Mint get(mint) : <T as minting::Trait>::MintId;
+
         /// The current lead.
         pub CurrentLead get(current_lead) : Option<LeadOf<T>>;
+
+        /// Next identifier value for new worker opening.
+        pub NextWorkerOpeningId get(next_worker_opening_id): WorkerOpeningId<T>;
+
+        /// Maps identifier to worker opening.
+        pub WorkerOpeningById get(worker_opening_by_id): linked_map WorkerOpeningId<T> => WorkerOpening<T::OpeningId, T::BlockNumber, BalanceOf<T>, WorkerApplicationId<T>>;
+
+        /// Opening human readable text length limits
+        pub OpeningHumanReadableText get(opening_human_readable_text): InputValidationLengthConstraint;
+
+        /// Maps identifier to worker application on opening.
+        pub WorkerApplicationById get(worker_application_by_id) : linked_map WorkerApplicationId<T> => WorkerApplication<T::AccountId, WorkerOpeningId<T>, T::MemberId, T::ApplicationId>;
+
+        /// Next identifier value for new worker application.
+        pub NextWorkerApplicationId get(next_worker_application_id) : WorkerApplicationId<T>;
+
+        /// Worker application human readable text length limits
+        pub WorkerApplicationHumanReadableText get(worker_application_human_readable_text) : InputValidationLengthConstraint;
+
+        /// Maps identifier to corresponding worker.
+        pub WorkerById get(worker_by_id) : linked_map WorkerId<T> => WorkerOf<T>;
+
+        /// Next identifier for new worker.
+        pub NextWorkerId get(next_worker_id) : WorkerId<T>;
     }
 }
 
@@ -72,23 +229,391 @@ decl_module! {
 
             Ok(())
         }
-    }
-}
 
-impl<T: Trait<I>, I: Instance> Module<T, I> {
-    /// Checks that provided lead account id belongs to the current bureaucracy leader
-    pub fn ensure_is_lead_account(lead_account_id: T::AccountId) -> Result<(), &'static str> {
-        let lead = <CurrentLead<T, I>>::get();
+         /// Add an opening for a worker role.
+        pub fn add_worker_opening(
+            origin,
+            activate_at: hiring::ActivateOpeningAt<T::BlockNumber>,
+            commitment: OpeningPolicyCommitment<T::BlockNumber,
+            BalanceOf<T>>,
+            human_readable_text: Vec<u8>
+        ){
+            // Ensure lead is set and is origin signer
+            Self::ensure_origin_is_set_lead(origin)?;
 
-        if let Some(lead) = lead {
-            if lead.role_account_id != lead_account_id {
-                return Err(MSG_IS_NOT_LEAD_ACCOUNT);
-            }
-        } else {
-            return Err(MSG_CURRENT_LEAD_NOT_SET);
+            Self::ensure_opening_human_readable_text_is_valid(&human_readable_text)?;
+
+            // Add opening
+            // NB: This call can in principle fail, because the staking policies
+            // may not respect the minimum currency requirement.
+
+            let policy_commitment = commitment.clone();
+
+            // mutation
+
+            let opening_id = ensure_on_wrapped_error!(
+                hiring::Module::<T>::add_opening(
+                    activate_at,
+                    commitment.max_review_period_length,
+                    commitment.application_rationing_policy,
+                    commitment.application_staking_policy,
+                    commitment.role_staking_policy,
+                    human_readable_text,
+            ))?;
+
+            let new_worker_opening_id = NextWorkerOpeningId::<T, I>::get();
+
+            // Create and add worker opening.
+            let new_opening_by_id = WorkerOpening::<WorkerOpeningId<T>, T::BlockNumber, BalanceOf<T>, WorkerApplicationId<T>> {
+                opening_id : opening_id,
+                worker_applications: BTreeSet::new(),
+                policy_commitment: policy_commitment
+            };
+
+            WorkerOpeningById::<T, I>::insert(new_worker_opening_id, new_opening_by_id);
+
+            // Update NextWorkerOpeningId
+            NextWorkerOpeningId::<T, I>::mutate(|id| *id += <WorkerOpeningId<T> as One>::one());
+
+            // Trigger event
+            Self::deposit_event(RawEvent::WorkerOpeningAdded(new_worker_opening_id));
         }
 
-        Ok(())
+            /// Begin accepting worker applications to an opening that is active.
+        pub fn accept_worker_applications(origin, worker_opening_id: WorkerOpeningId<T>)  {
+
+            // Ensure lead is set and is origin signer
+            Self::ensure_origin_is_set_lead(origin)?;
+
+            // Ensure opening exists in this working group
+            // NB: Even though call to hiring module will have implicit check for
+            // existence of opening as well, this check is to make sure that the opening is for
+            // this working group, not something else.
+            let (worker_opening, _opening) = Self::ensure_worker_opening_exists(&worker_opening_id)?;
+
+            // Attempt to begin accepting applications
+            // NB: Combined ensure check and mutation in hiring module
+
+            // mutation
+
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::begin_accepting_applications(worker_opening.opening_id)
+            )?;
+
+
+            // Trigger event
+            Self::deposit_event(RawEvent::AcceptedWorkerApplications(worker_opening_id));
+        }
+
+        /// Apply on a worker opening.
+        pub fn apply_on_worker_opening(
+            origin,
+            member_id: T::MemberId,
+            worker_opening_id: WorkerOpeningId<T>,
+            role_account: T::AccountId,
+            opt_role_stake_balance: Option<BalanceOf<T>>,
+            opt_application_stake_balance: Option<BalanceOf<T>>,
+            human_readable_text: Vec<u8>
+        ) {
+            // Ensure origin which will server as the source account for staked funds is signed
+            let source_account = ensure_signed(origin)?;
+
+            // In absence of a more general key delegation system which allows an account with some funds to
+            // grant another account permission to stake from its funds, the origin of this call must have the funds
+            // and cannot specify another arbitrary account as the source account.
+            // Ensure the source_account is either the controller or root account of member with given id
+            ensure!(
+                membership::members::Module::<T>::ensure_member_controller_account(&source_account, &member_id).is_ok() ||
+                membership::members::Module::<T>::ensure_member_root_account(&source_account, &member_id).is_ok(),
+                MSG_ORIGIN_IS_NEITHER_MEMBER_CONTROLLER_OR_ROOT
+            );
+
+            // Ensure worker opening exists
+            let (worker_opening, _opening) = Self::ensure_worker_opening_exists(&worker_opening_id)?;
+
+            // Ensure that there is sufficient balance to cover stake proposed
+            Self::ensure_can_make_stake_imbalance(
+                vec![&opt_role_stake_balance, &opt_application_stake_balance],
+                &source_account)
+                .map_err(|_err| MSG_INSUFFICIENT_BALANCE_TO_APPLY)?;
+
+            // Ensure application text is valid
+            Self::ensure_worker_application_text_is_valid(&human_readable_text)?;
+
+            // Ensure application can actually be added
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::ensure_can_add_application(worker_opening.opening_id, opt_role_stake_balance, opt_application_stake_balance)
+            )?;
+
+            // Ensure member does not have an active application to this opening
+            Self::ensure_member_has_no_active_application_on_opening(
+                worker_opening.worker_applications,
+                member_id
+            )?;
+
+            // mutation
+
+            // Make imbalances for staking
+            let opt_role_stake_imbalance = Self::make_stake_opt_imbalance(&opt_role_stake_balance, &source_account);
+            let opt_application_stake_imbalance = Self::make_stake_opt_imbalance(&opt_application_stake_balance, &source_account);
+
+            // Call hiring module to add application
+            let add_application_result = hiring::Module::<T>::add_application(
+                worker_opening.opening_id,
+                opt_role_stake_imbalance,
+                opt_application_stake_imbalance,
+                human_readable_text
+            );
+
+            // Has to hold
+            assert!(add_application_result.is_ok());
+
+            let application_id = add_application_result.unwrap().application_id_added;
+
+            // Get id of new worker application
+            let new_worker_application_id = NextWorkerApplicationId::<T, I>::get();
+
+            // Make worker application
+            let worker_application = WorkerApplication::new(&role_account, &worker_opening_id, &member_id, &application_id);
+
+            // Store application
+            WorkerApplicationById::<T, I>::insert(new_worker_application_id, worker_application);
+
+            // Update next worker application identifier value
+            NextWorkerApplicationId::<T, I>::mutate(|id| *id += <WorkerApplicationId<T> as One>::one());
+
+            // Add application to set of application in worker opening
+            WorkerOpeningById::<T, I>::mutate(worker_opening_id, |worker_opening| {
+                worker_opening.worker_applications.insert(new_worker_application_id);
+            });
+
+            // Trigger event
+            Self::deposit_event(RawEvent::AppliedOnWorkerOpening(worker_opening_id, new_worker_application_id));
+        }
+
+        pub fn withdraw_worker_application(
+            origin,
+            worker_application_id: WorkerApplicationId<T>
+        ) {
+            // Ensuring worker application actually exists
+            let (worker_application, _, worker_opening) = Self::ensure_worker_application_exists(&worker_application_id)?;
+
+            // Ensure that it is signed
+            let signer_account = ensure_signed(origin)?;
+
+            // Ensure that signer is applicant role account
+            ensure!(
+                signer_account == worker_application.role_account,
+                MSG_ORIGIN_IS_NOT_APPLICANT
+            );
+
+            // Attempt to deactivate application
+            // NB: Combined ensure check and mutation in hiring module
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::deactive_application(
+                    worker_application.application_id,
+                    worker_opening.policy_commitment.exit_worker_role_application_stake_unstaking_period,
+                    worker_opening.policy_commitment.exit_worker_role_stake_unstaking_period
+                )
+            )?;
+
+            // mutation
+
+            // Trigger event
+            Self::deposit_event(RawEvent::WorkerApplicationWithdrawn(worker_application_id));
+        }
+
+        pub fn terminate_worker_application(
+            origin,
+            worker_application_id: WorkerApplicationId<T>
+        ) {
+
+            // Ensure lead is set and is origin signer
+            Self::ensure_origin_is_set_lead(origin)?;
+
+            // Ensuring worker application actually exists
+            let (worker_application, _, worker_opening) = Self::ensure_worker_application_exists(&worker_application_id)?;
+
+            // Attempt to deactivate application
+            // NB: Combined ensure check and mutation in hiring module
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::deactive_application(
+                    worker_application.application_id,
+                    worker_opening.policy_commitment.terminate_worker_application_stake_unstaking_period,
+                    worker_opening.policy_commitment.terminate_worker_role_stake_unstaking_period
+                )
+            )?;
+
+            // mutation
+
+            // Trigger event
+            Self::deposit_event(RawEvent::WorkerApplicationTerminated(worker_application_id));
+        }
+
+        /// Begin reviewing, and therefore not accepting new applications.
+        pub fn begin_worker_applicant_review(origin, worker_opening_id: WorkerOpeningId<T>) {
+
+            // Ensure lead is set and is origin signer
+            Self::ensure_origin_is_set_lead(origin)?;
+
+            // Ensure opening exists
+            // NB: Even though call to hiring modul will have implicit check for
+            // existence of opening as well, this check is to make sure that the opening is for
+            // this working group, not something else.
+            let (worker_opening, _opening) = Self::ensure_worker_opening_exists(&worker_opening_id)?;
+
+            // Attempt to begin review of applications
+            // NB: Combined ensure check and mutation in hiring module
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::begin_review(worker_opening.opening_id)
+                )?;
+
+            // mutation
+
+            // Trigger event
+            Self::deposit_event(RawEvent::BeganWorkerApplicationReview(worker_opening_id));
+        }
+
+        /// Fill opening for worker
+        pub fn fill_worker_opening(
+            origin,
+            worker_opening_id: WorkerOpeningId<T>,
+            successful_worker_application_ids: WorkerApplicationIdSet<T>,
+            reward_policy: Option<RewardPolicy<minting::BalanceOf<T>, T::BlockNumber>>
+        ) {
+            // Ensure lead is set and is origin signer
+            Self::ensure_origin_is_set_lead(origin)?;
+
+            // Ensure worker opening exists
+            let (worker_opening, _) = Self::ensure_worker_opening_exists(&worker_opening_id)?;
+
+            // Make iterator over successful worker application
+            let successful_iter = successful_worker_application_ids
+                                    .iter()
+                                    // recover worker application from id
+                                    .map(|worker_application_id| { Self::ensure_worker_application_exists(worker_application_id)})
+                                    // remove Err cases, i.e. non-existing applications
+                                    .filter_map(|result| result.ok());
+
+            // Count number of successful workers provided
+            let num_provided_successful_worker_application_ids = successful_worker_application_ids.len();
+
+            // Ensure all worker applications exist
+            let number_of_successful_applications = successful_iter
+                                                    .clone()
+                                                    .count();
+
+            ensure!(
+                number_of_successful_applications == num_provided_successful_worker_application_ids,
+                MSG_SUCCESSFUL_WORKER_APPLICATION_DOES_NOT_EXIST
+            );
+
+            // Attempt to fill opening
+            let successful_application_ids = successful_iter
+                                            .clone()
+                                            .map(|(successful_worker_application, _, _)| successful_worker_application.application_id)
+                                            .collect::<BTreeSet<_>>();
+
+            // NB: Combined ensure check and mutation in hiring module
+            ensure_on_wrapped_error!(
+                hiring::Module::<T>::fill_opening(
+                    worker_opening.opening_id,
+                    successful_application_ids,
+                    worker_opening.policy_commitment.fill_opening_successful_applicant_application_stake_unstaking_period,
+                    worker_opening.policy_commitment.fill_opening_failed_applicant_application_stake_unstaking_period,
+                    worker_opening.policy_commitment.fill_opening_failed_applicant_role_stake_unstaking_period
+                )
+            )?;
+
+            let create_reward_settings = if let Some(policy) = reward_policy {
+                // A reward will need to be created so ensure our configured mint exists
+                let mint_id = Self::mint();
+
+                ensure!(<minting::Mints<T>>::exists(mint_id), MSG_FILL_WORKER_OPENING_MINT_DOES_NOT_EXIST);
+
+                // Make sure valid parameters are selected for next payment at block number
+                ensure!(policy.next_payment_at_block > <system::Module<T>>::block_number(), MSG_FILL_WORKER_OPENING_INVALID_NEXT_PAYMENT_BLOCK);
+
+                // The verified reward settings to use
+                Some((mint_id, policy))
+            } else {
+                None
+            };
+
+            // mutation
+
+            let mut worker_application_id_to_worker_id = BTreeMap::new();
+
+            successful_iter
+            .clone()
+            .for_each(|(successful_worker_application, id, _)| {
+                // Create a reward relationship
+                let reward_relationship = if let Some((mint_id, checked_policy)) = create_reward_settings.clone() {
+
+                    // Create a new recipient for the new relationship
+                    let recipient = <recurringrewards::Module<T>>::add_recipient();
+
+                    // member must exist, since it was checked that it can enter the role
+                    let member_profile = <membership::members::Module<T>>::member_profile(successful_worker_application.member_id).unwrap();
+
+                    // rewards are deposited in the member's root account
+                    let reward_destination_account = member_profile.root_account;
+
+                    // values have been checked so this should not fail!
+                    let relationship_id = <recurringrewards::Module<T>>::add_reward_relationship(
+                        mint_id,
+                        recipient,
+                        reward_destination_account,
+                        checked_policy.amount_per_payout,
+                        checked_policy.next_payment_at_block,
+                        checked_policy.payout_interval,
+                    ).expect("Failed to create reward relationship!");
+
+                    Some(relationship_id)
+                } else {
+                    None
+                };
+
+                // Get possible stake for role
+                let application = hiring::ApplicationById::<T>::get(successful_worker_application.application_id);
+
+                // Staking profile for worker
+                let stake_profile =
+                    if let Some(ref stake_id) = application.active_role_staking_id {
+                        Some(
+                            WorkerRoleStakeProfile::new(
+                                stake_id,
+                                &worker_opening.policy_commitment.terminate_worker_role_stake_unstaking_period,
+                                &worker_opening.policy_commitment.exit_worker_role_stake_unstaking_period
+                            )
+                        )
+                    } else {
+                        None
+                    };
+
+                // Get worker id
+                let new_worker_id = <NextWorkerId<T, I>>::get();
+
+                // Construct worker
+                let worker = Worker::new(
+                    &(successful_worker_application.role_account),
+                   &reward_relationship,
+                   &stake_profile,
+                   &WorkerRoleStage::Active,
+                );
+
+                // Store worker
+                <WorkerById<T, I>>::insert(new_worker_id, worker);
+
+                // Update next worker id
+                <NextWorkerId<T, I>>::mutate(|id| *id += <WorkerId<T> as One>::one());
+
+                worker_application_id_to_worker_id.insert(id, new_worker_id);
+            });
+
+            // Trigger event
+            Self::deposit_event(RawEvent::WorkerOpeningFilled(worker_opening_id, worker_application_id_to_worker_id));
+        }
     }
 }
 
@@ -109,3 +634,155 @@ where
         })
     }
 }
+
+impl<T: Trait<I>, I: Instance> Module<T, I> {
+    /// Checks that provided lead account id belongs to the current bureaucracy leader
+    pub fn ensure_is_lead_account(lead_account_id: T::AccountId) -> Result<(), &'static str> {
+        let lead = <CurrentLead<T, I>>::get();
+
+        if let Some(lead) = lead {
+            if lead.role_account_id != lead_account_id {
+                return Err(MSG_IS_NOT_LEAD_ACCOUNT);
+            }
+        } else {
+            return Err(MSG_CURRENT_LEAD_NOT_SET);
+        }
+
+        Ok(())
+    }
+
+    fn ensure_opening_human_readable_text_is_valid(text: &[u8]) -> dispatch::Result {
+        <OpeningHumanReadableText<I>>::get().ensure_valid(
+            text.len(),
+            MSG_OPENING_TEXT_TOO_SHORT,
+            MSG_OPENING_TEXT_TOO_LONG,
+        )
+    }
+
+    fn ensure_origin_is_set_lead(origin: T::Origin) -> Result<(), &'static str> {
+        // Ensure is signed
+        let signer = ensure_signed(origin)?;
+
+        Self::ensure_is_lead_account(signer)
+    }
+
+    fn ensure_worker_opening_exists(
+        worker_opening_id: &WorkerOpeningId<T>,
+    ) -> Result<WorkerOpeningInfo<T>, &'static str> {
+        ensure!(
+            WorkerOpeningById::<T, I>::exists(worker_opening_id),
+            MSG_WORKER_OPENING_DOES_NOT_EXIST
+        );
+
+        let worker_opening = WorkerOpeningById::<T, I>::get(worker_opening_id);
+
+        let opening = hiring::OpeningById::<T>::get(worker_opening.opening_id);
+
+        Ok((worker_opening, opening))
+    }
+
+    fn make_stake_opt_imbalance(
+        opt_balance: &Option<BalanceOf<T>>,
+        source_account: &T::AccountId,
+    ) -> Option<NegativeImbalance<T>> {
+        if let Some(balance) = opt_balance {
+            let withdraw_result = CurrencyOf::<T>::withdraw(
+                source_account,
+                *balance,
+                WithdrawReasons::all(),
+                ExistenceRequirement::AllowDeath,
+            );
+
+            assert!(withdraw_result.is_ok());
+
+            withdraw_result.ok()
+        } else {
+            None
+        }
+    }
+
+    fn ensure_member_has_no_active_application_on_opening(
+        worker_applications: WorkerApplicationIdSet<T>,
+        member_id: T::MemberId,
+    ) -> Result<(), &'static str> {
+        for worker_application_id in worker_applications {
+            let worker_application = WorkerApplicationById::<T, I>::get(worker_application_id);
+            // Look for application by the member for the opening
+            if worker_application.member_id != member_id {
+                continue;
+            }
+            // Get application details
+            let application = <hiring::ApplicationById<T>>::get(worker_application.application_id);
+            // Return error if application is in active stage
+            if application.stage == hiring::ApplicationStage::Active {
+                return Err(MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING);
+            }
+        }
+        // Member does not have any active applications to the opening
+        Ok(())
+    }
+
+    fn ensure_worker_application_text_is_valid(text: &[u8]) -> dispatch::Result {
+        <WorkerApplicationHumanReadableText<I>>::get().ensure_valid(
+            text.len(),
+            MSG_WORKER_APPLICATION_TEXT_TOO_SHORT,
+            MSG_WORKER_APPLICATION_TEXT_TOO_LONG,
+        )
+    }
+
+    // CRITICAL:
+    // https://github.com/Joystream/substrate-runtime-joystream/issues/92
+    // This assumes that ensure_can_withdraw can be don
+    // for a sum of balance that later will be actually withdrawn
+    // using individual terms in that sum.
+    // This needs to be fully checked across all possibly scenarios
+    // of actual balance, minimum balance limit, reservation, vesting and locking.
+    fn ensure_can_make_stake_imbalance(
+        opt_balances: Vec<&Option<BalanceOf<T>>>,
+        source_account: &T::AccountId,
+    ) -> Result<(), &'static str> {
+        let zero_balance = <BalanceOf<T> as Zero>::zero();
+
+        // Total amount to be staked
+        let total_amount = opt_balances.iter().fold(zero_balance, |sum, opt_balance| {
+            sum + if let Some(balance) = opt_balance {
+                *balance
+            } else {
+                zero_balance
+            }
+        });
+
+        if total_amount > zero_balance {
+            // Ensure that
+            if CurrencyOf::<T>::free_balance(source_account) < total_amount {
+                Err(MSG_INSUFFICIENT_BALANCE_TO_COVER_STAKE)
+            } else {
+                let new_balance = CurrencyOf::<T>::free_balance(source_account) - total_amount;
+
+                CurrencyOf::<T>::ensure_can_withdraw(
+                    source_account,
+                    total_amount,
+                    WithdrawReasons::all(),
+                    new_balance,
+                )
+            }
+        } else {
+            Ok(())
+        }
+    }
+
+    fn ensure_worker_application_exists(
+        worker_application_id: &WorkerApplicationId<T>,
+    ) -> Result<WorkerApplicationInfo<T>, &'static str> {
+        ensure!(
+            WorkerApplicationById::<T, I>::exists(worker_application_id),
+            MSG_WORKER_APPLICATION_DOES_NOT_EXIST
+        );
+
+        let worker_application = WorkerApplicationById::<T, I>::get(worker_application_id);
+
+        let worker_opening = WorkerOpeningById::<T, I>::get(worker_application.worker_opening_id);
+
+        Ok((worker_application, *worker_application_id, worker_opening))
+    }
+}

+ 140 - 0
runtime-modules/bureaucracy/src/tests/mock.rs

@@ -0,0 +1,140 @@
+use primitives::H256;
+use sr_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+
+use crate::{Instance1, Module, Trait};
+
+impl_outer_origin! {
+        pub enum Origin for Test {}
+}
+
+mod bureaucracy {
+    pub use crate::Event;
+    pub use crate::Instance1;
+}
+
+mod membership_mod {
+    pub use membership::members::Event;
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Test {
+        balances<T>,
+        bureaucracy Instance1 <T>,
+        membership_mod<T>,
+    }
+}
+
+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;
+    pub const InitialMembersBalance: u64 = 2000;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 - remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl hiring::Trait for Test {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
+}
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+impl membership::members::Trait for Test {
+    type Event = TestEvent;
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type InitialMembersBalance = InitialMembersBalance;
+}
+
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = Balances;
+}
+
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl balances::Trait for Test {
+    type Balance = u64;
+    type OnFreeBalanceZero = ();
+    type OnNewAccount = ();
+    type TransferPayment = ();
+    type DustRemoval = ();
+    type Event = TestEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl recurringrewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+pub type Balances = balances::Module<Test>;
+pub type System = system::Module<Test>;
+
+impl Trait<Instance1> for Test {
+    type Event = TestEvent;
+}
+
+pub type Membership = membership::members::Module<Test>;
+pub type Bureaucracy1 = Module<Test, Instance1>;
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}

+ 1361 - 0
runtime-modules/bureaucracy/src/tests/mod.rs

@@ -0,0 +1,1361 @@
+mod mock;
+
+use crate::tests::mock::Test;
+use crate::types::{
+    Lead, OpeningPolicyCommitment, RewardPolicy, Worker, WorkerApplication, WorkerOpening,
+    WorkerRoleStage, WorkerRoleStakeProfile,
+};
+use crate::{Instance1, RawEvent};
+use common::constraints::InputValidationLengthConstraint;
+use mock::{build_test_externalities, Balances, Bureaucracy1, Membership, System, TestEvent};
+use srml_support::StorageValue;
+use std::collections::{BTreeMap, BTreeSet};
+use system::{EventRecord, Phase, RawOrigin};
+
+fn set_mint_id(mint_id: u64) {
+    <crate::Mint<Test, crate::Instance1>>::put(mint_id);
+}
+
+fn create_mint() -> u64 {
+    <minting::Module<Test>>::add_mint(100, None).unwrap()
+}
+
+struct FillWorkerOpeningFixture {
+    origin: RawOrigin<u64>,
+    opening_id: u64,
+    successful_worker_application_ids: BTreeSet<u64>,
+    role_account: u64,
+    reward_policy: Option<RewardPolicy<u64, u64>>,
+}
+
+impl FillWorkerOpeningFixture {
+    pub fn default_for_ids(opening_id: u64, application_ids: Vec<u64>) -> Self {
+        let application_ids: BTreeSet<u64> = application_ids.iter().map(|x| *x).collect();
+
+        FillWorkerOpeningFixture {
+            origin: RawOrigin::Signed(1),
+            opening_id,
+            successful_worker_application_ids: application_ids,
+            role_account: 1,
+            reward_policy: None,
+        }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        FillWorkerOpeningFixture { origin, ..self }
+    }
+
+    fn with_reward_policy(self, reward_policy: RewardPolicy<u64, u64>) -> Self {
+        FillWorkerOpeningFixture {
+            reward_policy: Some(reward_policy),
+            ..self
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let saved_worker_next_id = Bureaucracy1::next_worker_id();
+        let actual_result = Bureaucracy1::fill_worker_opening(
+            self.origin.clone().into(),
+            self.opening_id,
+            self.successful_worker_application_ids.clone(),
+            self.reward_policy.clone(),
+        );
+        assert_eq!(actual_result.clone(), expected_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(Bureaucracy1::next_worker_id(), saved_worker_next_id + 1);
+            let worker_id = saved_worker_next_id;
+
+            let opening = Bureaucracy1::worker_opening_by_id(self.opening_id);
+
+            let role_stake_profile = if opening
+                .policy_commitment
+                .application_staking_policy
+                .is_some()
+                || opening.policy_commitment.role_staking_policy.is_some()
+            {
+                let stake_id = 0;
+                Some(WorkerRoleStakeProfile::new(
+                    &stake_id,
+                    &opening
+                        .policy_commitment
+                        .terminate_worker_role_stake_unstaking_period,
+                    &opening
+                        .policy_commitment
+                        .exit_worker_role_stake_unstaking_period,
+                ))
+            } else {
+                None
+            };
+            let reward_relationship = self.reward_policy.clone().map(|_| 0);
+
+            let expected_worker = Worker {
+                role_account: self.role_account,
+                reward_relationship,
+                role_stake_profile,
+                stage: WorkerRoleStage::Active,
+            };
+
+            let actual_worker = Bureaucracy1::worker_by_id(worker_id);
+
+            assert_eq!(actual_worker, expected_worker);
+        }
+    }
+}
+
+struct BeginReviewWorkerApplicationsFixture {
+    origin: RawOrigin<u64>,
+    opening_id: u64,
+}
+
+impl BeginReviewWorkerApplicationsFixture {
+    pub fn default_for_opening_id(opening_id: u64) -> Self {
+        BeginReviewWorkerApplicationsFixture {
+            origin: RawOrigin::Signed(1),
+            opening_id,
+        }
+    }
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        BeginReviewWorkerApplicationsFixture { origin, ..self }
+    }
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let actual_result = Bureaucracy1::begin_worker_applicant_review(
+            self.origin.clone().into(),
+            self.opening_id,
+        );
+        assert_eq!(actual_result, expected_result);
+    }
+}
+
+struct TerminateApplicationFixture {
+    origin: RawOrigin<u64>,
+    worker_application_id: u64,
+}
+
+impl TerminateApplicationFixture {
+    fn with_signer(self, account_id: u64) -> Self {
+        TerminateApplicationFixture {
+            origin: RawOrigin::Signed(account_id),
+            ..self
+        }
+    }
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        TerminateApplicationFixture { origin, ..self }
+    }
+    pub fn default_for_application_id(application_id: u64) -> Self {
+        TerminateApplicationFixture {
+            origin: RawOrigin::Signed(1),
+            worker_application_id: application_id,
+        }
+    }
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let actual_result = Bureaucracy1::terminate_worker_application(
+            self.origin.clone().into(),
+            self.worker_application_id,
+        );
+        assert_eq!(actual_result.clone(), expected_result);
+    }
+}
+struct WithdrawApplicationFixture {
+    origin: RawOrigin<u64>,
+    worker_application_id: u64,
+}
+
+impl WithdrawApplicationFixture {
+    fn with_signer(self, account_id: u64) -> Self {
+        WithdrawApplicationFixture {
+            origin: RawOrigin::Signed(account_id),
+            ..self
+        }
+    }
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        WithdrawApplicationFixture { origin, ..self }
+    }
+    pub fn default_for_application_id(application_id: u64) -> Self {
+        WithdrawApplicationFixture {
+            origin: RawOrigin::Signed(1),
+            worker_application_id: application_id,
+        }
+    }
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let actual_result = Bureaucracy1::withdraw_worker_application(
+            self.origin.clone().into(),
+            self.worker_application_id,
+        );
+        assert_eq!(actual_result.clone(), expected_result);
+    }
+}
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let _ =
+        <Balances as srml_support::traits::Currency<u64>>::deposit_creating(&account_id, balance);
+}
+
+fn setup_members(count: u8) {
+    let authority_account_id = 1;
+    Membership::set_screening_authority(RawOrigin::Root.into(), authority_account_id).unwrap();
+
+    for i in 0..count {
+        let account_id: u64 = i as u64;
+        let handle: [u8; 20] = [i; 20];
+        Membership::add_screened_member(
+            RawOrigin::Signed(authority_account_id).into(),
+            account_id,
+            membership::members::UserInfo {
+                handle: Some(handle.to_vec()),
+                avatar_uri: None,
+                about: None,
+            },
+        )
+        .unwrap();
+    }
+}
+
+struct ApplyOnWorkerOpeningFixture {
+    origin: RawOrigin<u64>,
+    member_id: u64,
+    worker_opening_id: u64,
+    role_account: u64,
+    opt_role_stake_balance: Option<u64>,
+    opt_application_stake_balance: Option<u64>,
+    human_readable_text: Vec<u8>,
+}
+
+impl ApplyOnWorkerOpeningFixture {
+    fn with_text(self, text: Vec<u8>) -> Self {
+        ApplyOnWorkerOpeningFixture {
+            human_readable_text: text,
+            ..self
+        }
+    }
+
+    fn with_role_stake(self, stake: u64) -> Self {
+        ApplyOnWorkerOpeningFixture {
+            opt_role_stake_balance: Some(stake),
+            ..self
+        }
+    }
+
+    fn with_application_stake(self, stake: u64) -> Self {
+        ApplyOnWorkerOpeningFixture {
+            opt_application_stake_balance: Some(stake),
+            ..self
+        }
+    }
+
+    pub fn default_for_opening_id(opening_id: u64) -> Self {
+        ApplyOnWorkerOpeningFixture {
+            origin: RawOrigin::Signed(1),
+            member_id: 1,
+            worker_opening_id: opening_id,
+            role_account: 1,
+            opt_role_stake_balance: None,
+            opt_application_stake_balance: None,
+            human_readable_text: Vec::new(),
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let saved_application_next_id = Bureaucracy1::next_worker_application_id();
+        let actual_result = Bureaucracy1::apply_on_worker_opening(
+            self.origin.clone().into(),
+            self.member_id,
+            self.worker_opening_id,
+            self.role_account,
+            self.opt_role_stake_balance,
+            self.opt_application_stake_balance,
+            self.human_readable_text.clone(),
+        );
+        assert_eq!(actual_result.clone(), expected_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(
+                Bureaucracy1::next_worker_application_id(),
+                saved_application_next_id + 1
+            );
+            let application_id = saved_application_next_id;
+
+            let actual_application = Bureaucracy1::worker_application_by_id(application_id);
+
+            let expected_application = WorkerApplication {
+                role_account: self.role_account,
+                worker_opening_id: self.worker_opening_id,
+                member_id: self.member_id,
+                application_id,
+            };
+
+            assert_eq!(actual_application, expected_application);
+
+            let current_opening = Bureaucracy1::worker_opening_by_id(self.worker_opening_id);
+            assert!(current_opening
+                .worker_applications
+                .contains(&application_id));
+        }
+    }
+}
+
+struct AcceptWorkerApplicationsFixture {
+    origin: RawOrigin<u64>,
+    opening_id: u64,
+}
+
+impl AcceptWorkerApplicationsFixture {
+    pub fn default_for_opening_id(opening_id: u64) -> Self {
+        AcceptWorkerApplicationsFixture {
+            origin: RawOrigin::Signed(1),
+            opening_id,
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let actual_result =
+            Bureaucracy1::accept_worker_applications(self.origin.clone().into(), self.opening_id);
+        assert_eq!(actual_result, expected_result);
+    }
+}
+
+struct SetLeadFixture;
+impl SetLeadFixture {
+    fn set_lead(lead_account_id: u64) {
+        assert_eq!(
+            Bureaucracy1::set_lead(RawOrigin::Root.into(), 1, lead_account_id),
+            Ok(())
+        );
+    }
+}
+
+struct AddWorkerOpeningFixture {
+    origin: RawOrigin<u64>,
+    activate_at: hiring::ActivateOpeningAt<u64>,
+    commitment: OpeningPolicyCommitment<u64, u64>,
+    human_readable_text: Vec<u8>,
+}
+
+impl Default for AddWorkerOpeningFixture {
+    fn default() -> Self {
+        AddWorkerOpeningFixture {
+            origin: RawOrigin::Signed(1),
+            activate_at: hiring::ActivateOpeningAt::CurrentBlock,
+            commitment: <OpeningPolicyCommitment<u64, u64>>::default(),
+            human_readable_text: Vec::new(),
+        }
+    }
+}
+
+impl AddWorkerOpeningFixture {
+    fn with_policy_commitment(self, policy_commitment: OpeningPolicyCommitment<u64, u64>) -> Self {
+        AddWorkerOpeningFixture {
+            commitment: policy_commitment,
+            ..self
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: Result<(), &str>) {
+        let saved_opening_next_id = Bureaucracy1::next_worker_opening_id();
+        let actual_result = Bureaucracy1::add_worker_opening(
+            self.origin.clone().into(),
+            self.activate_at.clone(),
+            self.commitment.clone(),
+            self.human_readable_text.clone(),
+        );
+        assert_eq!(actual_result.clone(), expected_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(
+                Bureaucracy1::next_worker_opening_id(),
+                saved_opening_next_id + 1
+            );
+            let opening_id = saved_opening_next_id;
+
+            let actual_opening = Bureaucracy1::worker_opening_by_id(opening_id);
+
+            let expected_opening = WorkerOpening::<u64, u64, u64, u64> {
+                opening_id,
+                worker_applications: BTreeSet::new(),
+                policy_commitment: self.commitment.clone(),
+            };
+
+            assert_eq!(actual_opening, expected_opening);
+        }
+    }
+
+    fn with_text(self, text: Vec<u8>) -> Self {
+        AddWorkerOpeningFixture {
+            human_readable_text: text,
+            ..self
+        }
+    }
+
+    fn with_activate_at(self, activate_at: hiring::ActivateOpeningAt<u64>) -> Self {
+        AddWorkerOpeningFixture {
+            activate_at,
+            ..self
+        }
+    }
+}
+
+struct EventFixture;
+impl EventFixture {
+    fn assert_crate_events(
+        expected_raw_events: Vec<
+            RawEvent<u64, u64, u64, u64, std::collections::BTreeMap<u64, u64>, crate::Instance1>,
+        >,
+    ) {
+        let converted_events = expected_raw_events
+            .iter()
+            .map(|ev| TestEvent::bureaucracy_Instance1(ev.clone()))
+            .collect::<Vec<TestEvent>>();
+
+        Self::assert_global_events(converted_events)
+    }
+    fn assert_global_events(expected_raw_events: Vec<TestEvent>) {
+        let expected_events = expected_raw_events
+            .iter()
+            .map(|ev| EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: ev.clone(),
+                topics: vec![],
+            })
+            .collect::<Vec<EventRecord<_, _>>>();
+
+        assert_eq!(System::events(), expected_events);
+    }
+
+    fn assert_last_crate_event(
+        expected_raw_event: RawEvent<
+            u64,
+            u64,
+            u64,
+            u64,
+            std::collections::BTreeMap<u64, u64>,
+            crate::Instance1,
+        >,
+    ) {
+        let converted_event = TestEvent::bureaucracy_Instance1(expected_raw_event);
+
+        Self::assert_last_global_event(converted_event)
+    }
+
+    fn assert_last_global_event(expected_event: TestEvent) {
+        let expected_event = EventRecord {
+            phase: Phase::ApplyExtrinsic(0),
+            event: expected_event,
+            topics: vec![],
+        };
+
+        assert_eq!(System::events().pop().unwrap(), expected_event);
+    }
+}
+
+#[test]
+fn set_forum_sudo_set() {
+    build_test_externalities().execute_with(|| {
+        // Ensure that lead is default
+        assert_eq!(Bureaucracy1::current_lead(), None);
+
+        let lead_account_id = 1;
+        let lead_member_id = 1;
+
+        // Set lead
+        assert_eq!(
+            Bureaucracy1::set_lead(RawOrigin::Root.into(), lead_member_id, lead_account_id),
+            Ok(())
+        );
+
+        let lead = Lead {
+            member_id: lead_member_id,
+            role_account_id: lead_account_id,
+        };
+        assert_eq!(Bureaucracy1::current_lead(), Some(lead));
+
+        EventFixture::assert_crate_events(vec![RawEvent::LeaderSet(
+            lead_member_id,
+            lead_account_id,
+        )]);
+    });
+}
+
+#[test]
+fn add_worker_opening_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_crate_events(vec![
+            RawEvent::LeaderSet(1, lead_account_id),
+            RawEvent::WorkerOpeningAdded(0),
+        ]);
+    });
+}
+
+#[test]
+fn add_worker_opening_fails_with_lead_is_not_set() {
+    build_test_externalities().execute_with(|| {
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+
+        add_worker_opening_fixture.call_and_assert(Err(crate::MSG_CURRENT_LEAD_NOT_SET));
+    });
+}
+
+#[test]
+fn add_worker_opening_fails_with_invalid_human_readable_text() {
+    build_test_externalities().execute_with(|| {
+        SetLeadFixture::set_lead(1);
+
+        <crate::OpeningHumanReadableText<Instance1>>::put(InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 5,
+        });
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default().with_text(Vec::new());
+
+        add_worker_opening_fixture.call_and_assert(Err(crate::MSG_OPENING_TEXT_TOO_SHORT));
+
+        let add_worker_opening_fixture =
+            AddWorkerOpeningFixture::default().with_text(b"Long text".to_vec());
+
+        add_worker_opening_fixture.call_and_assert(Err(crate::MSG_OPENING_TEXT_TOO_LONG));
+    });
+}
+
+#[test]
+fn add_worker_opening_fails_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        SetLeadFixture::set_lead(1);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+            .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(0));
+
+        add_worker_opening_fixture.call_and_assert(Err(
+            crate::errors::MSG_ADD_WORKER_OPENING_ACTIVATES_IN_THE_PAST,
+        ));
+    });
+}
+
+#[test]
+fn accept_worker_applications_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+            .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let accept_worker_applications_fixture =
+            AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        accept_worker_applications_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_crate_events(vec![
+            RawEvent::LeaderSet(1, lead_account_id),
+            RawEvent::WorkerOpeningAdded(opening_id),
+            RawEvent::AcceptedWorkerApplications(opening_id),
+        ]);
+    });
+}
+
+#[test]
+fn accept_worker_applications_fails_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        SetLeadFixture::set_lead(1);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let accept_worker_applications_fixture =
+            AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        accept_worker_applications_fixture.call_and_assert(Err(
+            crate::errors::MSG_ACCEPT_WORKER_APPLICATIONS_OPENING_IS_NOT_WAITING_TO_BEGIN,
+        ));
+    });
+}
+
+#[test]
+fn accept_worker_applications_fails_with_not_lead() {
+    build_test_externalities().execute_with(|| {
+        SetLeadFixture::set_lead(1);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        SetLeadFixture::set_lead(2);
+
+        let opening_id = 0; // newly created opening
+
+        let accept_worker_applications_fixture =
+            AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        accept_worker_applications_fixture.call_and_assert(Err(crate::MSG_IS_NOT_LEAD_ACCOUNT));
+    });
+}
+
+#[test]
+fn accept_worker_applications_fails_with_no_opening() {
+    build_test_externalities().execute_with(|| {
+        SetLeadFixture::set_lead(1);
+
+        let opening_id = 0; // newly created opening
+
+        let accept_worker_applications_fixture =
+            AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        accept_worker_applications_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_OPENING_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_global_events(vec![
+            TestEvent::bureaucracy_Instance1(RawEvent::LeaderSet(1, lead_account_id)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(0, 0)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(1, 1)),
+            TestEvent::bureaucracy_Instance1(RawEvent::WorkerOpeningAdded(opening_id)),
+            TestEvent::bureaucracy_Instance1(RawEvent::AppliedOnWorkerOpening(opening_id, 0)),
+        ]);
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_no_opening() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_OPENING_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_not_set_members() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_ORIGIN_IS_NEITHER_MEMBER_CONTROLLER_OR_ROOT));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
+                .with_application_stake(100);
+        appy_on_worker_opening_fixture.call_and_assert(Err(
+            crate::errors::MSG_ADD_WORKER_OPENING_STAKE_PROVIDED_WHEN_REDUNDANT,
+        ));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_invalid_application_stake() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
+                .with_application_stake(100);
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_INSUFFICIENT_BALANCE_TO_APPLY));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_invalid_role_stake() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_role_stake(100);
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_INSUFFICIENT_BALANCE_TO_APPLY));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_invalid_text() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        <crate::WorkerApplicationHumanReadableText<Instance1>>::put(
+            InputValidationLengthConstraint {
+                min: 1,
+                max_min_diff: 5,
+            },
+        );
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_text(Vec::new());
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_APPLICATION_TEXT_TOO_SHORT));
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
+                .with_text(b"Long text".to_vec());
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_APPLICATION_TEXT_TOO_LONG));
+    });
+}
+
+#[test]
+fn apply_on_worker_opening_fails_with_already_active_application() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        appy_on_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING));
+    });
+}
+
+#[test]
+fn withdraw_worker_application_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let withdraw_application_fixture =
+            WithdrawApplicationFixture::default_for_application_id(application_id);
+        withdraw_application_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_global_events(vec![
+            TestEvent::bureaucracy_Instance1(RawEvent::LeaderSet(1, lead_account_id)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(0, 0)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(1, 1)),
+            TestEvent::bureaucracy_Instance1(RawEvent::WorkerOpeningAdded(opening_id)),
+            TestEvent::bureaucracy_Instance1(RawEvent::AppliedOnWorkerOpening(
+                opening_id,
+                application_id,
+            )),
+            TestEvent::bureaucracy_Instance1(RawEvent::WorkerApplicationWithdrawn(application_id)),
+        ]);
+    });
+}
+
+#[test]
+fn withdraw_worker_application_fails_invalid_application_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_application_id = 6;
+
+        let withdraw_application_fixture =
+            WithdrawApplicationFixture::default_for_application_id(invalid_application_id);
+        withdraw_application_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_APPLICATION_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn withdraw_worker_application_fails_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let withdraw_application_fixture =
+            WithdrawApplicationFixture::default_for_application_id(application_id)
+                .with_origin(RawOrigin::None);
+        withdraw_application_fixture.call_and_assert(Err("RequireSignedOrigin"));
+    });
+}
+
+#[test]
+fn withdraw_worker_application_fails_with_invalid_application_author() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+        let invalid_author_account_id = 55;
+        let withdraw_application_fixture =
+            WithdrawApplicationFixture::default_for_application_id(application_id)
+                .with_signer(invalid_author_account_id);
+        withdraw_application_fixture.call_and_assert(Err(crate::MSG_ORIGIN_IS_NOT_APPLICANT));
+    });
+}
+
+#[test]
+fn withdraw_worker_application_fails_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let withdraw_application_fixture =
+            WithdrawApplicationFixture::default_for_application_id(application_id);
+        withdraw_application_fixture.call_and_assert(Ok(()));
+        withdraw_application_fixture.call_and_assert(Err(
+            crate::errors::MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_NOT_ACTIVE,
+        ));
+    });
+}
+
+#[test]
+fn terminate_worker_application_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let terminate_application_fixture =
+            TerminateApplicationFixture::default_for_application_id(application_id);
+        terminate_application_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_global_events(vec![
+            TestEvent::bureaucracy_Instance1(RawEvent::LeaderSet(1, lead_account_id)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(0, 0)),
+            TestEvent::membership_mod(membership::members::RawEvent::MemberRegistered(1, 1)),
+            TestEvent::bureaucracy_Instance1(RawEvent::WorkerOpeningAdded(opening_id)),
+            TestEvent::bureaucracy_Instance1(RawEvent::AppliedOnWorkerOpening(
+                opening_id,
+                application_id,
+            )),
+            TestEvent::bureaucracy_Instance1(RawEvent::WorkerApplicationTerminated(application_id)),
+        ]);
+    });
+}
+
+#[test]
+fn terminate_worker_application_fails_with_invalid_application_author() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+        let invalid_author_account_id = 55;
+        let terminate_application_fixture =
+            TerminateApplicationFixture::default_for_application_id(application_id)
+                .with_signer(invalid_author_account_id);
+        terminate_application_fixture.call_and_assert(Err(crate::MSG_IS_NOT_LEAD_ACCOUNT));
+    });
+}
+
+#[test]
+fn terminate_worker_application_fails_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let terminate_application_fixture =
+            TerminateApplicationFixture::default_for_application_id(application_id)
+                .with_origin(RawOrigin::None);
+        terminate_application_fixture.call_and_assert(Err("RequireSignedOrigin"));
+    });
+}
+
+#[test]
+fn terminate_worker_application_fails_invalid_application_id() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let invalid_application_id = 6;
+
+        let terminate_application_fixture =
+            TerminateApplicationFixture::default_for_application_id(invalid_application_id);
+        terminate_application_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_APPLICATION_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn terminate_worker_application_fails_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let terminate_application_fixture =
+            TerminateApplicationFixture::default_for_application_id(application_id);
+        terminate_application_fixture.call_and_assert(Ok(()));
+        terminate_application_fixture.call_and_assert(Err(
+            crate::errors::MSG_WITHDRAW_WORKER_APPLICATION_APPLICATION_NOT_ACTIVE,
+        ));
+    });
+}
+
+#[test]
+fn begin_review_worker_applications_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture.call_and_assert(Ok(()));
+
+        EventFixture::assert_crate_events(vec![
+            RawEvent::LeaderSet(1, lead_account_id),
+            RawEvent::WorkerOpeningAdded(opening_id),
+            RawEvent::BeganWorkerApplicationReview(opening_id),
+        ]);
+    });
+}
+
+#[test]
+fn begin_review_worker_applications_fails_with_not_a_lead() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let new_lead_account_id = 33;
+        SetLeadFixture::set_lead(new_lead_account_id);
+
+        let opening_id = 0; // newly created opening
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture
+            .call_and_assert(Err(crate::MSG_IS_NOT_LEAD_ACCOUNT));
+    });
+}
+
+#[test]
+fn begin_review_worker_applications_fails_with_invalid_opening() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let invalid_opening_id = 6; // newly created opening
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(invalid_opening_id);
+        begin_review_worker_applications_fixture
+            .call_and_assert(Err(crate::MSG_WORKER_OPENING_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn begin_review_worker_applications_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture.call_and_assert(Ok(()));
+        begin_review_worker_applications_fixture.call_and_assert(Err(
+            crate::errors::MSG_BEGIN_WORKER_APPLICANT_REVIEW_OPENING_OPENING_IS_NOT_WAITING_TO_BEGIN,
+        ));
+    });
+}
+
+#[test]
+fn begin_review_worker_applications_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
+                .with_origin(RawOrigin::None);
+        begin_review_worker_applications_fixture.call_and_assert(Err("RequireSignedOrigin"));
+    });
+}
+
+#[test]
+fn fill_worker_opening_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+        increase_total_balance_issuance_using_account_id(1, 10000);
+        setup_members(2);
+
+        let add_worker_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                role_staking_policy: Some(hiring::StakingPolicy {
+                    amount: 10,
+                    amount_mode: hiring::StakingAmountLimitMode::AtLeast,
+                    crowded_out_unstaking_period_length: None,
+                    review_period_expired_unstaking_period_length: None,
+                }),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_role_stake(10);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture.call_and_assert(Ok(()));
+
+        let mint_id = create_mint();
+        set_mint_id(mint_id);
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
+                .with_reward_policy(RewardPolicy {
+                    amount_per_payout: 1000,
+                    next_payment_at_block: 20,
+                    payout_interval: None,
+                });
+        fill_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let worker_id = 0; // newly created worker
+        let mut worker_application_dictionary = BTreeMap::new();
+        worker_application_dictionary.insert(application_id, worker_id);
+
+        EventFixture::assert_last_crate_event(RawEvent::WorkerOpeningFilled(
+            opening_id,
+            worker_application_dictionary,
+        ));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new())
+                .with_origin(RawOrigin::None);
+        fill_worker_opening_fixture.call_and_assert(Err("RequireSignedOrigin"));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_not_a_lead() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let new_lead_account_id = 33;
+        SetLeadFixture::set_lead(new_lead_account_id);
+
+        let opening_id = 0; // newly created opening
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
+        fill_worker_opening_fixture.call_and_assert(Err(crate::MSG_IS_NOT_LEAD_ACCOUNT));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_invalid_opening() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        let invalid_opening_id = 6; // newly created opening
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(invalid_opening_id, Vec::new());
+        fill_worker_opening_fixture.call_and_assert(Err(crate::MSG_WORKER_OPENING_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_invalid_application_list() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture.call_and_assert(Ok(()));
+
+        let invalid_application_id = 66;
+        let fill_worker_opening_fixture = FillWorkerOpeningFixture::default_for_ids(
+            opening_id,
+            vec![application_id, invalid_application_id],
+        );
+        fill_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_SUCCESSFUL_WORKER_APPLICATION_DOES_NOT_EXIST));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_invalid_application_with_hiring_error() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
+        fill_worker_opening_fixture.call_and_assert(Err(
+            crate::errors::MSG_FULL_WORKER_OPENING_OPENING_NOT_IN_REVIEW_PERIOD_STAGE,
+        ));
+    });
+}
+
+#[test]
+fn fill_worker_opening_fails_with_invalid_reward_policy() {
+    build_test_externalities().execute_with(|| {
+        let lead_account_id = 1;
+        SetLeadFixture::set_lead(lead_account_id);
+
+        setup_members(2);
+
+        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        add_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let opening_id = 0; // newly created opening
+
+        let appy_on_worker_opening_fixture =
+            ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
+        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+
+        let application_id = 0; // newly created application
+
+        let begin_review_worker_applications_fixture =
+            BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
+        begin_review_worker_applications_fixture.call_and_assert(Ok(()));
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
+                .with_reward_policy(RewardPolicy {
+                    amount_per_payout: 10000,
+                    next_payment_at_block: 100,
+                    payout_interval: None,
+                });
+
+        fill_worker_opening_fixture
+            .call_and_assert(Err(crate::MSG_FILL_WORKER_OPENING_MINT_DOES_NOT_EXIST));
+
+        set_mint_id(22);
+
+        let fill_worker_opening_fixture =
+            FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
+                .with_reward_policy(RewardPolicy {
+                    amount_per_payout: 10000,
+                    next_payment_at_block: 0,
+                    payout_interval: None,
+                });
+        fill_worker_opening_fixture.call_and_assert(Err(
+            crate::errors::MSG_FULL_WORKER_OPENING_OPENING_NOT_IN_REVIEW_PERIOD_STAGE,
+        ));
+    });
+}

+ 257 - 1
runtime-modules/bureaucracy/src/types.rs

@@ -1,8 +1,99 @@
 use codec::{Decode, Encode};
+use rstd::borrow::ToOwned;
+use rstd::collections::btree_set::BTreeSet;
+use rstd::vec::Vec;
+
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
 
-/// Working group lead: curator lead
+/// Terms for slashings applied to a given role
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq)]
+pub struct SlashableTerms {
+    /// Maximum number of slashes.
+    pub max_count: u16,
+
+    /// Maximum percentage points of remaining stake which may be slashed in a single slash.
+    pub max_percent_pts_per_time: u16,
+}
+
+/// Terms for what slashing can be applied in some context
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq)]
+pub enum SlashingTerms {
+    Unslashable,
+    Slashable(SlashableTerms),
+}
+
+/// Must be default constructible because it indirectly is a value in a storage map.
+/// ***SHOULD NEVER ACTUALLY GET CALLED, IS REQUIRED TO DUE BAD STORAGE MODEL IN SUBSTRATE***
+impl Default for SlashingTerms {
+    fn default() -> Self {
+        Self::Unslashable
+    }
+}
+
+/// A commitment to the set of policy variables relevant to an opening.
+/// An applicant can observe this commitment and be secure that the terms
+/// of the application process cannot be changed ex-post.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, Default, PartialEq, Eq)]
+pub struct OpeningPolicyCommitment<BlockNumber, Balance> {
+    /// Rationing to be used
+    pub application_rationing_policy: Option<hiring::ApplicationRationingPolicy>,
+
+    /// Maximum length of review period of applications
+    pub max_review_period_length: BlockNumber,
+
+    /// Staking policy for application
+    pub application_staking_policy: Option<hiring::StakingPolicy<Balance, BlockNumber>>,
+
+    /// Staking policy for role itself
+    pub role_staking_policy: Option<hiring::StakingPolicy<Balance, BlockNumber>>,
+
+    // Slashing terms during application
+    // pub application_slashing_terms: SlashingTerms,
+
+    // Slashing terms during role, NOT application itself!
+    pub role_slashing_terms: SlashingTerms,
+
+    /// When filling an opening: Unstaking period for application stake of successful applicants
+    pub fill_opening_successful_applicant_application_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When filling an opening:
+    pub fill_opening_failed_applicant_application_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When filling an opening:
+    pub fill_opening_failed_applicant_role_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When terminating a worker:
+    pub terminate_worker_application_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When terminating a worker:
+    pub terminate_worker_role_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When a worker exists: ..
+    pub exit_worker_role_application_stake_unstaking_period: Option<BlockNumber>,
+
+    /// When a worker exists: ..
+    pub exit_worker_role_stake_unstaking_period: Option<BlockNumber>,
+}
+
+/// An opening for a worker role.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
+pub struct WorkerOpening<OpeningId, BlockNumber, Balance, WorkerApplicationId: core::cmp::Ord> {
+    /// Identifer for underlying opening in the hiring module.
+    pub opening_id: OpeningId,
+
+    /// Set of identifiers for all worker applications ever added
+    pub worker_applications: BTreeSet<WorkerApplicationId>,
+
+    /// Commitment to policies in opening.
+    pub policy_commitment: OpeningPolicyCommitment<BlockNumber, Balance>,
+}
+
+/// Working group lead: worker lead
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
 pub struct Lead<MemberId, AccountId> {
@@ -12,3 +103,168 @@ pub struct Lead<MemberId, AccountId> {
     /// Account used to authenticate in this role,
     pub role_account_id: AccountId,
 }
+
+/// An application for the worker role.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
+pub struct WorkerApplication<AccountId, WorkerOpeningId, MemberId, ApplicationId> {
+    /// Account used to authenticate in this role,
+    pub role_account: AccountId,
+
+    /// Opening on which this application applies
+    pub worker_opening_id: WorkerOpeningId,
+
+    /// Member applying
+    pub member_id: MemberId,
+
+    /// Underlying application in hiring module
+    pub application_id: ApplicationId,
+}
+
+impl<AccountId: Clone, WorkerOpeningId: Clone, MemberId: Clone, ApplicationId: Clone>
+    WorkerApplication<AccountId, WorkerOpeningId, MemberId, ApplicationId>
+{
+    pub fn new(
+        role_account: &AccountId,
+        worker_opening_id: &WorkerOpeningId,
+        member_id: &MemberId,
+        application_id: &ApplicationId,
+    ) -> Self {
+        WorkerApplication {
+            role_account: (*role_account).clone(),
+            worker_opening_id: (*worker_opening_id).clone(),
+            member_id: (*member_id).clone(),
+            application_id: (*application_id).clone(),
+        }
+    }
+}
+
+/// Role stake information for a worker.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
+pub struct WorkerRoleStakeProfile<StakeId, BlockNumber> {
+    /// Whether participant is staked, and if so, the identifier for this staking in the staking module.
+    pub stake_id: StakeId,
+
+    /// Unstaking period when terminated.
+    pub termination_unstaking_period: Option<BlockNumber>,
+
+    /// Unstaking period when exiting.
+    pub exit_unstaking_period: Option<BlockNumber>,
+}
+
+impl<StakeId: Clone, BlockNumber: Clone> WorkerRoleStakeProfile<StakeId, BlockNumber> {
+    pub fn new(
+        stake_id: &StakeId,
+        termination_unstaking_period: &Option<BlockNumber>,
+        exit_unstaking_period: &Option<BlockNumber>,
+    ) -> Self {
+        Self {
+            stake_id: (*stake_id).clone(),
+            termination_unstaking_period: (*termination_unstaking_period).clone(),
+            exit_unstaking_period: (*exit_unstaking_period).clone(),
+        }
+    }
+}
+
+/// Working group participant: worker
+/// This role can be staked, have reward and be inducted through the hiring module.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
+pub struct Worker<AccountId, RewardRelationshipId, StakeId, BlockNumber> {
+    /// Account used to authenticate in this role,
+    pub role_account: AccountId,
+    /// Whether the role has recurring reward, and if so an identifier for this.
+    pub reward_relationship: Option<RewardRelationshipId>,
+    /// When set, describes role stake of worker.
+    pub role_stake_profile: Option<WorkerRoleStakeProfile<StakeId, BlockNumber>>,
+    /// The stage of this worker in the working group.
+    pub stage: WorkerRoleStage<BlockNumber>,
+}
+
+impl<AccountId: Clone, RewardRelationshipId: Clone, StakeId: Clone, BlockNumber: Clone>
+    Worker<AccountId, RewardRelationshipId, StakeId, BlockNumber>
+{
+    pub fn new(
+        role_account: &AccountId,
+        reward_relationship: &Option<RewardRelationshipId>,
+        role_stake_profile: &Option<WorkerRoleStakeProfile<StakeId, BlockNumber>>,
+        stage: &WorkerRoleStage<BlockNumber>,
+    ) -> Self {
+        Worker {
+            role_account: (*role_account).clone(),
+            reward_relationship: (*reward_relationship).clone(),
+            role_stake_profile: (*role_stake_profile).clone(),
+            stage: (*stage).clone(),
+        }
+    }
+}
+
+// The stage of the involvement of a curator in the working group.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, PartialEq)]
+pub enum WorkerRoleStage<BlockNumber> {
+    /// Currently active.
+    Active,
+
+    /// Currently unstaking
+    Unstaking(CuratorExitSummary<BlockNumber>),
+
+    /// No longer active and unstaked
+    Exited(CuratorExitSummary<BlockNumber>),
+}
+
+/// Must be default constructible because it indirectly is a value in a storage map.
+/// ***SHOULD NEVER ACTUALLY GET CALLED, IS REQUIRED TO DUE BAD STORAGE MODEL IN SUBSTRATE***
+impl<BlockNumber> Default for WorkerRoleStage<BlockNumber> {
+    fn default() -> Self {
+        WorkerRoleStage::Active
+    }
+}
+
+/// The exit stage of a curators involvement in the working group.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, PartialEq)]
+pub struct CuratorExitSummary<BlockNumber> {
+    /// Origin for exit.
+    pub origin: CuratorExitInitiationOrigin,
+
+    /// When exit was initiated.
+    pub initiated_at_block_number: BlockNumber,
+
+    /// Explainer for why exit was initited.
+    pub rationale_text: Vec<u8>,
+}
+
+impl<BlockNumber: Clone> CuratorExitSummary<BlockNumber> {
+    pub fn new(
+        origin: &CuratorExitInitiationOrigin,
+        initiated_at_block_number: &BlockNumber,
+        rationale_text: &[u8],
+    ) -> Self {
+        CuratorExitSummary {
+            origin: (*origin).clone(),
+            initiated_at_block_number: (*initiated_at_block_number).clone(),
+            rationale_text: rationale_text.to_owned(),
+        }
+    }
+}
+
+/// Origin of exit initiation on behalf of a curator.'
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Clone, PartialEq)]
+pub enum CuratorExitInitiationOrigin {
+    /// Lead is origin.
+    Lead,
+
+    /// The curator exiting is the origin.
+    Curator,
+}
+
+/// The recurring reward if any to be assigned to an actor when filling in the position.
+#[derive(Encode, Decode, Clone, Eq, PartialEq, Debug)]
+pub struct RewardPolicy<Balance, BlockNumber> {
+    pub amount_per_payout: Balance,
+    pub next_payment_at_block: BlockNumber,
+    pub payout_interval: Option<BlockNumber>,
+}

+ 13 - 0
runtime-modules/common/Cargo.toml

@@ -10,6 +10,8 @@ std = [
 	'sr-primitives/std',
 	'srml-support/std',
 	'system/std',
+	'codec/std',
+	'serde'
 ]
 
 
@@ -30,3 +32,14 @@ default_features = false
 git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-system'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'

+ 40 - 0
runtime-modules/common/src/constraints.rs

@@ -0,0 +1,40 @@
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+/// 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(())
+        }
+    }
+}

+ 1 - 0
runtime-modules/common/src/lib.rs

@@ -1,5 +1,6 @@
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
+pub mod constraints;
 pub mod currency;
 pub mod origin_validator;

+ 6 - 5
runtime-modules/content-working-group/Cargo.toml

@@ -22,6 +22,7 @@ std = [
     'versioned_store/std',
     'versioned_store_permissions/std',
     'recurringrewards/std',
+    'common/std',
 ]
 
 
@@ -106,6 +107,11 @@ default_features = false
 package = 'substrate-membership-module'
 path = '../membership'
 
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'
+
 [dev-dependencies.runtime-io]
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'
@@ -123,8 +129,3 @@ default_features = false
 git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-timestamp'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dev-dependencies.common]
-default_features = false
-package = 'substrate-common-module'
-path = '../common'

+ 1 - 3
runtime-modules/content-working-group/src/lib.rs

@@ -38,9 +38,7 @@ use srml_support::{
 };
 use system::{self, ensure_root, ensure_signed};
 
-/// DIRTY IMPORT BECAUSE
-/// InputValidationLengthConstraint has not been factored out yet!!!
-use forum::InputValidationLengthConstraint;
+use common::constraints::InputValidationLengthConstraint;
 
 /// Module configuration trait for this Substrate module.
 pub trait Trait:

+ 12 - 1
runtime-modules/forum/Cargo.toml

@@ -61,11 +61,21 @@ git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-balances'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 
-[dev-dependencies.common]
+[dependencies.common]
 default_features = false
 package = 'substrate-common-module'
 path = '../common'
 
+[dev-dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../hiring'
+
+[dev-dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../stake'
+
 [features]
 default = ['std']
 std = [
@@ -80,4 +90,5 @@ std = [
   	'balances/std',
 	'timestamp/std',
 	'bureaucracy/std',
+	'common/std',
 ]

+ 2 - 41
runtime-modules/forum/src/lib.rs

@@ -16,50 +16,11 @@ use runtime_primitives::traits::EnsureOrigin;
 use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
 use system::{ensure_signed, RawOrigin};
 
+pub use common::constraints::InputValidationLengthConstraint;
+
 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
 /////////////////////////////////////////////////////////////////
 

+ 41 - 7
runtime-modules/forum/src/mock.rs

@@ -74,6 +74,7 @@ parameter_types! {
     pub const ExistentialDeposit: u32 = 0;
     pub const TransferFee: u32 = 0;
     pub const CreationFee: u32 = 0;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
 }
 
 impl system::Trait for Runtime {
@@ -101,7 +102,9 @@ impl timestamp::Trait for Runtime {
     type MinimumPeriod = MinimumPeriod;
 }
 
-impl bureaucracy::Trait<bureaucracy::Instance1> for Runtime {}
+impl bureaucracy::Trait<bureaucracy::Instance1> for Runtime {
+    type Event = ();
+}
 
 impl recurringrewards::Trait for Runtime {
     type PayoutStatusHandler = ();
@@ -109,6 +112,21 @@ impl recurringrewards::Trait for Runtime {
     type RewardRelationshipId = u64;
 }
 
+impl stake::Trait for Runtime {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+impl hiring::Trait for Runtime {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
+}
+
 impl membership::members::Trait for Runtime {
     type Event = ();
     type MemberId = u64;
@@ -143,23 +161,26 @@ impl minting::Trait for Runtime {
 impl Trait for Runtime {
     type Event = ();
     type MembershipRegistry = registry::TestMembershipRegistryModule;
+    type EnsureForumLeader = bureaucracy::Module<Runtime, bureaucracy::Instance1>;
 }
 
 #[derive(Clone)]
 pub enum OriginType {
     Signed(<Runtime as system::Trait>::AccountId),
     //Inherent, <== did not find how to make such an origin yet
-    Root,
+    //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
+        //OriginType::Root => system::RawOrigin::Root.into(), //Origin::root
     }
 }
 
+pub static ERROR_ORIGIN_NOT_FORUM_SUDO: &str = "Invalid origin";
+
 pub const NOT_FORUM_SUDO_ORIGIN: OriginType = OriginType::Signed(111);
 
 pub const NOT_MEMBER_ORIGIN: OriginType = OriginType::Signed(222);
@@ -170,6 +191,10 @@ pub const INVLAID_THREAD_ID: ThreadId = 444;
 
 pub const INVLAID_POST_ID: ThreadId = 555;
 
+pub(crate) const FORUM_SUDO_ID: u64 = 33;
+
+pub(crate) const FORUM_SUDO_MEMBER_ID: u64 = 1;
+
 pub fn generate_text(len: usize) -> Vec<u8> {
     vec![b'x'; len]
 }
@@ -213,6 +238,7 @@ pub struct CreateCategoryFixture {
 
 impl CreateCategoryFixture {
     pub fn call_and_assert(&self) {
+        set_bureaucracy_forum_lead();
         assert_eq!(
             TestForumModule::create_category(
                 mock_origin(self.origin.clone()),
@@ -235,6 +261,7 @@ pub struct UpdateCategoryFixture {
 
 impl UpdateCategoryFixture {
     pub fn call_and_assert(&self) {
+        set_bureaucracy_forum_lead();
         assert_eq!(
             TestForumModule::update_category(
                 mock_origin(self.origin.clone()),
@@ -255,6 +282,15 @@ pub struct CreateThreadFixture {
     pub result: dispatch::Result,
 }
 
+type Bureaucracy1 = bureaucracy::Module<Runtime, bureaucracy::Instance1>;
+
+pub(crate) fn set_bureaucracy_forum_lead() {
+    assert_eq!(
+        Bureaucracy1::set_lead(RawOrigin::Root.into(), FORUM_SUDO_MEMBER_ID, FORUM_SUDO_ID),
+        Ok(())
+    );
+}
+
 impl CreateThreadFixture {
     pub fn call_and_assert(&self) {
         assert_eq!(
@@ -425,14 +461,13 @@ pub fn assert_not_forum_sudo_cannot_update_category(
     update_operation: fn(OriginType, CategoryId) -> dispatch::Result,
 ) {
     let config = default_genesis_config();
-    let forum_sudo = 33;
-    let origin = OriginType::Signed(forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     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(bureaucracy::MSG_ORIGIN_IS_NOT_LEAD)
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
         );
     });
 }
@@ -516,7 +551,6 @@ pub fn genesis_config(
     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,

+ 46 - 103
runtime-modules/forum/src/tests.rs

@@ -9,64 +9,6 @@ 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
  * ==============================================================================
@@ -80,7 +22,7 @@ fn set_forum_sudo_update() {
 #[test]
 fn create_root_category_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         assert_create_category(origin, None, Ok(()));
@@ -90,7 +32,7 @@ fn create_root_category_successfully() {
 #[test]
 fn create_subcategory_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let root_category_id = create_root_category(origin.clone());
@@ -101,7 +43,7 @@ fn create_subcategory_successfully() {
 #[test]
 fn create_category_title_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.category_title_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -119,7 +61,7 @@ fn create_category_title_too_short() {
 #[test]
 fn create_category_title_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.category_title_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -137,7 +79,7 @@ fn create_category_title_too_long() {
 #[test]
 fn create_category_description_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.category_description_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -155,7 +97,7 @@ fn create_category_description_too_short() {
 #[test]
 fn create_category_description_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.category_description_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -189,7 +131,7 @@ fn update_category_undelete_and_unarchive() {
      * leaf category is deleted, and then try to undelete.
      */
 
-    let forum_sudo = 32;
+    let forum_sudo = FORUM_SUDO_ID;
 
     let created_at = RuntimeBlockchainTimestamp { block: 0, time: 0 };
 
@@ -248,7 +190,6 @@ fn update_category_undelete_and_unarchive() {
         1,                           // next_thread_id
         &vec![],                     // post_by_id
         1,                           // next_post_id
-        forum_sudo,
         &sloppy_constraint,
         &sloppy_constraint,
         &sloppy_constraint,
@@ -283,7 +224,7 @@ fn update_category_undelete_and_unarchive() {
 #[test]
 fn create_thread_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(origin);
@@ -303,7 +244,7 @@ fn create_thread_successfully() {
 #[test]
 fn create_thread_title_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.thread_title_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -324,7 +265,7 @@ fn create_thread_title_too_short() {
 #[test]
 fn create_thread_title_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.thread_title_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -345,7 +286,7 @@ fn create_thread_title_too_long() {
 #[test]
 fn create_thread_text_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.post_text_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -366,7 +307,7 @@ fn create_thread_text_too_short() {
 #[test]
 fn create_thread_text_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.post_text_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -387,7 +328,7 @@ fn create_thread_text_too_long() {
 #[test]
 fn create_post_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, _, _) = create_root_category_and_thread_and_post(origin);
@@ -397,7 +338,7 @@ fn create_post_successfully() {
 #[test]
 fn create_post_text_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.post_text_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -416,7 +357,7 @@ fn create_post_text_too_short() {
 #[test]
 fn create_post_text_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.post_text_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -438,7 +379,7 @@ fn create_post_text_too_long() {
 #[test]
 fn moderate_thread_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
@@ -449,7 +390,7 @@ fn moderate_thread_successfully() {
 #[test]
 fn cannot_moderate_already_moderated_thread() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
@@ -467,7 +408,7 @@ fn cannot_moderate_already_moderated_thread() {
 #[test]
 fn moderate_thread_rationale_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.thread_moderation_rationale_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -483,7 +424,7 @@ fn moderate_thread_rationale_too_short() {
 #[test]
 fn moderate_thread_rationale_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.thread_moderation_rationale_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -499,7 +440,7 @@ fn moderate_thread_rationale_too_long() {
 #[test]
 fn moderate_post_successfully() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
@@ -510,7 +451,7 @@ fn moderate_post_successfully() {
 #[test]
 fn moderate_post_rationale_too_short() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let min_len = config.post_moderation_rationale_constraint.min as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -526,7 +467,7 @@ fn moderate_post_rationale_too_short() {
 #[test]
 fn moderate_post_rationale_too_long() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
     let max_len = config.post_moderation_rationale_constraint.max() as usize;
 
     build_test_externalities(config).execute_with(|| {
@@ -542,7 +483,7 @@ fn moderate_post_rationale_too_long() {
 #[test]
 fn cannot_moderate_already_moderated_post() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
@@ -576,7 +517,7 @@ fn not_forum_sudo_cannot_create_root_category() {
 #[test]
 fn not_forum_sudo_cannot_create_subcategory() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let root_category_id = create_root_category(origin);
@@ -611,7 +552,7 @@ fn not_forum_sudo_cannot_undelete_category() {
 #[test]
 fn not_forum_sudo_cannot_moderate_thread() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
@@ -625,7 +566,7 @@ fn not_forum_sudo_cannot_moderate_thread() {
 #[test]
 fn not_forum_sudo_cannot_moderate_post() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
@@ -642,7 +583,7 @@ fn not_forum_sudo_cannot_moderate_post() {
 #[test]
 fn not_member_cannot_create_thread() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         CreateThreadFixture {
@@ -659,7 +600,7 @@ fn not_member_cannot_create_thread() {
 #[test]
 fn not_member_cannot_create_post() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, thread_id) = create_root_category_and_thread(origin);
@@ -676,7 +617,7 @@ fn not_member_cannot_create_post() {
 #[test]
 fn not_member_cannot_edit_post() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin);
@@ -697,7 +638,7 @@ fn not_member_cannot_edit_post() {
 #[test]
 fn cannot_create_subcategory_with_invalid_parent_category_id() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         assert_create_category(
@@ -742,9 +683,10 @@ fn cannot_create_post_with_invalid_thread_id() {
 #[test]
 fn cannot_moderate_thread_with_invalid_id() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
+        set_bureaucracy_forum_lead();
         assert_err!(
             moderate_thread(origin, INVLAID_THREAD_ID, good_rationale()),
             ERROR_THREAD_DOES_NOT_EXIST
@@ -755,9 +697,10 @@ fn cannot_moderate_thread_with_invalid_id() {
 #[test]
 fn cannot_moderate_post_with_invalid_id() {
     let config = default_genesis_config();
-    let origin = OriginType::Signed(config.forum_sudo);
+    let origin = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
+        mock::set_bureaucracy_forum_lead();
         assert_err!(
             moderate_post(origin, INVLAID_POST_ID, good_rationale()),
             ERROR_POST_DOES_NOT_EXIST
@@ -771,7 +714,7 @@ fn cannot_moderate_post_with_invalid_id() {
 #[test]
 fn archive_then_unarchive_category_successfully() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -786,7 +729,7 @@ fn archive_then_unarchive_category_successfully() {
 #[test]
 fn delete_then_undelete_category_successfully() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -802,7 +745,7 @@ fn delete_then_undelete_category_successfully() {
 // #[test]
 // fn cannot_unarchive_not_archived_category() {
 //     let config = default_genesis_config();
-//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+//     let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
 //     build_test_externalities(config).execute_with(|| {
 //         let category_id = create_root_category(forum_sudo.clone());
@@ -823,7 +766,7 @@ fn delete_then_undelete_category_successfully() {
 // #[test]
 // fn cannot_undelete_not_deleted_category() {
 //     let config = default_genesis_config();
-//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+//     let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
 //     build_test_externalities(config).execute_with(|| {
 //         let category_id = create_root_category(forum_sudo.clone());
@@ -843,7 +786,7 @@ fn delete_then_undelete_category_successfully() {
 #[test]
 fn cannot_create_subcategory_in_archived_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -859,7 +802,7 @@ fn cannot_create_subcategory_in_archived_category() {
 #[test]
 fn cannot_create_subcategory_in_deleted_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -875,7 +818,7 @@ fn cannot_create_subcategory_in_deleted_category() {
 #[test]
 fn cannot_create_thread_in_archived_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -891,7 +834,7 @@ fn cannot_create_thread_in_archived_category() {
 #[test]
 fn cannot_create_thread_in_deleted_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -907,7 +850,7 @@ fn cannot_create_thread_in_deleted_category() {
 #[test]
 fn cannot_create_post_in_thread_of_archived_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -925,7 +868,7 @@ fn cannot_create_post_in_thread_of_archived_category() {
 #[test]
 fn cannot_create_post_in_thread_of_deleted_category() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let category_id = create_root_category(forum_sudo.clone());
@@ -943,7 +886,7 @@ fn cannot_create_post_in_thread_of_deleted_category() {
 #[test]
 fn cannot_create_post_in_moderated_thread() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (_, _, thread_id) = create_root_category_and_thread(forum_sudo.clone());
@@ -963,7 +906,7 @@ fn cannot_create_post_in_moderated_thread() {
 #[test]
 fn cannot_edit_post_in_moderated_thread() {
     let config = default_genesis_config();
-    let forum_sudo = OriginType::Signed(config.forum_sudo);
+    let forum_sudo = OriginType::Signed(FORUM_SUDO_ID);
 
     build_test_externalities(config).execute_with(|| {
         let (member_origin, _, thread_id, post_id) =

+ 0 - 1
runtime-modules/proposals/engine/Cargo.toml

@@ -20,7 +20,6 @@ std = [
     'sr-primitives/std',
     'membership/std',
     'common/std',
-
 ]