Bladeren bron

Merge pull request #1970 from mnaamani/integration-tests-refactoring

Integration tests refactoring
Mokhtar Naamani 4 jaren geleden
bovenliggende
commit
0b885534d4
44 gewijzigde bestanden met toevoegingen van 1524 en 1097 verwijderingen
  1. 3 3
      storage-node/packages/runtime-api/index.js
  2. 2 1
      tests/network-tests/run-storage-node-tests.sh
  3. 1 8
      tests/network-tests/run-tests.sh
  4. 186 324
      tests/network-tests/src/Api.ts
  5. 96 16
      tests/network-tests/src/Fixture.ts
  6. 6 0
      tests/network-tests/src/Flow.ts
  7. 16 0
      tests/network-tests/src/InvertedPromise.ts
  8. 128 0
      tests/network-tests/src/Job.ts
  9. 55 0
      tests/network-tests/src/JobManager.ts
  10. 85 0
      tests/network-tests/src/QueryNodeApi.ts
  11. 94 0
      tests/network-tests/src/Resources.ts
  12. 61 0
      tests/network-tests/src/Scenario.ts
  13. 23 37
      tests/network-tests/src/fixtures/contentDirectoryModule.ts
  14. 0 34
      tests/network-tests/src/fixtures/councilElectionHappyCase.ts
  15. 10 11
      tests/network-tests/src/fixtures/councilElectionModule.ts
  16. 28 31
      tests/network-tests/src/fixtures/membershipModule.ts
  17. 92 154
      tests/network-tests/src/fixtures/proposalsModule.ts
  18. 9 10
      tests/network-tests/src/fixtures/sudoHireLead.ts
  19. 95 143
      tests/network-tests/src/fixtures/workingGroupModule.ts
  20. 7 4
      tests/network-tests/src/flows/contentDirectory/contentDirectoryInitialization.ts
  21. 13 5
      tests/network-tests/src/flows/contentDirectory/creatingChannel.ts
  22. 21 13
      tests/network-tests/src/flows/contentDirectory/creatingVideo.ts
  23. 14 10
      tests/network-tests/src/flows/contentDirectory/updatingChannel.ts
  24. 21 14
      tests/network-tests/src/flows/council/setup.ts
  25. 15 15
      tests/network-tests/src/flows/membership/creatingMemberships.ts
  26. 13 4
      tests/network-tests/src/flows/proposals/electionParametersProposal.ts
  27. 48 24
      tests/network-tests/src/flows/proposals/manageLeaderRole.ts
  28. 12 3
      tests/network-tests/src/flows/proposals/spendingProposal.ts
  29. 12 3
      tests/network-tests/src/flows/proposals/textProposal.ts
  30. 13 4
      tests/network-tests/src/flows/proposals/updateRuntime.ts
  31. 12 3
      tests/network-tests/src/flows/proposals/validatorCountProposal.ts
  32. 23 3
      tests/network-tests/src/flows/proposals/workingGroupMintCapacityProposal.ts
  33. 9 3
      tests/network-tests/src/flows/storageNode/getContentFromStorageNode.ts
  34. 26 8
      tests/network-tests/src/flows/workingGroup/atLeastValueBug.ts
  35. 24 11
      tests/network-tests/src/flows/workingGroup/leaderSetup.ts
  36. 29 12
      tests/network-tests/src/flows/workingGroup/manageWorkerAsLead.ts
  37. 26 9
      tests/network-tests/src/flows/workingGroup/manageWorkerAsWorker.ts
  38. 30 10
      tests/network-tests/src/flows/workingGroup/workerPayout.ts
  39. 9 48
      tests/network-tests/src/scenarios/content-directory.ts
  40. 30 56
      tests/network-tests/src/scenarios/full.ts
  41. 4 31
      tests/network-tests/src/scenarios/storage-node.ts
  42. 11 0
      tests/network-tests/src/scenarios/tests/resource-locks-1.ts
  43. 14 0
      tests/network-tests/src/scenarios/tests/resource-locks-2.ts
  44. 98 32
      tests/network-tests/src/sender.ts

+ 3 - 3
storage-node/packages/runtime-api/index.js

@@ -124,8 +124,8 @@ class RuntimeApi {
     return this.workers.isRoleAccountOfStorageProvider(this.storageProviderId, this.identities.key.address)
   }
 
-  executeWithAccountLock(accountId, func) {
-    return this.asyncLock.acquire(`${accountId}`, func)
+  executeWithAccountLock(func) {
+    return this.asyncLock.acquire('tx-queue', func)
   }
 
   static matchingEvents(subscribed = [], events = []) {
@@ -207,7 +207,7 @@ class RuntimeApi {
     }
 
     // synchronize access to nonce
-    await this.executeWithAccountLock(accountId, async () => {
+    await this.executeWithAccountLock(async () => {
       const nonce = await this.api.rpc.system.accountNextIndex(accountId)
       const signed = tx.sign(fromKey, { nonce })
       const txhash = signed.hash

+ 2 - 1
tests/network-tests/run-storage-node-tests.sh

@@ -37,7 +37,8 @@ docker-compose up -d processor
 yarn workspace @joystream/cd-schemas initialize:dev
 
 # Fixes Error: No active storage providers available
-sleep 3m
+echo "Waiting for ipfs name registration"
+sleep 120
 
 echo "Creating channel..."
 yarn joystream-cli media:createChannel \

+ 1 - 8
tests/network-tests/run-tests.sh

@@ -73,13 +73,6 @@ function cleanup() {
 
 trap cleanup EXIT
 
-# Initialize content-directory
-# sleep 15
-# yarn workspace @joystream/cd-schemas initialize:dev
-# NOTE: Skipping this step and let the scenarios do this setup instead
-# or align the scenario expectations of the initial state to match
-# with what we do here.
-
 if [ "$TARGET_RUNTIME" == "$RUNTIME" ]; then
   echo "Not Performing a runtime upgrade."
 else
@@ -103,6 +96,6 @@ fi
 yarn workspace api-scripts tsnode-strict src/status.ts | grep Runtime
 
 echo "Waiting for chain to startup..."
-sleep 5s
+sleep 10
 
 ./run-test-scenario.sh $1

+ 186 - 324
tests/network-tests/src/Api.ts

@@ -16,7 +16,7 @@ import { ElectionStake, Seat } from '@joystream/types/council'
 import { AccountInfo, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { Sender } from './sender'
+import { Sender, LogLevel } from './sender'
 import { Utils } from './utils'
 import { Stake, StakedState, StakeId } from '@joystream/types/stake'
 import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards'
@@ -34,25 +34,26 @@ import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntit
 import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 import { initializeContentDir, InputParser } from '@joystream/cd-schemas'
 import { OperationType } from '@joystream/types/content-directory'
-import { gql, ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'
 import { ContentId, DataObject } from '@joystream/types/media'
-
 import Debugger from 'debug'
-const debug = Debugger('api')
 
 export enum WorkingGroups {
   StorageWorkingGroup = 'storageWorkingGroup',
   ContentDirectoryWorkingGroup = 'contentDirectoryWorkingGroup',
 }
 
-export class Api {
-  protected readonly api: ApiPromise
-  protected readonly sender: Sender
-  protected readonly keyring: Keyring
+export class ApiFactory {
+  private readonly api: ApiPromise
+  private readonly keyring: Keyring
   // source of funds for all new accounts
-  protected readonly treasuryAccount: string
+  private readonly treasuryAccount: string
 
-  public static async create(provider: WsProvider, treasuryAccountUri: string, sudoAccountUri: string): Promise<Api> {
+  public static async create(
+    provider: WsProvider,
+    treasuryAccountUri: string,
+    sudoAccountUri: string
+  ): Promise<ApiFactory> {
+    const debug = Debugger('api-factory')
     let connectAttempts = 0
     while (true) {
       connectAttempts++
@@ -67,7 +68,7 @@ export class Api {
         // Give it a few seconds to be ready.
         await Utils.wait(5000)
 
-        return new Api(api, treasuryAccountUri, sudoAccountUri)
+        return new ApiFactory(api, treasuryAccountUri, sudoAccountUri)
       } catch (err) {
         if (connectAttempts === 3) {
           throw new Error('Unable to connect to chain')
@@ -80,15 +81,40 @@ export class Api {
   constructor(api: ApiPromise, treasuryAccountUri: string, sudoAccountUri: string) {
     this.api = api
     this.keyring = new Keyring({ type: 'sr25519' })
-    const treasuryKey = this.keyring.addFromUri(treasuryAccountUri)
-    this.treasuryAccount = treasuryKey.address
+    this.treasuryAccount = this.keyring.addFromUri(treasuryAccountUri).address
     this.keyring.addFromUri(sudoAccountUri)
-    this.sender = new Sender(api, this.keyring)
+  }
+
+  public getApi(label: string): Api {
+    return new Api(this.api, this.treasuryAccount, this.keyring, label)
   }
 
   public close(): void {
     this.api.disconnect()
   }
+}
+
+export class Api {
+  private readonly api: ApiPromise
+  private readonly sender: Sender
+  private readonly keyring: Keyring
+  // source of funds for all new accounts
+  private readonly treasuryAccount: string
+
+  constructor(api: ApiPromise, treasuryAccount: string, keyring: Keyring, label: string) {
+    this.api = api
+    this.keyring = keyring
+    this.treasuryAccount = treasuryAccount
+    this.sender = new Sender(api, keyring, label)
+  }
+
+  public enableDebugTxLogs(): void {
+    this.sender.setLogLevel(LogLevel.Debug)
+  }
+
+  public enableVerboseTxLogs(): void {
+    this.sender.setLogLevel(LogLevel.Verbose)
+  }
 
   public createKeyPairs(n: number): KeyringPair[] {
     const nKeyPairs: KeyringPair[] = []
@@ -110,25 +136,19 @@ export class Api {
     }
   }
 
-  public async makeSudoCall(tx: SubmittableExtrinsic<'promise'>, expectFailure = false): Promise<ISubmittableResult> {
+  public async makeSudoCall(tx: SubmittableExtrinsic<'promise'>): Promise<ISubmittableResult> {
     const sudo = await this.api.query.sudo.key()
-    return this.sender.signAndSend(this.api.tx.sudo.sudo(tx), sudo, expectFailure)
+    return this.sender.signAndSend(this.api.tx.sudo.sudo(tx), sudo)
   }
 
   public createPaidTermId(value: BN): PaidTermId {
     return this.api.createType('PaidTermId', value)
   }
 
-  public async buyMembership(
-    account: string,
-    paidTermsId: PaidTermId,
-    name: string,
-    expectFailure = false
-  ): Promise<ISubmittableResult> {
+  public async buyMembership(account: string, paidTermsId: PaidTermId, name: string): Promise<ISubmittableResult> {
     return this.sender.signAndSend(
       this.api.tx.members.buyMembership(paidTermsId, /* Handle: */ name, /* Avatar uri: */ '', /* About: */ ''),
-      account,
-      expectFailure
+      account
     )
   }
 
@@ -149,8 +169,8 @@ export class Api {
     return this.transferBalance(this.treasuryAccount, to, amount)
   }
 
-  public treasuryTransferBalanceToAccounts(to: string[], amount: BN): void {
-    to.map((account) => this.transferBalance(this.treasuryAccount, account, amount))
+  public treasuryTransferBalanceToAccounts(to: string[], amount: BN): Promise<ISubmittableResult[]> {
+    return Promise.all(to.map((account) => this.transferBalance(this.treasuryAccount, account, amount)))
   }
 
   public getPaidMembershipTerms(paidTermsId: PaidTermId): Promise<PaidMembershipTerms> {
@@ -552,15 +572,11 @@ export class Api {
   }
 
   private applyForCouncilElection(account: string, amount: BN): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account, false)
+    return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account)
   }
 
-  public batchApplyForCouncilElection(accounts: string[], amount: BN): Promise<void[]> {
-    return Promise.all(
-      accounts.map(async (account) => {
-        await this.applyForCouncilElection(account, amount)
-      })
-    )
+  public batchApplyForCouncilElection(accounts: string[], amount: BN): Promise<ISubmittableResult[]> {
+    return Promise.all(accounts.map(async (account) => this.applyForCouncilElection(account, amount)))
   }
 
   public async getCouncilElectionStake(address: string): Promise<BN> {
@@ -569,44 +585,47 @@ export class Api {
 
   private voteForCouncilMember(account: string, nominee: string, salt: string, stake: BN): Promise<ISubmittableResult> {
     const hashedVote: string = Utils.hashVote(nominee, salt)
-    return this.sender.signAndSend(this.api.tx.councilElection.vote(hashedVote, stake), account, false)
+    return this.sender.signAndSend(this.api.tx.councilElection.vote(hashedVote, stake), account)
   }
 
-  public batchVoteForCouncilMember(accounts: string[], nominees: string[], salt: string[], stake: BN): Promise<void[]> {
+  public batchVoteForCouncilMember(
+    accounts: string[],
+    nominees: string[],
+    salt: string[],
+    stake: BN
+  ): Promise<ISubmittableResult[]> {
     return Promise.all(
-      accounts.map(async (account, index) => {
-        await this.voteForCouncilMember(account, nominees[index], salt[index], stake)
-      })
+      accounts.map(async (account, index) => this.voteForCouncilMember(account, nominees[index], salt[index], stake))
     )
   }
 
   private revealVote(account: string, commitment: string, nominee: string, salt: string): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx.councilElection.reveal(commitment, nominee, salt), account, false)
+    return this.sender.signAndSend(this.api.tx.councilElection.reveal(commitment, nominee, salt), account)
   }
 
-  public batchRevealVote(accounts: string[], nominees: string[], salt: string[]): Promise<void[]> {
+  public batchRevealVote(accounts: string[], nominees: string[], salt: string[]): Promise<ISubmittableResult[]> {
     return Promise.all(
       accounts.map(async (account, index) => {
         const commitment = Utils.hashVote(nominees[index], salt[index])
-        await this.revealVote(account, commitment, nominees[index], salt[index])
+        return this.revealVote(account, commitment, nominees[index], salt[index])
       })
     )
   }
 
   public sudoStartAnnouncingPeriod(endsAtBlock: BN): Promise<ISubmittableResult> {
-    return this.makeSudoCall(this.api.tx.councilElection.setStageAnnouncing(endsAtBlock), false)
+    return this.makeSudoCall(this.api.tx.councilElection.setStageAnnouncing(endsAtBlock))
   }
 
   public sudoStartVotingPeriod(endsAtBlock: BN): Promise<ISubmittableResult> {
-    return this.makeSudoCall(this.api.tx.councilElection.setStageVoting(endsAtBlock), false)
+    return this.makeSudoCall(this.api.tx.councilElection.setStageVoting(endsAtBlock))
   }
 
   public sudoStartRevealingPeriod(endsAtBlock: BN): Promise<ISubmittableResult> {
-    return this.makeSudoCall(this.api.tx.councilElection.setStageRevealing(endsAtBlock), false)
+    return this.makeSudoCall(this.api.tx.councilElection.setStageRevealing(endsAtBlock))
   }
 
   public sudoSetCouncilMintCapacity(capacity: BN): Promise<ISubmittableResult> {
-    return this.makeSudoCall(this.api.tx.council.setCouncilMintCapacity(capacity), false)
+    return this.makeSudoCall(this.api.tx.council.setCouncilMintCapacity(capacity))
   }
 
   public getBestBlock(): Promise<BN> {
@@ -624,12 +643,6 @@ export class Api {
     return council.map((seat) => seat.member.toString())
   }
 
-  // This method is deprecated. Is there a replacement in newer versions of substrate?
-  // Do we even use this method?
-  // public getRuntime(): Promise<Bytes> {
-  //   return this.api.query.substrate.code<Bytes>()
-  // }
-
   public async proposeRuntime(
     account: string,
     stake: BN,
@@ -640,8 +653,7 @@ export class Api {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
       this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(memberId, name, description, stake, runtime),
-      account,
-      false
+      account
     )
   }
 
@@ -655,8 +667,7 @@ export class Api {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
       this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text),
-      account,
-      false
+      account
     )
   }
 
@@ -671,8 +682,7 @@ export class Api {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
       this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination),
-      account,
-      false
+      account
     )
   }
 
@@ -686,8 +696,7 @@ export class Api {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
       this.api.tx.proposalsCodex.createSetValidatorCountProposal(memberId, title, description, stake, validatorCount),
-      account,
-      false
+      account
     )
   }
 
@@ -717,8 +726,7 @@ export class Api {
         min_council_stake: minCouncilStake,
         min_voting_stake: minVotingStake,
       }),
-      account,
-      false
+      account
     )
   }
 
@@ -740,21 +748,20 @@ export class Api {
         openingId,
         this.api.createType('WorkingGroup', workingGroup)
       ),
-      account,
-      false
+      account
     )
   }
 
   public approveProposal(account: string, memberId: MemberId, proposal: ProposalId): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account, false)
+    return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account)
   }
 
-  public async batchApproveProposal(proposal: ProposalId): Promise<void[]> {
+  public async batchApproveProposal(proposal: ProposalId): Promise<ISubmittableResult[]> {
     const councilAccounts = await this.getCouncilAccounts()
     return Promise.all(
       councilAccounts.map(async (account) => {
         const memberId: MemberId = (await this.getMemberIds(account))[0]
-        await this.approveProposal(account, memberId, proposal)
+        return this.approveProposal(account, memberId, proposal)
       })
     )
   }
@@ -767,96 +774,84 @@ export class Api {
     return this.getBlockDuration().muln(durationInBlocks).toNumber()
   }
 
-  public expectMemberRegisteredEvent(events: EventRecord[]): MemberId {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'MemberRegistered')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
-    }
-    return record.event.data[0] as MemberId
+  public findEventRecord(events: EventRecord[], section: string, method: string): EventRecord | undefined {
+    return events.find((record) => record.event.section === section && record.event.method === method)
   }
 
-  public expectProposalCreatedEvent(events: EventRecord[]): ProposalId {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'ProposalCreated')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findMemberRegisteredEvent(events: EventRecord[]): MemberId | undefined {
+    const record = this.findEventRecord(events, 'members', 'MemberRegistered')
+    if (record) {
+      return record.event.data[0] as MemberId
     }
-    return record.event.data[1] as ProposalId
   }
 
-  public expectOpeningAddedEvent(events: EventRecord[]): OpeningId {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'OpeningAdded')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findProposalCreatedEvent(events: EventRecord[]): ProposalId | undefined {
+    const record = this.findEventRecord(events, 'proposalsEngine', 'ProposalCreated')
+    if (record) {
+      return record.event.data[1] as ProposalId
     }
-    return record.event.data[0] as OpeningId
   }
 
-  public expectLeaderSetEvent(events: EventRecord[]): WorkerId {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'LeaderSet')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findOpeningAddedEvent(events: EventRecord[], workingGroup: WorkingGroups): OpeningId | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'OpeningAdded')
+    if (record) {
+      return record.event.data[0] as OpeningId
     }
-    return (record.event.data as unknown) as WorkerId
   }
 
-  public expectBeganApplicationReviewEvent(events: EventRecord[]): ApplicationId {
-    const record = events.find(
-      (record) => record.event.method && record.event.method.toString() === 'BeganApplicationReview'
-    )
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findLeaderSetEvent(events: EventRecord[], workingGroup: WorkingGroups): WorkerId | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'LeaderSet')
+    if (record) {
+      return (record.event.data as unknown) as WorkerId
     }
-    return (record.event.data as unknown) as ApplicationId
   }
 
-  public expectTerminatedLeaderEvent(events: EventRecord[]): void {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'TerminatedLeader')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findBeganApplicationReviewEvent(
+    events: EventRecord[],
+    workingGroup: WorkingGroups
+  ): ApplicationId | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'BeganApplicationReview')
+    if (record) {
+      return (record.event.data as unknown) as ApplicationId
     }
   }
 
-  public expectWorkerRewardAmountUpdatedEvent(events: EventRecord[]): WorkerId {
-    const record = events.find(
-      (record) => record.event.method && record.event.method.toString() === 'WorkerRewardAmountUpdated'
-    )
-    if (!record) {
-      throw new Error('Expected Event Not Found')
-    }
-    return (record.event.data[0] as unknown) as WorkerId
+  public findTerminatedLeaderEvent(events: EventRecord[], workingGroup: WorkingGroups): EventRecord | undefined {
+    return this.findEventRecord(events, workingGroup, 'TerminatedLeader')
   }
 
-  public expectStakeDecreasedEvent(events: EventRecord[]): void {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'StakeDecreased')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findWorkerRewardAmountUpdatedEvent(
+    events: EventRecord[],
+    workingGroup: WorkingGroups,
+    workerId: WorkerId
+  ): WorkerId | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'WorkerRewardAmountUpdated')
+    if (record) {
+      const id = (record.event.data[0] as unknown) as WorkerId
+      if (id.eq(workerId)) {
+        return workerId
+      }
     }
   }
 
-  public expectStakeSlashedEvent(events: EventRecord[]): void {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'StakeSlashed')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
-    }
+  public findStakeDecreasedEvent(events: EventRecord[], workingGroup: WorkingGroups): EventRecord | undefined {
+    return this.findEventRecord(events, workingGroup, 'StakeDecreased')
   }
 
-  public expectMintCapacityChangedEvent(events: EventRecord[]): BN {
-    const record = events.find(
-      (record) => record.event.method && record.event.method.toString() === 'MintCapacityChanged'
-    )
-    if (!record) {
-      throw new Error('Expected Event Not Found')
-    }
-    return (record.event.data[1] as unknown) as BN
+  public findStakeSlashedEvent(events: EventRecord[], workingGroup: WorkingGroups): EventRecord | undefined {
+    return this.findEventRecord(events, workingGroup, 'StakeSlashed')
   }
 
-  public async expectRuntimeUpgraded(): Promise<void> {
-    await this.expectSystemEvent('RuntimeUpdated')
+  public findMintCapacityChangedEvent(events: EventRecord[], workingGroup: WorkingGroups): BN | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'MintCapacityChanged')
+    if (record) {
+      return (record.event.data[1] as unknown) as BN
+    }
   }
 
-  // Resolves with events that were emitted at the same time that the proposal
-  // was finalized (I think!)
-  public waitForProposalToFinalize(id: ProposalId): Promise<EventRecord[]> {
+  // Resolves to true when proposal finalized and executed successfully
+  // Resolved to false when proposal finalized and execution fails
+  public waitForProposalToFinalize(id: ProposalId): Promise<[boolean, EventRecord[]]> {
     return new Promise(async (resolve) => {
       const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
         events.forEach((record) => {
@@ -867,7 +862,7 @@ export class Api {
             record.event.data[1].toString().includes('Executed')
           ) {
             unsubscribe()
-            resolve(events)
+            resolve([true, events])
           } else if (
             record.event.method &&
             record.event.method.toString() === 'ProposalStatusUpdated' &&
@@ -875,24 +870,26 @@ export class Api {
             record.event.data[1].toString().includes('ExecutionFailed')
           ) {
             unsubscribe()
-            resolve(events)
+            resolve([false, events])
           }
         })
       })
     })
   }
 
-  public expectOpeningFilledEvent(events: EventRecord[]): ApplicationIdToWorkerIdMap {
-    const record = events.find((record) => record.event.method && record.event.method.toString() === 'OpeningFilled')
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findOpeningFilledEvent(
+    events: EventRecord[],
+    workingGroup: WorkingGroups
+  ): ApplicationIdToWorkerIdMap | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'OpeningFilled')
+    if (record) {
+      return (record.event.data[1] as unknown) as ApplicationIdToWorkerIdMap
     }
-    return (record.event.data[1] as unknown) as ApplicationIdToWorkerIdMap
   }
 
   // Looks for the first occurance of an expected event, and resolves.
   // Use this when the event we are expecting is not particular to a specific extrinsic
-  public expectSystemEvent(eventName: string): Promise<Event> {
+  public waitForSystemEvent(eventName: string): Promise<Event> {
     return new Promise(async (resolve) => {
       const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
         events.forEach((record) => {
@@ -905,14 +902,14 @@ export class Api {
     })
   }
 
-  public expectApplicationReviewBeganEvent(events: EventRecord[]): ApplicationId {
-    const record = events.find(
-      (record) => record.event.method && record.event.method.toString() === 'BeganApplicationReview'
-    )
-    if (!record) {
-      throw new Error('Expected Event Not Found')
+  public findApplicationReviewBeganEvent(
+    events: EventRecord[],
+    workingGroup: WorkingGroups
+  ): ApplicationId | undefined {
+    const record = this.findEventRecord(events, workingGroup, 'BeganApplicationReview')
+    if (record) {
+      return (record.event.data as unknown) as ApplicationId
     }
-    return (record.event.data as unknown) as ApplicationId
   }
 
   public async getWorkingGroupMintCapacity(module: WorkingGroups): Promise<BN> {
@@ -949,8 +946,7 @@ export class Api {
       text: string
       type: string
     },
-    module: WorkingGroups,
-    expectFailure: boolean
+    module: WorkingGroups
   ): Promise<ISubmittableResult> {
     const activateAt: ActivateOpeningAt = this.api.createType(
       'ActivateOpeningAt',
@@ -1015,8 +1011,7 @@ export class Api {
 
     return this.sender.signAndSend(
       this.createAddOpeningTransaction(activateAt, commitment, openingParameters.text, openingParameters.type, module),
-      lead,
-      expectFailure
+      lead
     )
   }
 
@@ -1107,8 +1102,7 @@ export class Api {
     })
 
     return this.makeSudoCall(
-      this.createAddOpeningTransaction(activateAt, commitment, openingParameters.text, openingParameters.type, module),
-      false
+      this.createAddOpeningTransaction(activateAt, commitment, openingParameters.text, openingParameters.type, module)
     )
   }
 
@@ -1206,8 +1200,7 @@ export class Api {
           working_group: leaderOpening.workingGroup,
         }
       ),
-      leaderOpening.account,
-      false
+      leaderOpening.account
     )
   }
 
@@ -1244,8 +1237,7 @@ export class Api {
         fillOpening.proposalStake,
         fillOpeningParameters
       ),
-      fillOpening.account,
-      false
+      fillOpening.account
     )
   }
 
@@ -1273,8 +1265,7 @@ export class Api {
           'working_group': workingGroup,
         }
       ),
-      account,
-      false
+      account
     )
   }
 
@@ -1298,8 +1289,7 @@ export class Api {
         rewardAmount,
         this.api.createType('WorkingGroup', workingGroup)
       ),
-      account,
-      false
+      account
     )
   }
 
@@ -1323,8 +1313,7 @@ export class Api {
         rewardAmount,
         this.api.createType('WorkingGroup', workingGroup)
       ),
-      account,
-      false
+      account
     )
   }
 
@@ -1348,8 +1337,7 @@ export class Api {
         rewardAmount,
         this.api.createType('WorkingGroup', workingGroup)
       ),
-      account,
-      false
+      account
     )
   }
 
@@ -1371,8 +1359,7 @@ export class Api {
         mintCapacity,
         this.api.createType('WorkingGroup', workingGroup)
       ),
-      account,
-      false
+      account
     )
   }
 
@@ -1391,7 +1378,7 @@ export class Api {
     openingId: OpeningId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].acceptApplications(openingId), leader, false)
+    return this.sender.signAndSend(this.api.tx[module].acceptApplications(openingId), leader)
   }
 
   public async beginApplicantReview(
@@ -1399,11 +1386,11 @@ export class Api {
     openingId: OpeningId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].beginApplicantReview(openingId), leader, false)
+    return this.sender.signAndSend(this.api.tx[module].beginApplicantReview(openingId), leader)
   }
 
   public async sudoBeginApplicantReview(openingId: OpeningId, module: WorkingGroups): Promise<ISubmittableResult> {
-    return this.makeSudoCall(this.api.tx[module].beginApplicantReview(openingId), false)
+    return this.makeSudoCall(this.api.tx[module].beginApplicantReview(openingId))
   }
 
   public async applyOnOpening(
@@ -1413,14 +1400,12 @@ export class Api {
     roleStake: BN,
     applicantStake: BN,
     text: string,
-    expectFailure: boolean,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
       this.api.tx[module].applyOnOpening(memberId, openingId, roleAccountAddress, roleStake, applicantStake, text),
-      account,
-      expectFailure
+      account
     )
   }
 
@@ -1430,12 +1415,11 @@ export class Api {
     roleStake: BN,
     applicantStake: BN,
     text: string,
-    module: WorkingGroups,
-    expectFailure: boolean
+    module: WorkingGroups
   ): Promise<ISubmittableResult[]> {
     return Promise.all(
       accounts.map(async (account) =>
-        this.applyOnOpening(account, account, openingId, roleStake, applicantStake, text, expectFailure, module)
+        this.applyOnOpening(account, account, openingId, roleStake, applicantStake, text, module)
       )
     )
   }
@@ -1455,8 +1439,7 @@ export class Api {
         next_payment_at_block: nextPaymentBlock,
         payout_interval: payoutInterval,
       }),
-      leader,
-      false
+      leader
     )
   }
 
@@ -1473,8 +1456,7 @@ export class Api {
         'amount_per_payout': amountPerPayout,
         'next_payment_at_block': nextPaymentBlock,
         'payout_interval': payoutInterval,
-      }),
-      false
+      })
     )
   }
 
@@ -1484,27 +1466,25 @@ export class Api {
     stake: BN,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].increaseStake(workerId, stake), worker, false)
+    return this.sender.signAndSend(this.api.tx[module].increaseStake(workerId, stake), worker)
   }
 
   public async decreaseStake(
     leader: string,
     workerId: WorkerId,
     stake: BN,
-    module: WorkingGroups,
-    expectFailure: boolean
+    module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].decreaseStake(workerId, stake), leader, expectFailure)
+    return this.sender.signAndSend(this.api.tx[module].decreaseStake(workerId, stake), leader)
   }
 
   public async slashStake(
     leader: string,
     workerId: WorkerId,
     stake: BN,
-    module: WorkingGroups,
-    expectFailure: boolean
+    module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].slashStake(workerId, stake), leader, expectFailure)
+    return this.sender.signAndSend(this.api.tx[module].slashStake(workerId, stake), leader)
   }
 
   public async updateRoleAccount(
@@ -1513,7 +1493,7 @@ export class Api {
     newRoleAccount: string,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].updateRoleAccount(workerId, newRoleAccount), worker, false)
+    return this.sender.signAndSend(this.api.tx[module].updateRoleAccount(workerId, newRoleAccount), worker)
   }
 
   public async updateRewardAccount(
@@ -1522,7 +1502,7 @@ export class Api {
     newRewardAccount: string,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].updateRewardAccount(workerId, newRewardAccount), worker, false)
+    return this.sender.signAndSend(this.api.tx[module].updateRewardAccount(workerId, newRewardAccount), worker)
   }
 
   public async withdrawApplication(
@@ -1530,7 +1510,7 @@ export class Api {
     applicationId: ApplicationId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].withdrawApplication(applicationId), account, false)
+    return this.sender.signAndSend(this.api.tx[module].withdrawApplication(applicationId), account)
   }
 
   public async batchWithdrawActiveApplications(
@@ -1557,7 +1537,7 @@ export class Api {
     applicationId: ApplicationId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].terminateApplication(applicationId), leader, false)
+    return this.sender.signAndSend(this.api.tx[module].terminateApplication(applicationId), leader)
   }
 
   public async batchTerminateApplication(
@@ -1572,33 +1552,30 @@ export class Api {
     leader: string,
     workerId: WorkerId,
     text: string,
-    module: WorkingGroups,
-    expectFailure: boolean
+    module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].terminateRole(workerId, text, false), leader, expectFailure)
+    return this.sender.signAndSend(this.api.tx[module].terminateRole(workerId, text, false), leader)
   }
 
   public async leaveRole(
     account: string,
     workerId: WorkerId,
     text: string,
-    expectFailure: boolean,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(this.api.tx[module].leaveRole(workerId, text), account, expectFailure)
+    return this.sender.signAndSend(this.api.tx[module].leaveRole(workerId, text), account)
   }
 
   public async batchLeaveRole(
     workerIds: WorkerId[],
     text: string,
-    expectFailure: boolean,
     module: WorkingGroups
-  ): Promise<void[]> {
+  ): Promise<ISubmittableResult[]> {
     return Promise.all(
       workerIds.map(async (workerId) => {
         // get role_account of worker
         const worker = await this.getWorkerById(workerId, module)
-        await this.leaveRole(worker.role_account_id.toString(), workerId, text, expectFailure, module)
+        return this.leaveRole(worker.role_account_id.toString(), workerId, text, module)
       })
     )
   }
@@ -1752,16 +1729,16 @@ export class Api {
     return this.api.createType('u32', this.api.consts[module].maxWorkerNumberLimit)
   }
 
-  async sendContentDirectoryTransaction(operations: OperationType[]): Promise<void> {
+  async sendContentDirectoryTransaction(operations: OperationType[]): Promise<ISubmittableResult> {
     const transaction = this.api.tx.contentDirectory.transaction(
       { Lead: null }, // We use member with id 0 as actor (in this case we assume this is Alice)
       operations // We provide parsed operations as second argument
     )
     const lead = (await this.getGroupLead(WorkingGroups.ContentDirectoryWorkingGroup)) as Worker
-    await this.sender.signAndSend(transaction, lead.role_account_id, false)
+    return this.sender.signAndSend(transaction, lead.role_account_id)
   }
 
-  public async createChannelEntity(channel: ChannelEntity): Promise<void> {
+  public async createChannelEntity(channel: ChannelEntity): Promise<ISubmittableResult> {
     // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
     const parser = InputParser.createWithKnownSchemas(
       this.api,
@@ -1775,10 +1752,10 @@ export class Api {
     )
     // We parse the input into CreateEntity and AddSchemaSupportToEntity operations
     const operations = await parser.getEntityBatchOperations()
-    return await this.sendContentDirectoryTransaction(operations)
+    return this.sendContentDirectoryTransaction(operations)
   }
 
-  public async createVideoEntity(video: VideoEntity): Promise<void> {
+  public async createVideoEntity(video: VideoEntity): Promise<ISubmittableResult> {
     // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
     const parser = InputParser.createWithKnownSchemas(
       this.api,
@@ -1792,13 +1769,13 @@ export class Api {
     )
     // We parse the input into CreateEntity and AddSchemaSupportToEntity operations
     const operations = await parser.getEntityBatchOperations()
-    return await this.sendContentDirectoryTransaction(operations)
+    return this.sendContentDirectoryTransaction(operations)
   }
 
   public async updateChannelEntity(
     channelUpdateInput: Record<string, any>,
     uniquePropValue: Record<string, any>
-  ): Promise<void> {
+  ): Promise<ISubmittableResult> {
     // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
     const parser = InputParser.createWithKnownSchemas(this.api)
 
@@ -1810,7 +1787,7 @@ export class Api {
       'Channel', // Class name
       CHANNEL_ID // Id of the entity we want to update
     )
-    return await this.sendContentDirectoryTransaction(updateOperations)
+    return this.sendContentDirectoryTransaction(updateOperations)
   }
 
   async getDataObjectByContentId(contentId: ContentId): Promise<DataObject | null> {
@@ -1818,127 +1795,12 @@ export class Api {
     return dataObject.unwrapOr(null)
   }
 
-  public async initializeContentDirectory(leadKeyPair: KeyringPair): Promise<void> {
-    await initializeContentDir(this.api, leadKeyPair)
-  }
-}
-
-export class QueryNodeApi extends Api {
-  private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
-
-  public static async new(
-    provider: WsProvider,
-    queryNodeProvider: ApolloClient<NormalizedCacheObject>,
-    treasuryAccountUri: string,
-    sudoAccountUri: string
-  ): Promise<QueryNodeApi> {
-    let connectAttempts = 0
-    while (true) {
-      connectAttempts++
-      debug(`Connecting to chain, attempt ${connectAttempts}..`)
-      try {
-        const api = await ApiPromise.create({ provider, types })
-
-        // Wait for api to be connected and ready
-        await api.isReady
-
-        // If a node was just started up it might take a few seconds to start producing blocks
-        // Give it a few seconds to be ready.
-        await Utils.wait(5000)
-
-        return new QueryNodeApi(api, queryNodeProvider, treasuryAccountUri, sudoAccountUri)
-      } catch (err) {
-        if (connectAttempts === 3) {
-          throw new Error('Unable to connect to chain')
-        }
-      }
-      await Utils.wait(5000)
+  public async initializeContentDirectory(): Promise<void> {
+    const lead = await this.getGroupLead(WorkingGroups.ContentDirectoryWorkingGroup)
+    if (!lead) {
+      throw new Error('No Lead is set for storage wokring group')
     }
-  }
-
-  constructor(
-    api: ApiPromise,
-    queryNodeProvider: ApolloClient<NormalizedCacheObject>,
-    treasuryAccountUri: string,
-    sudoAccountUri: string
-  ) {
-    super(api, treasuryAccountUri, sudoAccountUri)
-    this.queryNodeProvider = queryNodeProvider
-  }
-
-  public async getChannelbyHandle(handle: string): Promise<ApolloQueryResult<any>> {
-    const GET_CHANNEL_BY_TITLE = gql`
-      query($handle: String!) {
-        channels(where: { handle_eq: $handle }) {
-          handle
-          description
-          coverPhotoUrl
-          avatarPhotoUrl
-          isPublic
-          isCurated
-          videos {
-            title
-            description
-            duration
-            thumbnailUrl
-            isExplicit
-            isPublic
-          }
-        }
-      }
-    `
-
-    return await this.queryNodeProvider.query({ query: GET_CHANNEL_BY_TITLE, variables: { handle } })
-  }
-
-  public async performFullTextSearchOnChannelTitle(text: string): Promise<ApolloQueryResult<any>> {
-    const FULL_TEXT_SEARCH_ON_CHANNEL_TITLE = gql`
-      query($text: String!) {
-        search(text: $text) {
-          item {
-            ... on Channel {
-              handle
-              description
-            }
-          }
-        }
-      }
-    `
-
-    return await this.queryNodeProvider.query({ query: FULL_TEXT_SEARCH_ON_CHANNEL_TITLE, variables: { text } })
-  }
-
-  public async performFullTextSearchOnVideoTitle(text: string): Promise<ApolloQueryResult<any>> {
-    const FULL_TEXT_SEARCH_ON_VIDEO_TITLE = gql`
-      query($text: String!) {
-        search(text: $text) {
-          item {
-            ... on Video {
-              title
-            }
-          }
-        }
-      }
-    `
-
-    return await this.queryNodeProvider.query({ query: FULL_TEXT_SEARCH_ON_VIDEO_TITLE, variables: { text } })
-  }
-
-  public async performWhereQueryByVideoTitle(title: string): Promise<ApolloQueryResult<any>> {
-    const WHERE_QUERY_ON_VIDEO_TITLE = gql`
-      query($title: String!) {
-        videos(where: { title_eq: $title }) {
-          media {
-            location {
-              __typename
-              ... on JoystreamMediaLocation {
-                dataObjectId
-              }
-            }
-          }
-        }
-      }
-    `
-    return await this.queryNodeProvider.query({ query: WHERE_QUERY_ON_VIDEO_TITLE, variables: { title } })
+    const leadKeyPair = this.keyring.getPair(lead.role_account_id.toString())
+    return initializeContentDir(this.api, leadKeyPair)
   }
 }

+ 96 - 16
tests/network-tests/src/Fixture.ts

@@ -1,30 +1,110 @@
 import { Api } from './Api'
+import { assert } from 'chai'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { DispatchResult } from '@polkadot/types/interfaces/system'
 
-export interface Fixture {
-  runner(expectFailure: boolean): Promise<void>
+export abstract class BaseFixture {
+  protected readonly api: Api
+  private _executed = false
+  // The reason of the "Unexpected" failure of running the fixture
+  private _err: Error | undefined = undefined
+
+  constructor(api: Api) {
+    this.api = api
+  }
+
+  // Derviced classes must not override this
+  public async runner(): Promise<void> {
+    this._executed = true
+    return this.execute()
+  }
+
+  abstract execute(): Promise<void>
+
+  // Used by execution implementation to signal failure
+  protected error(err: Error): void {
+    this._err = err
+  }
+
+  get executed(): boolean {
+    return this._executed
+  }
+
+  public didFail(): boolean {
+    if (!this.execute) {
+      throw new Error('Trying to check execution result before running fixture')
+    }
+    return this._err !== undefined
+  }
+
+  public executionError(): Error | undefined {
+    if (!this.executed) {
+      throw new Error('Trying to check execution result before running fixture')
+    }
+    return this._err
+  }
+
+  protected expectDispatchError(result: ISubmittableResult, errMessage: string): ISubmittableResult {
+    const success = result.findRecord('system', 'ExtrinsicSuccess')
+
+    if (success) {
+      const sudid = result.findRecord('sudo', 'Sudid')
+      if (sudid) {
+        const dispatchResult = sudid.event.data[0] as DispatchResult
+        if (dispatchResult.isOk) {
+          this.error(new Error(errMessage))
+        }
+      } else {
+        this.error(new Error(errMessage))
+      }
+    }
+
+    return result
+  }
+
+  protected expectDispatchSuccess(result: ISubmittableResult, errMessage: string): ISubmittableResult {
+    const success = result.findRecord('system', 'ExtrinsicSuccess')
+
+    if (success) {
+      const sudid = result.findRecord('sudo', 'Sudid')
+      if (sudid) {
+        const dispatchResult = sudid.event.data[0] as DispatchResult
+        if (dispatchResult.isError) {
+          this.error(new Error(errMessage))
+          // Log DispatchError details
+        }
+      }
+    } else {
+      this.error(new Error(errMessage))
+      // Log DispatchError
+    }
+
+    return result
+  }
 }
 
-// Fixture that measures start and end blocks
-// ensures fixture only runs once
-export class BaseFixture implements Fixture {
-  protected api: Api
+// Runs a fixture and measures how long it took to run
+// Ensures fixture only runs once, and asserts that it doesn't fail
+export class FixtureRunner {
+  private fixture: BaseFixture
   private ran = false
 
-  constructor(api: Api) {
-    this.api = api
-    // record starting block
+  constructor(fixture: BaseFixture) {
+    this.fixture = fixture
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async run(): Promise<void> {
     if (this.ran) {
-      return
+      throw new Error('Fixture already ran')
     }
+
     this.ran = true
-    return this.execute(expectFailure)
-    // record end blocks
-  }
 
-  protected async execute(expectFailure: boolean): Promise<void> {
-    return
+    // TODO: record starting block
+
+    await this.fixture.runner()
+    // TODO: record ending block
+    const err = this.fixture.executionError()
+    assert.equal(err, undefined)
   }
 }

+ 6 - 0
tests/network-tests/src/Flow.ts

@@ -0,0 +1,6 @@
+import { Api } from './Api'
+import { QueryNodeApi } from './QueryNodeApi'
+import { ResourceLocker } from './Resources'
+
+export type FlowProps = { api: Api; env: NodeJS.ProcessEnv; query: QueryNodeApi; lock: ResourceLocker }
+export type Flow = (args: FlowProps) => Promise<void>

+ 16 - 0
tests/network-tests/src/InvertedPromise.ts

@@ -0,0 +1,16 @@
+function noop(): void {
+  // No-Op
+}
+
+export class InvertedPromise<T> {
+  public resolve: (value: T) => void = noop
+  public reject: (reason?: any) => void = noop
+  public readonly promise: Promise<T>
+
+  constructor() {
+    this.promise = new Promise((resolve, reject) => {
+      this.resolve = resolve
+      this.reject = reject
+    })
+  }
+}

+ 128 - 0
tests/network-tests/src/Job.ts

@@ -0,0 +1,128 @@
+import Debugger from 'debug'
+import { EventEmitter } from 'events'
+import { ApiFactory } from './Api'
+import { QueryNodeApi } from './QueryNodeApi'
+import { Flow } from './Flow'
+import { InvertedPromise } from './InvertedPromise'
+import { ResourceManager } from './Resources'
+
+export type JobProps = { apiFactory: ApiFactory; env: NodeJS.ProcessEnv; query: QueryNodeApi }
+
+export enum JobOutcome {
+  Succeeded = 'Succeeded',
+  Failed = 'Failed',
+  Skipped = 'Skipped',
+}
+
+export class Job {
+  private _required: Job[] = []
+  private _after: Job[] = []
+  private _locked = false
+  private readonly _flows: Flow[]
+  private readonly _manager: EventEmitter
+  private readonly _outcome: InvertedPromise<JobOutcome>
+  private readonly _label: string
+  private readonly debug: Debugger.Debugger
+
+  constructor(manager: EventEmitter, flows: Flow[], label: string) {
+    this._label = label
+    this._manager = manager
+    this._flows = flows
+    this._outcome = new InvertedPromise<JobOutcome>()
+    this._manager.on('run', this.run.bind(this))
+    this.debug = Debugger(`job:${this._label}`)
+  }
+
+  // Depend on another job to complete successfully
+  public requires(job: Job): Job {
+    if (this._locked) throw new Error('Job is locked')
+    if (job === this) throw new Error('Job Cannot depend on itself')
+    if (job.hasDependencyOn(this)) {
+      throw new Error('Job Circualr dependency')
+    }
+    this._required.push(job)
+    return this
+  }
+
+  // Depend on another job to complete (does not matter if it is successful)
+  public after(job: Job): Job {
+    if (this._locked) throw new Error('Job is locked')
+    if (job === this) throw new Error('Job Cannot depend on itself')
+    if (job.hasDependencyOn(this)) {
+      throw new Error('Job Circualr dependency')
+    }
+    this._after.push(job)
+    return this
+  }
+
+  public then(job: Job): Job {
+    job.requires(this)
+    return job
+  }
+
+  public hasDependencyOn(job: Job): boolean {
+    return !!this._required.find((j) => j === job) || !!this._after.find((j) => j === job)
+  }
+
+  // configure to have flows run serially instead of in parallel
+  // public serially(): Job {
+  //   return this
+  // }
+
+  get outcome(): Promise<JobOutcome> {
+    return this._outcome.promise
+  }
+
+  get label(): string {
+    return this._label
+  }
+
+  private async run(jobProps: JobProps, resources: ResourceManager): Promise<void> {
+    // prevent any additional changes to configuration
+    this._locked = true
+
+    // wait for all required dependencies to complete successfully
+    const requiredOutcomes = await Promise.all(this._required.map((job) => job.outcome))
+    if (requiredOutcomes.find((outcome) => outcome !== JobOutcome.Succeeded)) {
+      this.debug('[Skipping] - Required jobs not successful!')
+      return this._outcome.resolve(JobOutcome.Skipped)
+    }
+
+    // Wait for other jobs to complete, irrespective of outcome
+    await Promise.all(this._after.map((job) => job.outcome))
+
+    this.debug('Running')
+    const flowRunResults = await Promise.allSettled(
+      this._flows.map(async (flow, index) => {
+        const locker = resources.createLocker()
+        try {
+          await flow({
+            api: jobProps.apiFactory.getApi(`${this.label}:${flow.name}-${index}`),
+            env: jobProps.env,
+            query: jobProps.query,
+            lock: locker.lock,
+          })
+        } catch (err) {
+          locker.release()
+          throw err
+        }
+        locker.release()
+      })
+    )
+
+    flowRunResults.forEach((result, ix) => {
+      if (result.status === 'rejected') {
+        this.debug(`flow ${ix} failed:`)
+        console.error(result.reason)
+      }
+    })
+
+    if (flowRunResults.find((result) => result.status === 'rejected')) {
+      this.debug('[Failed]')
+      this._outcome.resolve(JobOutcome.Failed)
+    } else {
+      this.debug('[Succeeded]')
+      this._outcome.resolve(JobOutcome.Succeeded)
+    }
+  }
+}

+ 55 - 0
tests/network-tests/src/JobManager.ts

@@ -0,0 +1,55 @@
+import { EventEmitter } from 'events'
+import { Flow } from './Flow'
+import { Job, JobOutcome, JobProps } from './Job'
+import { ApiFactory } from './Api'
+import { QueryNodeApi } from './QueryNodeApi'
+import { ResourceManager } from './Resources'
+
+export class JobManager extends EventEmitter {
+  private _jobs: Job[] = []
+  private readonly _apiFactory: ApiFactory
+  private readonly _env: NodeJS.ProcessEnv
+  private readonly _query: QueryNodeApi
+
+  constructor({ apiFactory, env, query }: { apiFactory: ApiFactory; env: NodeJS.ProcessEnv; query: QueryNodeApi }) {
+    super()
+    this._apiFactory = apiFactory
+    this._env = env
+    this._query = query
+  }
+
+  public createJob(label: string, flows: Flow[] | Flow): Job {
+    const arrFlows: Array<Flow> = []
+    const job = new Job(this, arrFlows.concat(flows), label)
+
+    this._jobs.push(job)
+
+    return job
+  }
+
+  private getJobProps(): JobProps {
+    return {
+      env: this._env,
+      query: this._query,
+      apiFactory: this._apiFactory,
+    }
+  }
+
+  public async run(resources: ResourceManager): Promise<void> {
+    this.emit('run', this.getJobProps(), resources)
+
+    const outcomes = await Promise.all(this._jobs.map((job) => job.outcome))
+
+    // summary of job results
+    console.error('Job Results:')
+    outcomes.forEach((outcome, i) => {
+      const { label } = this._jobs[i]
+      console.error(`${label}: ${outcome}`)
+    })
+
+    const failed = outcomes.find((outcome) => outcome !== JobOutcome.Succeeded)
+    if (failed) {
+      throw new Error('Scenario Failed')
+    }
+  }
+}

+ 85 - 0
tests/network-tests/src/QueryNodeApi.ts

@@ -0,0 +1,85 @@
+import { gql, ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'
+
+export class QueryNodeApi {
+  private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
+
+  constructor(queryNodeProvider: ApolloClient<NormalizedCacheObject>) {
+    this.queryNodeProvider = queryNodeProvider
+  }
+
+  public async getChannelbyHandle(handle: string): Promise<ApolloQueryResult<any>> {
+    const GET_CHANNEL_BY_TITLE = gql`
+      query($handle: String!) {
+        channels(where: { handle_eq: $handle }) {
+          handle
+          description
+          coverPhotoUrl
+          avatarPhotoUrl
+          isPublic
+          isCurated
+          videos {
+            title
+            description
+            duration
+            thumbnailUrl
+            isExplicit
+            isPublic
+          }
+        }
+      }
+    `
+
+    return await this.queryNodeProvider.query({ query: GET_CHANNEL_BY_TITLE, variables: { handle } })
+  }
+
+  public async performFullTextSearchOnChannelTitle(text: string): Promise<ApolloQueryResult<any>> {
+    const FULL_TEXT_SEARCH_ON_CHANNEL_TITLE = gql`
+      query($text: String!) {
+        search(text: $text) {
+          item {
+            ... on Channel {
+              handle
+              description
+            }
+          }
+        }
+      }
+    `
+
+    return await this.queryNodeProvider.query({ query: FULL_TEXT_SEARCH_ON_CHANNEL_TITLE, variables: { text } })
+  }
+
+  public async performFullTextSearchOnVideoTitle(text: string): Promise<ApolloQueryResult<any>> {
+    const FULL_TEXT_SEARCH_ON_VIDEO_TITLE = gql`
+      query($text: String!) {
+        search(text: $text) {
+          item {
+            ... on Video {
+              title
+            }
+          }
+        }
+      }
+    `
+
+    return await this.queryNodeProvider.query({ query: FULL_TEXT_SEARCH_ON_VIDEO_TITLE, variables: { text } })
+  }
+
+  public async performWhereQueryByVideoTitle(title: string): Promise<ApolloQueryResult<any>> {
+    const WHERE_QUERY_ON_VIDEO_TITLE = gql`
+      query($title: String!) {
+        videos(where: { title_eq: $title }) {
+          media {
+            location {
+              __typename
+              ... on JoystreamMediaLocation {
+                dataObjectId
+              }
+            }
+          }
+        }
+      }
+    `
+    return await this.queryNodeProvider.query({ query: WHERE_QUERY_ON_VIDEO_TITLE, variables: { title } })
+  }
+}

+ 94 - 0
tests/network-tests/src/Resources.ts

@@ -0,0 +1,94 @@
+import { assert } from 'chai'
+import { Utils } from './utils'
+import Debugger from 'debug'
+
+const debug = Debugger('resources')
+
+type NamedLocks = Record<Resource, Lock>
+export type ResourceLocker = (resource: Resource, timeout?: number) => Promise<() => void>
+
+class Lock {
+  private name: string
+
+  // the number of concurrent locks that can be acquired concurrently before the resource
+  // becomes unavailable until a lock is released.
+  private readonly concurrency: number
+  private lockCount = 0
+
+  constructor(key: string, concurrency?: number) {
+    this.name = key
+    this.concurrency = concurrency || 1
+  }
+
+  public async lock(timeoutMinutes = 2): Promise<() => void> {
+    const timeoutAt = Date.now() + timeoutMinutes * 60 * 1000
+
+    while (this.lockCount === this.concurrency) {
+      debug(`waiting for ${this.name}`)
+      await Utils.wait(30000)
+      if (Date.now() > timeoutAt) throw new Error(`Timeout getting resource lock: ${this.name}`)
+    }
+
+    debug(`acquired ${this.name}`)
+    this.lockCount++
+
+    // Return a function used to release the lock
+    return (() => {
+      let called = false
+      return () => {
+        if (called) return
+        called = true
+        debug(`released ${this.name}`)
+        this.lockCount--
+      }
+    })()
+  }
+}
+
+export enum Resource {
+  Council = 'Council',
+  Proposals = 'Proposals',
+}
+
+export class ResourceManager {
+  // Internal Map
+  private resources = new Map<string, Lock>()
+
+  private readonly locks: NamedLocks
+
+  constructor() {
+    this.locks = this.createNamedLocks()
+  }
+
+  private add(key: string, concurrency?: number): Lock {
+    assert(!this.resources.has(key))
+    this.resources.set(key, new Lock(key, concurrency))
+    return this.resources.get(key) as Lock
+  }
+
+  private createNamedLocks(): NamedLocks {
+    return {
+      [Resource.Council]: this.add(Resource.Council),
+      // We assume that a flow will only have one active proposal at a time
+      // Runtime is configured for MaxActiveProposalLimit = 5
+      // So we should ensure we don't exceed that number of active proposals
+      // which limits the number of concurrent tests that create proposals
+      [Resource.Proposals]: this.add(Resource.Proposals, 5),
+    }
+  }
+
+  public createLocker(): { release: () => void; lock: ResourceLocker } {
+    const unlockers: Array<() => void> = []
+    const release = () => {
+      unlockers.forEach((unlock) => unlock())
+    }
+    return {
+      release,
+      lock: async (resource: Resource, timeout?: number) => {
+        const unlock = await this.locks[resource].lock(timeout)
+        unlockers.push(unlock)
+        return unlock
+      },
+    }
+  }
+}

+ 61 - 0
tests/network-tests/src/Scenario.ts

@@ -0,0 +1,61 @@
+import { WsProvider } from '@polkadot/api'
+import { ApiFactory } from './Api'
+import { QueryNodeApi } from './QueryNodeApi'
+import { config } from 'dotenv'
+import { ApolloClient, InMemoryCache } from '@apollo/client'
+import Debugger from 'debug'
+import { Flow } from './Flow'
+import { Job } from './Job'
+import { JobManager } from './JobManager'
+import { ResourceManager } from './Resources'
+
+export type ScenarioProps = {
+  env: NodeJS.ProcessEnv
+  debug: Debugger.Debugger
+  job: (label: string, flows: Flow[] | Flow) => Job
+}
+
+export async function scenario(scene: (props: ScenarioProps) => Promise<void>): Promise<void> {
+  // Load env variables
+  config()
+  const env = process.env
+
+  // Connect api to the chain
+  const nodeUrl: string = env.NODE_URL || 'ws://127.0.0.1:9944'
+  const provider = new WsProvider(nodeUrl)
+
+  const apiFactory = await ApiFactory.create(
+    provider,
+    env.TREASURY_ACCOUNT_URI || '//Alice',
+    env.SUDO_ACCOUNT_URI || '//Alice'
+  )
+
+  const queryNodeUrl: string = env.QUERY_NODE_URL || 'http://127.0.0.1:8081/graphql'
+
+  const queryNodeProvider = new ApolloClient({
+    uri: queryNodeUrl,
+    cache: new InMemoryCache(),
+    defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
+  })
+
+  const query = new QueryNodeApi(queryNodeProvider)
+
+  const debug = Debugger('scenario')
+
+  const jobs = new JobManager({ apiFactory, query, env })
+
+  await scene({ env, debug, job: jobs.createJob.bind(jobs) })
+
+  const resources = new ResourceManager()
+
+  try {
+    await jobs.run(resources)
+  } catch (err) {
+    console.error(err)
+    process.exit(-1)
+  }
+
+  // Note: disconnecting and then reconnecting to the chain in the same process
+  // doesn't seem to work!
+  apiFactory.close()
+}

+ 23 - 37
tests/network-tests/src/fixtures/contentDirectoryModule.ts

@@ -1,65 +1,51 @@
-import { QueryNodeApi } from '../Api'
-import BN from 'bn.js'
-import { assert } from 'chai'
-import { Seat } from '@joystream/types/council'
-import { v4 as uuid } from 'uuid'
-import { Utils } from '../utils'
-import { Fixture } from '../Fixture'
+import { Api } from '../Api'
+import { BaseFixture } from '../Fixture'
 import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 
-export class CreateChannelFixture implements Fixture {
-  private api: QueryNodeApi
+export class CreateChannelFixture extends BaseFixture {
   public channelEntity: ChannelEntity
 
-  public constructor(api: QueryNodeApi, channelEntity: ChannelEntity) {
-    this.api = api
+  public constructor(api: Api, channelEntity: ChannelEntity) {
+    super(api)
     this.channelEntity = channelEntity
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
-    await this.api.createChannelEntity(this.channelEntity)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+  public async execute(): Promise<void> {
+    this.expectDispatchSuccess(
+      await this.api.createChannelEntity(this.channelEntity),
+      'Create Channel should have succeeded'
+    )
   }
 }
 
-export class CreateVideoFixture implements Fixture {
-  private api: QueryNodeApi
+export class CreateVideoFixture extends BaseFixture {
   public videoEntity: VideoEntity
 
-  public constructor(api: QueryNodeApi, videoEntity: VideoEntity) {
-    this.api = api
+  public constructor(api: Api, videoEntity: VideoEntity) {
+    super(api)
     this.videoEntity = videoEntity
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
-    await this.api.createVideoEntity(this.videoEntity)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+  public async execute(): Promise<void> {
+    this.expectDispatchSuccess(await this.api.createVideoEntity(this.videoEntity), 'Create Video should have succeeded')
   }
 }
 
-export class UpdateChannelFixture implements Fixture {
-  private api: QueryNodeApi
+export class UpdateChannelFixture extends BaseFixture {
   private channelUpdateInput: Record<string, any>
   private uniquePropValue: Record<string, any>
 
-  public constructor(api: QueryNodeApi, channelUpdateInput: Record<string, any>, uniquePropValue: Record<string, any>) {
-    this.api = api
+  public constructor(api: Api, channelUpdateInput: Record<string, any>, uniquePropValue: Record<string, any>) {
+    super(api)
     this.channelUpdateInput = channelUpdateInput
     this.uniquePropValue = uniquePropValue
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
-    await this.api.updateChannelEntity(this.channelUpdateInput, this.uniquePropValue)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+  public async execute(): Promise<void> {
+    this.expectDispatchSuccess(
+      await this.api.updateChannelEntity(this.channelUpdateInput, this.uniquePropValue),
+      'Update Channel should have succeeded'
+    )
   }
 }

+ 0 - 34
tests/network-tests/src/fixtures/councilElectionHappyCase.ts

@@ -1,34 +0,0 @@
-import { Fixture } from '../Fixture'
-import { ElectCouncilFixture } from './councilElectionModule'
-import { Api } from '../Api'
-import BN from 'bn.js'
-
-export class CouncilElectionHappyCaseFixture implements Fixture {
-  private api: Api
-  private voters: string[]
-  private applicants: string[]
-  private k: number
-  private greaterStake: BN
-  private lesserStake: BN
-
-  constructor(api: Api, voters: string[], applicants: string[], k: number, greaterStake: BN, lesserStake: BN) {
-    this.api = api
-    this.voters = voters
-    this.applicants = applicants
-    this.k = k
-    this.greaterStake = greaterStake
-    this.lesserStake = lesserStake
-  }
-
-  public async runner(expectFailure: boolean): Promise<void> {
-    const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
-      this.api,
-      this.voters,
-      this.applicants,
-      this.k,
-      this.greaterStake,
-      this.lesserStake
-    )
-    await electCouncilFixture.runner(false)
-  }
-}

+ 10 - 11
tests/network-tests/src/fixtures/councilElectionModule.ts

@@ -4,10 +4,9 @@ import { assert } from 'chai'
 import { Seat } from '@joystream/types/council'
 import { v4 as uuid } from 'uuid'
 import { Utils } from '../utils'
-import { Fixture } from '../Fixture'
+import { BaseFixture } from '../Fixture'
 
-export class ElectCouncilFixture implements Fixture {
-  private api: Api
+export class ElectCouncilFixture extends BaseFixture {
   private voters: string[]
   private applicants: string[]
   private k: number
@@ -15,7 +14,7 @@ export class ElectCouncilFixture implements Fixture {
   private lesserStake: BN
 
   public constructor(api: Api, voters: string[], applicants: string[], k: number, greaterStake: BN, lesserStake: BN) {
-    this.api = api
+    super(api)
     this.voters = voters
     this.applicants = applicants
     this.k = k
@@ -23,9 +22,11 @@ export class ElectCouncilFixture implements Fixture {
     this.lesserStake = lesserStake
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Assert no council exists
-    assert((await this.api.getCouncil()).length === 0)
+    if ((await this.api.getCouncil()).length) {
+      return this.error(new Error('Council election fixture expects no council seats to be filled'))
+    }
 
     let now = await this.api.getBestBlock()
     const applyForCouncilFee: BN = this.api.estimateApplyForCouncilFee(this.greaterStake)
@@ -98,15 +99,13 @@ export class ElectCouncilFixture implements Fixture {
     const seats: Seat[] = await this.api.getCouncil()
 
     // Assert a council was created
-    assert(seats.length)
+    if (!seats.length) {
+      this.error(new Error('Expected council to be elected'))
+    }
 
     // const applicantAddresses: string[] = this.applicantKeyPairs.map((keyPair) => keyPair.address)
     // const voterAddresses: string[] = this.voterKeyPairs.map((keyPair) => keyPair.address)
     // const councilMembers: string[] = seats.map((seat) => seat.member.toString())
     // const backers: string[] = seats.map((seat) => seat.backers.map((backer) => backer.member.toString())).flat()
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }

+ 28 - 31
tests/network-tests/src/fixtures/membershipModule.ts

@@ -1,7 +1,7 @@
 import { Api } from '../Api'
 import BN from 'bn.js'
 import { assert } from 'chai'
-import { Fixture, BaseFixture } from '../Fixture'
+import { BaseFixture } from '../Fixture'
 import { PaidTermId, MemberId } from '@joystream/types/members'
 import Debugger from 'debug'
 
@@ -22,8 +22,7 @@ export class BuyMembershipHappyCaseFixture extends BaseFixture {
     return this.memberIds.slice()
   }
 
-  public async execute(expectFailure: boolean): Promise<void> {
-    this.debug(`Registering ${this.accounts.length} new members`)
+  async execute(): Promise<void> {
     // Fee estimation and transfer
     const membershipFee: BN = await this.api.getMembershipFee(this.paidTerms)
     const membershipTransactionFee: BN = this.api.estimateBuyMembershipFee(
@@ -31,6 +30,7 @@ export class BuyMembershipHappyCaseFixture extends BaseFixture {
       this.paidTerms,
       'member_name_which_is_longer_than_expected'
     )
+
     this.api.treasuryTransferBalanceToAccounts(this.accounts, membershipTransactionFee.add(new BN(membershipFee)))
 
     this.memberIds = (
@@ -39,29 +39,31 @@ export class BuyMembershipHappyCaseFixture extends BaseFixture {
           this.api.buyMembership(account, this.paidTerms, `member${account.substring(0, 14)}`)
         )
       )
-    ).map(({ events }) => this.api.expectMemberRegisteredEvent(events))
+    )
+      .map(({ events }) => this.api.findMemberRegisteredEvent(events))
+      .filter((id) => id !== undefined) as MemberId[]
+
+    this.debug(`Registered ${this.memberIds.length} new members`)
 
-    this.debug(`New member ids: ${this.memberIds}`)
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    assert.equal(this.memberIds.length, this.accounts.length)
   }
 }
 
-export class BuyMembershipWithInsufficienFundsFixture implements Fixture {
-  private api: Api
+export class BuyMembershipWithInsufficienFundsFixture extends BaseFixture {
   private account: string
   private paidTerms: PaidTermId
 
   public constructor(api: Api, account: string, paidTerms: PaidTermId) {
-    this.api = api
+    super(api)
     this.account = account
     this.paidTerms = paidTerms
   }
 
-  public async runner(expectFailure: boolean) {
+  async execute(): Promise<void> {
     // Assertions
-    this.api.getMemberIds(this.account).then((membership) => assert(membership.length === 0, 'Account A is a member'))
+    const membership = await this.api.getMemberIds(this.account)
+
+    assert(membership.length === 0, 'Account must not be associated with a member')
 
     // Fee estimation and transfer
     const membershipFee: BN = await this.api.getMembershipFee(this.paidTerms)
@@ -70,25 +72,20 @@ export class BuyMembershipWithInsufficienFundsFixture implements Fixture {
       this.paidTerms,
       'member_name_which_is_longer_than_expected'
     )
-    this.api.treasuryTransferBalance(this.account, membershipTransactionFee)
-
-    // Balance assertion
-    await this.api
-      .getBalance(this.account)
-      .then((balance) =>
-        assert(
-          balance.toBn() < membershipFee.add(membershipTransactionFee),
-          'Account A already have sufficient balance to purchase membership'
-        )
-      )
 
-    // Buying memebership
-    await this.api.buyMembership(this.account, this.paidTerms, `late_member_${this.account.substring(0, 8)}`, true)
+    // Only provide enough funds for transaction fee but not enough to cover the membership fee
+    await this.api.treasuryTransferBalance(this.account, membershipTransactionFee)
 
-    // Assertions
-    this.api.getMemberIds(this.account).then((membership) => assert(membership.length === 0, 'Account A is a member'))
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const balance = await this.api.getBalance(this.account)
+
+    assert(
+      balance.toBn() < membershipFee.add(membershipTransactionFee),
+      'Account already has sufficient balance to purchase membership'
+    )
+
+    this.expectDispatchError(
+      await this.api.buyMembership(this.account, this.paidTerms, `late_member_${this.account.substring(0, 8)}`),
+      'Buying membership with insufficient funds should fail.'
+    )
   }
 }

+ 92 - 154
tests/network-tests/src/fixtures/proposalsModule.ts

@@ -2,15 +2,14 @@ import { Api, WorkingGroups } from '../Api'
 import { v4 as uuid } from 'uuid'
 import BN from 'bn.js'
 import { ProposalId } from '@joystream/types/proposals'
-import { Fixture } from '../Fixture'
+import { BaseFixture } from '../Fixture'
 import { assert } from 'chai'
 import { ApplicationId, OpeningId } from '@joystream/types/hiring'
 import { WorkerId } from '@joystream/types/working-group'
 import { Utils } from '../utils'
 import { EventRecord } from '@polkadot/types/interfaces'
 
-export class CreateWorkingGroupLeaderOpeningFixture implements Fixture {
-  private api: Api
+export class CreateWorkingGroupLeaderOpeningFixture extends BaseFixture {
   private proposer: string
   private applicationStake: BN
   private roleStake: BN
@@ -19,7 +18,7 @@ export class CreateWorkingGroupLeaderOpeningFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, applicationStake: BN, roleStake: BN, workingGroup: string) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.applicationStake = applicationStake
     this.roleStake = roleStake
@@ -30,7 +29,7 @@ export class CreateWorkingGroupLeaderOpeningFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing working group lead opening proposal ' + uuid().substring(0, 8)
@@ -68,16 +67,11 @@ export class CreateWorkingGroupLeaderOpeningFixture implements Fixture {
       workingGroup: this.workingGroup,
     })
 
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class BeginWorkingGroupLeaderApplicationReviewFixture implements Fixture {
-  private api: Api
+export class BeginWorkingGroupLeaderApplicationReviewFixture extends BaseFixture {
   private proposer: string
   private openingId: OpeningId
   private workingGroup: string
@@ -85,7 +79,7 @@ export class BeginWorkingGroupLeaderApplicationReviewFixture implements Fixture
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, openingId: OpeningId, workingGroup: string) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.openingId = openingId
     this.workingGroup = workingGroup
@@ -95,7 +89,7 @@ export class BeginWorkingGroupLeaderApplicationReviewFixture implements Fixture
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing begin working group lead application review proposal ' + uuid().substring(0, 8)
@@ -114,16 +108,12 @@ export class BeginWorkingGroupLeaderApplicationReviewFixture implements Fixture
       this.openingId,
       this.workingGroup
     )
-    this.result = this.api.expectProposalCreatedEvent(result.events)
 
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class FillLeaderOpeningProposalFixture implements Fixture {
-  private api: Api
+export class FillLeaderOpeningProposalFixture extends BaseFixture {
   private proposer: string
   private applicationId: ApplicationId
   private firstRewardInterval: BN
@@ -144,7 +134,7 @@ export class FillLeaderOpeningProposalFixture implements Fixture {
     openingId: OpeningId,
     workingGroup: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.applicationId = applicationId
     this.firstRewardInterval = firstRewardInterval
@@ -158,7 +148,7 @@ export class FillLeaderOpeningProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing fill opening proposal ' + uuid().substring(0, 8)
@@ -185,16 +175,11 @@ export class FillLeaderOpeningProposalFixture implements Fixture {
       workingGroup: workingGroupString,
     })
 
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class TerminateLeaderRoleProposalFixture implements Fixture {
-  private api: Api
+export class TerminateLeaderRoleProposalFixture extends BaseFixture {
   private proposer: string
   private slash: boolean
   private workingGroup: WorkingGroups
@@ -202,7 +187,7 @@ export class TerminateLeaderRoleProposalFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, slash: boolean, workingGroup: WorkingGroups) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.slash = slash
     this.workingGroup = workingGroup
@@ -212,7 +197,7 @@ export class TerminateLeaderRoleProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing begin working group lead application review proposal ' + uuid().substring(0, 8)
@@ -237,16 +222,11 @@ export class TerminateLeaderRoleProposalFixture implements Fixture {
       this.slash,
       workingGroupString
     )
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class SetLeaderRewardProposalFixture implements Fixture {
-  private api: Api
+export class SetLeaderRewardProposalFixture extends BaseFixture {
   private proposer: string
   private payoutAmount: BN
   private workingGroup: WorkingGroups
@@ -254,7 +234,7 @@ export class SetLeaderRewardProposalFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, payoutAmount: BN, workingGroup: WorkingGroups) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.payoutAmount = payoutAmount
     this.workingGroup = workingGroup
@@ -264,7 +244,7 @@ export class SetLeaderRewardProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing set leader reward proposal ' + uuid().substring(0, 8)
@@ -288,16 +268,11 @@ export class SetLeaderRewardProposalFixture implements Fixture {
       workingGroupString
     )
 
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class DecreaseLeaderStakeProposalFixture implements Fixture {
-  private api: Api
+export class DecreaseLeaderStakeProposalFixture extends BaseFixture {
   private proposer: string
   private stakeDecrement: BN
   private workingGroup: WorkingGroups
@@ -305,7 +280,7 @@ export class DecreaseLeaderStakeProposalFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, stakeDecrement: BN, workingGroup: WorkingGroups) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.stakeDecrement = stakeDecrement
     this.workingGroup = workingGroup
@@ -315,7 +290,7 @@ export class DecreaseLeaderStakeProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing decrease leader stake proposal ' + uuid().substring(0, 8)
@@ -339,16 +314,11 @@ export class DecreaseLeaderStakeProposalFixture implements Fixture {
       workingGroupString
     )
 
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class SlashLeaderProposalFixture implements Fixture {
-  private api: Api
+export class SlashLeaderProposalFixture extends BaseFixture {
   private proposer: string
   private slashAmount: BN
   private workingGroup: WorkingGroups
@@ -356,7 +326,7 @@ export class SlashLeaderProposalFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, slashAmount: BN, workingGroup: WorkingGroups) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.slashAmount = slashAmount
     this.workingGroup = workingGroup
@@ -366,7 +336,7 @@ export class SlashLeaderProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing slash leader stake proposal ' + uuid().substring(0, 8)
@@ -388,15 +358,11 @@ export class SlashLeaderProposalFixture implements Fixture {
       this.slashAmount,
       workingGroupString
     )
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class WorkingGroupMintCapacityProposalFixture implements Fixture {
-  private api: Api
+export class WorkingGroupMintCapacityProposalFixture extends BaseFixture {
   private proposer: string
   private mintCapacity: BN
   private workingGroup: WorkingGroups
@@ -404,7 +370,7 @@ export class WorkingGroupMintCapacityProposalFixture implements Fixture {
   private result: ProposalId | undefined
 
   constructor(api: Api, proposer: string, mintCapacity: BN, workingGroup: WorkingGroups) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.mintCapacity = mintCapacity
     this.workingGroup = workingGroup
@@ -414,7 +380,7 @@ export class WorkingGroupMintCapacityProposalFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing working group mint capacity proposal ' + uuid().substring(0, 8)
@@ -434,32 +400,23 @@ export class WorkingGroupMintCapacityProposalFixture implements Fixture {
       this.mintCapacity,
       workingGroupString
     )
-    this.result = this.api.expectProposalCreatedEvent(result.events)
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    this.result = this.api.findProposalCreatedEvent(result.events)
   }
 }
 
-export class ElectionParametersProposalFixture implements Fixture {
-  private api: Api
+export class ElectionParametersProposalFixture extends BaseFixture {
   private proposerAccount: string
 
   constructor(api: Api, proposerAccount: string) {
-    this.api = api
+    super(api)
     this.proposerAccount = proposerAccount
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8)
 
-    // Council accounts enough balance to ensure they can vote
-    const councilAccounts = await this.api.getCouncilAccounts()
-    const runtimeVoteFee: BN = this.api.estimateVoteForProposalFee()
-    this.api.treasuryTransferBalanceToAccounts(councilAccounts, runtimeVoteFee)
-
     const announcingPeriod: BN = new BN(28800)
     const votingPeriod: BN = new BN(14400)
     const revealingPeriod: BN = new BN(14400)
@@ -512,11 +469,12 @@ export class ElectionParametersProposalFixture implements Fixture {
       proposedMinCouncilStake,
       proposedMinVotingStake
     )
-    const proposalNumber = this.api.expectProposalCreatedEvent(proposalCreationResult.events)
+    const proposalNumber = this.api.findProposalCreatedEvent(proposalCreationResult.events) as ProposalId
+    assert.notEqual(proposalNumber, undefined)
 
-    // Approving the proposal
-    this.api.batchApproveProposal(proposalNumber)
-    await this.api.waitForProposalToFinalize(proposalNumber)
+    const approveProposalFixture = new VoteForProposalFixture(this.api, proposalNumber)
+    await approveProposalFixture.execute()
+    assert(approveProposalFixture.proposalExecuted)
 
     // Assertions
     const newAnnouncingPeriod: BN = await this.api.getAnnouncingPeriod()
@@ -559,29 +517,24 @@ export class ElectionParametersProposalFixture implements Fixture {
       proposedMinVotingStake.eq(newMinVotingStake),
       `Min voting stake has unexpected value ${newMinVotingStake}, expected ${proposedMinVotingStake}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class SpendingProposalFixture implements Fixture {
-  private api: Api
+export class SpendingProposalFixture extends BaseFixture {
   private proposer: string
   private spendingBalance: BN
   private mintCapacity: BN
 
   constructor(api: Api, proposer: string, spendingBalance: BN, mintCapacity: BN) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.spendingBalance = spendingBalance
     this.mintCapacity = mintCapacity
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const description = 'spending proposal which is used for API network testing with some mock data'
-    const runtimeVoteFee: BN = this.api.estimateVoteForProposalFee()
 
     // Topping the balances
     const proposalStake: BN = new BN(25000)
@@ -593,8 +546,7 @@ export class SpendingProposalFixture implements Fixture {
       this.proposer
     )
     this.api.treasuryTransferBalance(this.proposer, runtimeProposalFee.add(proposalStake))
-    const councilAccounts = await this.api.getCouncilAccounts()
-    this.api.treasuryTransferBalanceToAccounts(councilAccounts, runtimeVoteFee)
+
     await this.api.sudoSetCouncilMintCapacity(this.mintCapacity)
 
     const fundingRecipient = this.api.createKeyPairs(1)[0].address
@@ -608,12 +560,15 @@ export class SpendingProposalFixture implements Fixture {
       this.spendingBalance,
       fundingRecipient
     )
-    const proposalNumber: ProposalId = this.api.expectProposalCreatedEvent(result.events)
+    const proposalNumber: ProposalId = this.api.findProposalCreatedEvent(result.events) as ProposalId
+    assert.notEqual(proposalNumber, undefined)
 
     // Approving spending proposal
     const balanceBeforeMinting: BN = await this.api.getBalance(fundingRecipient)
-    this.api.batchApproveProposal(proposalNumber)
-    await this.api.waitForProposalToFinalize(proposalNumber)
+
+    const approveProposalFixture = new VoteForProposalFixture(this.api, proposalNumber)
+    await approveProposalFixture.execute()
+    assert(approveProposalFixture.proposalExecuted)
 
     const balanceAfterMinting: BN = await this.api.getBalance(fundingRecipient)
     assert(
@@ -622,29 +577,22 @@ export class SpendingProposalFixture implements Fixture {
         this.spendingBalance
       )}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class TextProposalFixture implements Fixture {
-  private api: Api
+export class TextProposalFixture extends BaseFixture {
   private proposer: string
 
   constructor(api: Api, proposer: string) {
-    this.api = api
+    super(api)
     this.proposer = proposer
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing text proposal ' + uuid().substring(0, 8)
     const proposalText: string = 'Text of the testing proposal ' + uuid().substring(0, 8)
-    const runtimeVoteFee: BN = this.api.estimateVoteForProposalFee()
-    const councilAccounts = await this.api.getCouncilAccounts()
-    this.api.treasuryTransferBalanceToAccounts(councilAccounts, runtimeVoteFee)
 
     // Proposal stake calculation
     const proposalStake: BN = new BN(25000)
@@ -657,38 +605,31 @@ export class TextProposalFixture implements Fixture {
     this.api.treasuryTransferBalance(this.proposer, runtimeProposalFee.add(proposalStake))
 
     // Proposal creation
-
     const result = await this.api.proposeText(this.proposer, proposalStake, proposalTitle, description, proposalText)
-    const proposalNumber: ProposalId = this.api.expectProposalCreatedEvent(result.events)
+    const proposalNumber = this.api.findProposalCreatedEvent(result.events) as ProposalId
+    assert.notEqual(proposalNumber, undefined)
 
     // Approving text proposal
-    this.api.batchApproveProposal(proposalNumber)
-    await this.api.waitForProposalToFinalize(proposalNumber)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const approveProposalFixture = new VoteForProposalFixture(this.api, proposalNumber)
+    await approveProposalFixture.execute()
+    assert(approveProposalFixture.proposalExecuted)
   }
 }
 
-export class ValidatorCountProposalFixture implements Fixture {
-  private api: Api
+export class ValidatorCountProposalFixture extends BaseFixture {
   private proposer: string
   private validatorCountIncrement: BN
 
   constructor(api: Api, proposer: string, validatorCountIncrement: BN) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.validatorCountIncrement = validatorCountIncrement
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
     const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8)
-    const runtimeVoteFee: BN = this.api.estimateVoteForProposalFee()
-    const councilAccounts = await this.api.getCouncilAccounts()
-    this.api.treasuryTransferBalanceToAccounts(councilAccounts, runtimeVoteFee)
 
     // Proposal stake calculation
     const proposalStake: BN = new BN(100000)
@@ -705,39 +646,36 @@ export class ValidatorCountProposalFixture implements Fixture {
       proposalStake,
       proposedValidatorCount
     )
-    const proposalNumber: ProposalId = this.api.expectProposalCreatedEvent(result.events)
+    const proposalNumber: ProposalId = this.api.findProposalCreatedEvent(result.events) as ProposalId
+    assert.notEqual(proposalNumber, undefined)
 
     // Approving the proposal
-    this.api.batchApproveProposal(proposalNumber)
-    await this.api.waitForProposalToFinalize(proposalNumber)
+    const approveProposalFixture = new VoteForProposalFixture(this.api, proposalNumber)
+    await approveProposalFixture.execute()
+    assert(approveProposalFixture.proposalExecuted)
 
     const newValidatorCount: BN = await this.api.getValidatorCount()
     assert(
       proposedValidatorCount.eq(newValidatorCount),
       `Validator count has unexpeccted value ${newValidatorCount}, expected ${proposedValidatorCount}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class UpdateRuntimeFixture implements Fixture {
-  private api: Api
+export class UpdateRuntimeFixture extends BaseFixture {
   private proposer: string
   private runtimePath: string
 
   constructor(api: Api, proposer: string, runtimePath: string) {
-    this.api = api
+    super(api)
     this.proposer = proposer
     this.runtimePath = runtimePath
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Setup
     const runtime: string = Utils.readRuntimeFromFile(this.runtimePath)
     const description = 'runtime upgrade proposal which is used for API network testing'
-    const runtimeVoteFee: BN = this.api.estimateVoteForProposalFee()
 
     // Topping the balances
     const proposalStake: BN = new BN(1000000)
@@ -748,8 +686,6 @@ export class UpdateRuntimeFixture implements Fixture {
       runtime
     )
     this.api.treasuryTransferBalance(this.proposer, runtimeProposalFee.add(proposalStake))
-    const councilAccounts = await this.api.getCouncilAccounts()
-    this.api.treasuryTransferBalanceToAccounts(councilAccounts, runtimeVoteFee)
 
     // Proposal creation
     const result = await this.api.proposeRuntime(
@@ -759,43 +695,45 @@ export class UpdateRuntimeFixture implements Fixture {
       'runtime to test proposal functionality' + uuid().substring(0, 8),
       runtime
     )
-    const proposalNumber: ProposalId = this.api.expectProposalCreatedEvent(result.events)
+    const proposalNumber: ProposalId = this.api.findProposalCreatedEvent(result.events) as ProposalId
+    assert.notEqual(proposalNumber, undefined)
 
     // Approving runtime update proposal
-    this.api.batchApproveProposal(proposalNumber)
-    await this.api.waitForProposalToFinalize(proposalNumber)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const approveProposalFixture = new VoteForProposalFixture(this.api, proposalNumber)
+    await approveProposalFixture.execute()
+    assert(approveProposalFixture.proposalExecuted)
   }
 }
 
-export class VoteForProposalFixture implements Fixture {
-  private api: Api
+export class VoteForProposalFixture extends BaseFixture {
   private proposalNumber: ProposalId
-  private events: EventRecord[] = []
+  private _proposalExecuted = false
+  private _events: EventRecord[] = []
 
   constructor(api: Api, proposalNumber: ProposalId) {
-    this.api = api
+    super(api)
     this.proposalNumber = proposalNumber
   }
 
-  public getEvents(): EventRecord[] {
-    return this.events
+  get proposalExecuted(): boolean {
+    return this._proposalExecuted
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  get events(): EventRecord[] {
+    return this._events
+  }
+
+  public async execute(): Promise<void> {
     const proposalVoteFee: BN = this.api.estimateVoteForProposalFee()
     const councilAccounts = await this.api.getCouncilAccounts()
     this.api.treasuryTransferBalanceToAccounts(councilAccounts, proposalVoteFee)
 
     // Approving the proposal
-    this.api.batchApproveProposal(this.proposalNumber)
-    this.events = await this.api.waitForProposalToFinalize(this.proposalNumber)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const onProposalFinalized = this.api.waitForProposalToFinalize(this.proposalNumber)
+    const approvals = await this.api.batchApproveProposal(this.proposalNumber)
+    approvals.map((result) => this.expectDispatchSuccess(result, 'Proposal Approval Vote Expected To Be Successful'))
+    const proposalOutcome = await onProposalFinalized
+    this._proposalExecuted = proposalOutcome[0]
+    this._events = proposalOutcome[1]
   }
 }

+ 9 - 10
tests/network-tests/src/fixtures/sudoHireLead.ts

@@ -1,4 +1,4 @@
-import { Fixture } from '../Fixture'
+import { BaseFixture } from '../Fixture'
 import {
   SudoAddLeaderOpeningFixture,
   ApplyForOpeningFixture,
@@ -12,8 +12,7 @@ import { PaidTermId } from '@joystream/types/members'
 import BN from 'bn.js'
 import { assert } from 'chai'
 
-export class SudoHireLeadFixture implements Fixture {
-  private api: Api
+export class SudoHireLeadFixture extends BaseFixture {
   private leadAccount: string
   private paidTerms: PaidTermId
   private applicationStake: BN
@@ -36,7 +35,7 @@ export class SudoHireLeadFixture implements Fixture {
     payoutAmount: BN,
     workingGroup: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.leadAccount = leadAccount
     this.paidTerms = paidTerms
     this.applicationStake = applicationStake
@@ -48,14 +47,14 @@ export class SudoHireLeadFixture implements Fixture {
     this.workingGroup = workingGroup
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
       this.api,
       [this.leadAccount],
       this.paidTerms
     )
     // Buying membership for leader account
-    await leaderHappyCaseFixture.runner(false)
+    await leaderHappyCaseFixture.runner()
 
     const addLeaderOpeningFixture: SudoAddLeaderOpeningFixture = new SudoAddLeaderOpeningFixture(
       this.api,
@@ -65,7 +64,7 @@ export class SudoHireLeadFixture implements Fixture {
       this.workingGroup
     )
     // Add lead opening
-    await addLeaderOpeningFixture.runner(false)
+    await addLeaderOpeningFixture.runner()
 
     const applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
       this.api,
@@ -75,7 +74,7 @@ export class SudoHireLeadFixture implements Fixture {
       addLeaderOpeningFixture.getCreatedOpeningId() as OpeningId,
       this.workingGroup
     )
-    await applyForLeaderOpeningFixture.runner(false)
+    await applyForLeaderOpeningFixture.runner()
 
     assert(applyForLeaderOpeningFixture.getApplicationIds().length === 1)
 
@@ -84,7 +83,7 @@ export class SudoHireLeadFixture implements Fixture {
       addLeaderOpeningFixture.getCreatedOpeningId() as OpeningId,
       this.workingGroup
     )
-    await beginLeaderApplicationReviewFixture.runner(false)
+    await beginLeaderApplicationReviewFixture.runner()
 
     const fillLeaderOpeningFixture = new SudoFillLeaderOpeningFixture(
       this.api,
@@ -95,6 +94,6 @@ export class SudoHireLeadFixture implements Fixture {
       this.payoutAmount,
       this.workingGroup
     )
-    await fillLeaderOpeningFixture.runner(false)
+    await fillLeaderOpeningFixture.runner()
   }
 }

+ 95 - 143
tests/network-tests/src/fixtures/workingGroupModule.ts

@@ -7,10 +7,9 @@ import { RewardRelationship } from '@joystream/types/recurring-rewards'
 import { Application, ApplicationIdToWorkerIdMap, Worker, WorkerId } from '@joystream/types/working-group'
 import { Utils } from '../utils'
 import { ApplicationId, Opening as HiringOpening, OpeningId } from '@joystream/types/hiring'
-import { Fixture } from '../Fixture'
+import { BaseFixture } from '../Fixture'
 
-export class AddWorkerOpeningFixture implements Fixture {
-  private api: Api
+export class AddWorkerOpeningFixture extends BaseFixture {
   private applicationStake: BN
   private roleStake: BN
   private activationDelay: BN
@@ -31,7 +30,7 @@ export class AddWorkerOpeningFixture implements Fixture {
     unstakingPeriod: BN,
     module: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.applicationStake = applicationStake
     this.roleStake = roleStake
     this.activationDelay = activationDelay
@@ -39,7 +38,7 @@ export class AddWorkerOpeningFixture implements Fixture {
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -73,18 +72,15 @@ export class AddWorkerOpeningFixture implements Fixture {
         text: uuid().substring(0, 8),
         type: 'Worker',
       },
-      this.module,
-      expectFailure
+      this.module
     )
 
-    if (!expectFailure) {
-      this.result = this.api.expectOpeningAddedEvent(result.events)
-    }
+    // We don't assert, we allow potential failure
+    this.result = this.api.findOpeningAddedEvent(result.events, this.module)
   }
 }
 
-export class SudoAddLeaderOpeningFixture implements Fixture {
-  private api: Api
+export class SudoAddLeaderOpeningFixture extends BaseFixture {
   private applicationStake: BN
   private roleStake: BN
   private activationDelay: BN
@@ -97,14 +93,14 @@ export class SudoAddLeaderOpeningFixture implements Fixture {
   }
 
   public constructor(api: Api, applicationStake: BN, roleStake: BN, activationDelay: BN, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.applicationStake = applicationStake
     this.roleStake = roleStake
     this.activationDelay = activationDelay
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const result = await this.api.sudoAddOpening(
       {
         activationDelay: this.activationDelay,
@@ -131,26 +127,22 @@ export class SudoAddLeaderOpeningFixture implements Fixture {
       this.module
     )
 
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    } else {
-      this.result = this.api.expectOpeningAddedEvent(result.events)
-    }
+    // We don't assert, we allow potential failure
+    this.result = this.api.findOpeningAddedEvent(result.events, this.module)
   }
 }
 
-export class AcceptApplicationsFixture implements Fixture {
-  private api: Api
+export class AcceptApplicationsFixture extends BaseFixture {
   private openingId: OpeningId
   private module: WorkingGroups
 
   public constructor(api: Api, openingId: OpeningId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.openingId = openingId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -165,14 +157,10 @@ export class AcceptApplicationsFixture implements Fixture {
     const wgOpening = await this.api.getWorkingGroupOpening(this.openingId, this.module)
     const opening: HiringOpening = await this.api.getHiringOpening(wgOpening.hiring_opening_id)
     assert(opening.is_active, `${this.module} Opening ${this.openingId} is not active`)
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class ApplyForOpeningFixture implements Fixture {
-  private api: Api
+export class ApplyForOpeningFixture extends BaseFixture {
   private applicants: string[]
   private applicationStake: BN
   private roleStake: BN
@@ -188,7 +176,7 @@ export class ApplyForOpeningFixture implements Fixture {
     openingId: OpeningId,
     module: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.applicants = applicants
     this.applicationStake = applicationStake
     this.roleStake = roleStake
@@ -200,7 +188,7 @@ export class ApplyForOpeningFixture implements Fixture {
     return this.result
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Fee estimation and transfer
     const applyOnOpeningFee: BN = this.api
       .estimateApplyOnOpeningFee(this.applicants[0], this.module)
@@ -215,8 +203,7 @@ export class ApplyForOpeningFixture implements Fixture {
       this.roleStake,
       this.applicationStake,
       uuid().substring(0, 8),
-      this.module,
-      expectFailure
+      this.module
     )
 
     const applicationIds = results.map(({ events }) => {
@@ -233,18 +220,17 @@ export class ApplyForOpeningFixture implements Fixture {
   }
 }
 
-export class WithdrawApplicationFixture implements Fixture {
-  private api: Api
+export class WithdrawApplicationFixture extends BaseFixture {
   private applicationIds: ApplicationId[]
   private module: WorkingGroups
 
   constructor(api: Api, applicationIds: ApplicationId[], module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.applicationIds = applicationIds
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Fee estimation and transfer
     const withdrawApplicaitonFee: BN = this.api.estimateWithdrawApplicationFee(this.module)
 
@@ -253,26 +239,24 @@ export class WithdrawApplicationFixture implements Fixture {
     this.api.treasuryTransferBalanceToAccounts(roleAccounts, withdrawApplicaitonFee)
 
     // Application withdrawal
-    await this.api.batchWithdrawActiveApplications(this.applicationIds, this.module)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const withdrawls = await this.api.batchWithdrawActiveApplications(this.applicationIds, this.module)
+    withdrawls.forEach((withdrawl) =>
+      this.expectDispatchSuccess(withdrawl, 'Application withdrawl should have succeedeed')
+    )
   }
 }
 
-export class BeginApplicationReviewFixture implements Fixture {
-  private api: Api
+export class BeginApplicationReviewFixture extends BaseFixture {
   private openingId: OpeningId
   private module: WorkingGroups
 
   constructor(api: Api, openingId: OpeningId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.openingId = openingId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -286,36 +270,27 @@ export class BeginApplicationReviewFixture implements Fixture {
     // const beginApplicantReviewPromise: Promise<ApplicationId> = this.api.expectApplicationReviewBegan()
     const result = await this.api.beginApplicantReview(leadAccount, this.openingId, this.module)
 
-    this.api.expectApplicationReviewBeganEvent(result.events)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    assert.notEqual(this.api.findApplicationReviewBeganEvent(result.events, this.module), undefined)
   }
 }
 
-export class SudoBeginLeaderApplicationReviewFixture implements Fixture {
-  private api: Api
+export class SudoBeginLeaderApplicationReviewFixture extends BaseFixture {
   private openingId: OpeningId
   private module: WorkingGroups
 
   constructor(api: Api, openingId: OpeningId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.openingId = openingId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Begin application review
     await this.api.sudoBeginApplicantReview(this.openingId, this.module)
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class FillOpeningFixture implements Fixture {
-  private api: Api
+export class FillOpeningFixture extends BaseFixture {
   private applicationIds: ApplicationId[]
   private openingId: OpeningId
   private firstPayoutInterval: BN
@@ -333,7 +308,7 @@ export class FillOpeningFixture implements Fixture {
     amountPerPayout: BN,
     module: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.applicationIds = applicationIds
     this.openingId = openingId
     this.firstPayoutInterval = firstPayoutInterval
@@ -346,7 +321,7 @@ export class FillOpeningFixture implements Fixture {
     return this.workerIds
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -377,7 +352,12 @@ export class FillOpeningFixture implements Fixture {
       this.payoutInterval,
       this.module
     )
-    const applicationIdToWorkerIdMap: ApplicationIdToWorkerIdMap = this.api.expectOpeningFilledEvent(result.events)
+    const applicationIdToWorkerIdMap = this.api.findOpeningFilledEvent(
+      result.events,
+      this.module
+    ) as ApplicationIdToWorkerIdMap
+    assert.notEqual(applicationIdToWorkerIdMap, undefined)
+
     this.workerIds = []
     applicationIdToWorkerIdMap.forEach((workerId) => this.workerIds.push(workerId))
 
@@ -390,15 +370,10 @@ export class FillOpeningFixture implements Fixture {
         `Role account ids does not match, worker account: ${worker.role_account_id}, application account ${application.role_account_id}`
       )
     })
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class SudoFillLeaderOpeningFixture implements Fixture {
-  private api: Api
+export class SudoFillLeaderOpeningFixture extends BaseFixture {
   private applicationId: ApplicationId
   private openingId: OpeningId
   private firstPayoutInterval: BN
@@ -415,7 +390,7 @@ export class SudoFillLeaderOpeningFixture implements Fixture {
     amountPerPayout: BN,
     module: WorkingGroups
   ) {
-    this.api = api
+    super(api)
     this.applicationId = applicationId
     this.openingId = openingId
     this.firstPayoutInterval = firstPayoutInterval
@@ -424,7 +399,7 @@ export class SudoFillLeaderOpeningFixture implements Fixture {
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Fill leader opening
     const now: BN = await this.api.getBestBlock()
     const result = await this.api.sudoFillOpening(
@@ -437,12 +412,18 @@ export class SudoFillLeaderOpeningFixture implements Fixture {
     )
 
     // Assertions
-    const applicationIdToWorkerIdMap = this.api.expectOpeningFilledEvent(result.events)
-    assert(applicationIdToWorkerIdMap.size === 1)
+    const applicationIdToWorkerIdMap = this.api.findOpeningFilledEvent(
+      result.events,
+      this.module
+    ) as ApplicationIdToWorkerIdMap
+    assert.notEqual(applicationIdToWorkerIdMap, undefined)
+    assert.equal(applicationIdToWorkerIdMap.size, 1)
+
     applicationIdToWorkerIdMap.forEach(async (workerId, applicationId) => {
       const worker: Worker = await this.api.getWorkerById(workerId, this.module)
       const application: Application = await this.api.getApplicationById(applicationId, this.module)
-      const leadWorkerId: WorkerId = (await this.api.getLeadWorkerId(this.module))!
+      const leadWorkerId = (await this.api.getLeadWorkerId(this.module)) as WorkerId
+      assert.notEqual(leadWorkerId, undefined)
       assert(
         worker.role_account_id.toString() === application.role_account_id.toString(),
         `Role account ids does not match, leader account: ${worker.role_account_id}, application account ${application.role_account_id}`
@@ -452,25 +433,20 @@ export class SudoFillLeaderOpeningFixture implements Fixture {
         `Role account ids does not match, leader account: ${worker.role_account_id}, application account ${application.role_account_id}`
       )
     })
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class IncreaseStakeFixture implements Fixture {
-  private api: Api
+export class IncreaseStakeFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     // Fee estimation and transfer
     const increaseStakeFee: BN = this.api.estimateIncreaseStakeFee(this.module)
     const stakeIncrement: BN = new BN(1)
@@ -488,24 +464,20 @@ export class IncreaseStakeFixture implements Fixture {
       increasedWorkerStake.eq(newWorkerStake),
       `Unexpected worker stake ${newWorkerStake}, expected ${increasedWorkerStake}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class UpdateRewardAccountFixture implements Fixture {
-  public api: Api
-  public workerId: WorkerId
-  public module: WorkingGroups
+export class UpdateRewardAccountFixture extends BaseFixture {
+  private workerId: WorkerId
+  private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const worker = await this.api.getWorkerById(this.workerId, this.module)
     const workerRoleAccount = worker.role_account_id.toString()
     // Fee estimation and transfer
@@ -520,24 +492,20 @@ export class UpdateRewardAccountFixture implements Fixture {
       newRewardAccount === createdAccount.address,
       `Unexpected role account ${newRewardAccount}, expected ${createdAccount.address}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class UpdateRoleAccountFixture implements Fixture {
-  private api: Api
+export class UpdateRoleAccountFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const worker = await this.api.getWorkerById(this.workerId, this.module)
     const workerRoleAccount = worker.role_account_id.toString()
     // Fee estimation and transfer
@@ -553,25 +521,20 @@ export class UpdateRoleAccountFixture implements Fixture {
       newRoleAccount === createdAccount.address,
       `Unexpected role account ${newRoleAccount}, expected ${createdAccount.address}`
     )
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }
 
-export class TerminateApplicationsFixture implements Fixture {
-  private api: Api
+export class TerminateApplicationsFixture extends BaseFixture {
   private applicationIds: ApplicationId[]
   private module: WorkingGroups
 
   constructor(api: Api, applicationIds: ApplicationId[], module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.applicationIds = applicationIds
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -583,26 +546,24 @@ export class TerminateApplicationsFixture implements Fixture {
     this.api.treasuryTransferBalance(leadAccount, terminateApplicationFee.muln(this.applicationIds.length))
 
     // Terminate worker applications
-    await this.api.batchTerminateApplication(leadAccount, this.applicationIds, this.module)
-
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
+    const terminations = await this.api.batchTerminateApplication(leadAccount, this.applicationIds, this.module)
+    terminations.forEach((termination) =>
+      this.expectDispatchSuccess(termination, 'Application should have been terminated')
+    )
   }
 }
 
-export class DecreaseStakeFixture implements Fixture {
-  private api: Api
+export class DecreaseStakeFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -618,31 +579,28 @@ export class DecreaseStakeFixture implements Fixture {
     const decreasedWorkerStake: BN = (await this.api.getWorkerStakeAmount(this.workerId, this.module)).sub(
       workerStakeDecrement
     )
-    await this.api.decreaseStake(leadAccount, this.workerId, workerStakeDecrement, this.module, expectFailure)
+    await this.api.decreaseStake(leadAccount, this.workerId, workerStakeDecrement, this.module)
     const newWorkerStake: BN = await this.api.getWorkerStakeAmount(this.workerId, this.module)
 
     // Assertions
-    if (!expectFailure) {
-      assert(
-        decreasedWorkerStake.eq(newWorkerStake),
-        `Unexpected worker stake ${newWorkerStake}, expected ${decreasedWorkerStake}`
-      )
-    }
+    assert(
+      decreasedWorkerStake.eq(newWorkerStake),
+      `Unexpected worker stake ${newWorkerStake}, expected ${decreasedWorkerStake}`
+    )
   }
 }
 
-export class SlashFixture implements Fixture {
-  private api: Api
+export class SlashFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -656,7 +614,7 @@ export class SlashFixture implements Fixture {
 
     // Slash worker
     const slashedStake: BN = (await this.api.getWorkerStakeAmount(this.workerId, this.module)).sub(slashAmount)
-    await this.api.slashStake(leadAccount, this.workerId, slashAmount, this.module, expectFailure)
+    await this.api.slashStake(leadAccount, this.workerId, slashAmount, this.module)
     const newStake: BN = await this.api.getWorkerStakeAmount(this.workerId, this.module)
 
     // Assertions
@@ -664,18 +622,17 @@ export class SlashFixture implements Fixture {
   }
 }
 
-export class TerminateRoleFixture implements Fixture {
-  private api: Api
+export class TerminateRoleFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const lead = await this.api.getGroupLead(this.module)
     if (!lead) {
       throw new Error('No Lead')
@@ -687,7 +644,7 @@ export class TerminateRoleFixture implements Fixture {
     this.api.treasuryTransferBalance(leadAccount, terminateRoleFee)
 
     // Terminate worker role
-    await this.api.terminateRole(leadAccount, this.workerId, uuid().substring(0, 8), this.module, expectFailure)
+    await this.api.terminateRole(leadAccount, this.workerId, uuid().substring(0, 8), this.module)
 
     // Assertions
     const isWorker: boolean = await this.api.isWorker(this.workerId, this.module)
@@ -695,24 +652,23 @@ export class TerminateRoleFixture implements Fixture {
   }
 }
 
-export class LeaveRoleFixture implements Fixture {
-  private api: Api
+export class LeaveRoleFixture extends BaseFixture {
   private workerIds: WorkerId[]
   private module: WorkingGroups
 
   constructor(api: Api, workerIds: WorkerId[], module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerIds = workerIds
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const roleAccounts = await this.api.getWorkerRoleAccounts(this.workerIds, this.module)
     // Fee estimation and transfer
     const leaveRoleFee: BN = this.api.estimateLeaveRoleFee(this.module)
     this.api.treasuryTransferBalanceToAccounts(roleAccounts, leaveRoleFee)
 
-    await this.api.batchLeaveRole(this.workerIds, uuid().substring(0, 8), expectFailure, this.module)
+    await this.api.batchLeaveRole(this.workerIds, uuid().substring(0, 8), this.module)
 
     // Assertions
     this.workerIds.forEach(async (workerId) => {
@@ -722,18 +678,17 @@ export class LeaveRoleFixture implements Fixture {
   }
 }
 
-export class AwaitPayoutFixture implements Fixture {
-  private api: Api
+export class AwaitPayoutFixture extends BaseFixture {
   private workerId: WorkerId
   private module: WorkingGroups
 
   constructor(api: Api, workerId: WorkerId, module: WorkingGroups) {
-    this.api = api
+    super(api)
     this.workerId = workerId
     this.module = module
   }
 
-  public async runner(expectFailure: boolean): Promise<void> {
+  public async execute(): Promise<void> {
     const worker: Worker = await this.api.getWorkerById(this.workerId, this.module)
     const reward: RewardRelationship = await this.api.getRewardRelationship(worker.reward_relationship.unwrap())
     const now: BN = await this.api.getBestBlock()
@@ -763,8 +718,5 @@ export class AwaitPayoutFixture implements Fixture {
       balanceAfterSecondPayout.eq(expectedBalanceSecond),
       `Unexpected balance, expected ${expectedBalanceSecond} got ${balanceAfterSecondPayout}`
     )
-    if (expectFailure) {
-      throw new Error('Successful fixture run while expecting failure')
-    }
   }
 }

+ 7 - 4
tests/network-tests/src/flows/contentDirectory/contentDirectoryInitialization.ts

@@ -1,6 +1,9 @@
-import { Api } from '../../Api'
-import { KeyringPair } from '@polkadot/keyring/types'
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+const debug = Debugger('initializeContentDirectory')
 
-export default async function initializeContentDirectory(api: Api, leadKeyPair: KeyringPair) {
-  await api.initializeContentDirectory(leadKeyPair)
+export default async function initializeContentDirectory({ api }: FlowProps): Promise<void> {
+  debug('Started')
+  await api.initializeContentDirectory()
+  debug('Done')
 }

+ 13 - 5
tests/network-tests/src/flows/contentDirectory/creatingChannel.ts

@@ -1,10 +1,13 @@
-import { QueryNodeApi } from '../../Api'
+import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { Utils } from '../../utils'
 import { CreateChannelFixture } from '../../fixtures/contentDirectoryModule'
 import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
 
-export function createSimpleChannelFixture(api: QueryNodeApi): CreateChannelFixture {
+export function createSimpleChannelFixture(api: Api): CreateChannelFixture {
   const channelEntity: ChannelEntity = {
     handle: 'New channel example',
     description: 'This is an example channel',
@@ -27,16 +30,21 @@ function assertChannelMatchQueriedResult(queriedChannel: any, channel: ChannelEn
   assert.equal(queriedChannel.isPublic, channel.isPublic, 'Should be equal')
 }
 
-export default async function channelCreation(api: QueryNodeApi) {
+export default async function channelCreation({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:creatingChannel')
+  debug('Started')
+
   const createChannelHappyCaseFixture = createSimpleChannelFixture(api)
 
-  await createChannelHappyCaseFixture.runner(false)
+  await new FixtureRunner(createChannelHappyCaseFixture).run()
 
   // Temporary solution (wait 2 minutes)
   await Utils.wait(120000)
 
   // Ensure newly created channel was parsed by query node
-  const result = await api.getChannelbyHandle(createChannelHappyCaseFixture.channelEntity.handle)
+  const result = await query.getChannelbyHandle(createChannelHappyCaseFixture.channelEntity.handle)
 
   assertChannelMatchQueriedResult(result.data.channels[0], createChannelHappyCaseFixture.channelEntity)
+
+  debug('Done')
 }

+ 21 - 13
tests/network-tests/src/flows/contentDirectory/creatingVideo.ts

@@ -1,10 +1,13 @@
-import { QueryNodeApi } from '../../Api'
+import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { CreateVideoFixture } from '../../fixtures/contentDirectoryModule'
 import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 import { assert } from 'chai'
 import { Utils } from '../../utils'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
 
-export function createVideoReferencingChannelFixture(api: QueryNodeApi, handle: string): CreateVideoFixture {
+export function createVideoReferencingChannelFixture(api: Api, handle: string): CreateVideoFixture {
   const videoEntity: VideoEntity = {
     title: 'Example video',
     description: 'This is an example video',
@@ -52,56 +55,61 @@ function assertVideoMatchQueriedResult(queriedVideo: any, video: VideoEntity) {
   assert.equal(queriedVideo.isPublic, video.isPublic, 'Should be equal')
 }
 
-export default async function createVideo(api: QueryNodeApi) {
+export default async function createVideo({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:creatingVideo')
+  debug('Started')
+
   const channelTitle = 'New channel example'
   const createVideoHappyCaseFixture = createVideoReferencingChannelFixture(api, channelTitle)
 
-  await createVideoHappyCaseFixture.runner(false)
+  await new FixtureRunner(createVideoHappyCaseFixture).run()
 
   // Temporary solution (wait 2 minutes)
   await Utils.wait(120000)
 
   // Perform number of full text searches on Channel title, that is a slight variation on title that one expects would return the video.
-  let channelFullTextSearchResult = await api.performFullTextSearchOnChannelTitle('video')
+  let channelFullTextSearchResult = await query.performFullTextSearchOnChannelTitle('video')
 
   assert(channelFullTextSearchResult.data.search.length === 1, 'Should contain exactly one entry')
 
   // Both channel and video title starts with `Example`
-  channelFullTextSearchResult = await api.performFullTextSearchOnChannelTitle('Example')
+  channelFullTextSearchResult = await query.performFullTextSearchOnChannelTitle('Example')
 
   assert(channelFullTextSearchResult.data.search.length === 2, 'Should contain two entries')
 
   // Perform number full text searches on Channel title, that absolutely should NOT return the video.
-  channelFullTextSearchResult = await api.performFullTextSearchOnChannelTitle('First')
+  channelFullTextSearchResult = await query.performFullTextSearchOnChannelTitle('First')
 
   assert(channelFullTextSearchResult.data.search.length === 0, 'Should be empty')
 
-  channelFullTextSearchResult = await api.performFullTextSearchOnChannelTitle('vid')
+  channelFullTextSearchResult = await query.performFullTextSearchOnChannelTitle('vid')
 
   assert(channelFullTextSearchResult.data.search.length === 0, 'Should be empty')
 
   // Ensure channel contains only one video with right data
-  const channelResult = await api.getChannelbyHandle(channelTitle)
+  const channelResult = await query.getChannelbyHandle(channelTitle)
 
   assert(channelResult.data.channels[0].videos.length === 1, 'Given channel should contain exactly one video')
 
   assertVideoMatchQueriedResult(channelResult.data.channels[0].videos[0], createVideoHappyCaseFixture.videoEntity)
 
   // Perform number of full text searches on Video title, that is a slight variation on title that one expects would return the video.
-  let videoFullTextSearchResult = await api.performFullTextSearchOnVideoTitle('Example')
+  let videoFullTextSearchResult = await query.performFullTextSearchOnVideoTitle('Example')
 
   assert(videoFullTextSearchResult.data.search.length === 2, 'Should contain two entries')
 
-  videoFullTextSearchResult = await api.performFullTextSearchOnVideoTitle('Example video')
+  videoFullTextSearchResult = await query.performFullTextSearchOnVideoTitle('Example video')
 
   assert(videoFullTextSearchResult.data.search.length === 1, 'Should contain exactly one video')
 
   // Perform number full text searches on Video title, that absolutely should NOT return the video.
-  videoFullTextSearchResult = await api.performFullTextSearchOnVideoTitle('unknown')
+  videoFullTextSearchResult = await query.performFullTextSearchOnVideoTitle('unknown')
 
   assert(videoFullTextSearchResult.data.search.length === 0, 'Should be empty')
 
-  videoFullTextSearchResult = await api.performFullTextSearchOnVideoTitle('MediaVideo')
+  videoFullTextSearchResult = await query.performFullTextSearchOnVideoTitle('MediaVideo')
 
   assert(videoFullTextSearchResult.data.search.length === 0, 'Should be empty')
+
+  debug('Done')
 }

+ 14 - 10
tests/network-tests/src/flows/contentDirectory/updatingChannel.ts

@@ -1,14 +1,13 @@
-import { QueryNodeApi } from '../../Api'
+import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { UpdateChannelFixture } from '../../fixtures/contentDirectoryModule'
 import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 import { assert } from 'chai'
 import { Utils } from '../../utils'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
 
-export function createUpdateChannelHandleFixture(
-  api: QueryNodeApi,
-  handle: string,
-  description: string
-): UpdateChannelFixture {
+export function createUpdateChannelHandleFixture(api: Api, handle: string, description: string): UpdateChannelFixture {
   // Create partial channel entity, only containing the fields we wish to update
   const channelUpdateInput: Partial<ChannelEntity> = {
     description,
@@ -19,20 +18,23 @@ export function createUpdateChannelHandleFixture(
   return new UpdateChannelFixture(api, channelUpdateInput, uniquePropVal)
 }
 
-export default async function updateChannel(api: QueryNodeApi) {
+export default async function updateChannel({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:updateChannel')
+  debug('Started')
+
   const handle = 'New channel example'
-  const channelResult = await api.getChannelbyHandle(handle)
+  const channelResult = await query.getChannelbyHandle(handle)
   const channel = channelResult.data.channels[0]
 
   const description = 'Updated description'
   const createUpdateChannelDescriptionHappyCaseFixture = createUpdateChannelHandleFixture(api, handle, description)
 
-  await createUpdateChannelDescriptionHappyCaseFixture.runner(false)
+  await new FixtureRunner(createUpdateChannelDescriptionHappyCaseFixture).run()
 
   // Temporary solution (wait 2 minutes)
   await Utils.wait(120000)
 
-  const channelAfterUpdateResult = await api.getChannelbyHandle(handle)
+  const channelAfterUpdateResult = await query.getChannelbyHandle(handle)
   const channelAfterUpdate = channelAfterUpdateResult.data.channels[0]
 
   // description field should be updated to provided one
@@ -42,4 +44,6 @@ export default async function updateChannel(api: QueryNodeApi) {
   assert.equal(channelAfterUpdate.coverPhotoUrl, channel.coverPhotoUrl, 'Should be equal')
   assert.equal(channelAfterUpdate.avatarPhotoUrl, channel.avatarPhotoUrl, 'Should be equal')
   assert.equal(channelAfterUpdate.isPublic, channel.isPublic, 'Should be equal')
+
+  debug('Done')
 }

+ 21 - 14
tests/network-tests/src/flows/proposals/councilSetup.ts → tests/network-tests/src/flows/council/setup.ts

@@ -1,20 +1,27 @@
 import BN from 'bn.js'
 import { PaidTermId } from '@joystream/types/members'
-import { Api } from '../../Api'
-import { CouncilElectionHappyCaseFixture } from '../../fixtures/councilElectionHappyCase'
+import { FlowProps } from '../../Flow'
+import { ElectCouncilFixture } from '../../fixtures/councilElectionModule'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
 import Debugger from 'debug'
-import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import { Resource } from '../../Resources'
 
-const debug = Debugger('flow:councilSetup')
+export default async function councilSetup({ api, env, lock }: FlowProps): Promise<void> {
+  const label = 'councilSetup'
+  const debug = Debugger(`flow:${label}`)
+
+  debug('Started')
+
+  await lock(Resource.Council)
 
-export default async function councilSetup(api: Api, env: NodeJS.ProcessEnv) {
   // Skip creating council if already elected
   if ((await api.getCouncil()).length) {
-    debug('Skipping Council Setup, Council already elected')
-    return
+    return debug('Skipping council setup. A Council is already elected')
   }
 
+  debug('Electing new council')
+
   const numberOfApplicants = (await api.getCouncilSize()).toNumber() * 2
   const applicants = api.createKeyPairs(numberOfApplicants).map((key) => key.address)
   const voters = api.createKeyPairs(5).map((key) => key.address)
@@ -24,13 +31,14 @@ export default async function councilSetup(api: Api, env: NodeJS.ProcessEnv) {
   const greaterStake: BN = new BN(+env.COUNCIL_STAKE_GREATER_AMOUNT!)
   const lesserStake: BN = new BN(+env.COUNCIL_STAKE_LESSER_AMOUNT!)
 
+  // Todo pass the label to the fixture to make debug logs of fixture associated with flow name
   const createMembersFixture = new BuyMembershipHappyCaseFixture(api, [...voters, ...applicants], paidTerms)
-  await createMembersFixture.runner(false)
+  await new FixtureRunner(createMembersFixture).run()
 
-  // The fixture moves manually with sudo the election stages, so proper processing
+  // The fixture uses sudo to transition through the election stages, so proper processing
   // that normally occurs during stage transitions does not happen. This can lead to a council
-  // that is smaller than the council size if not enough members apply.
-  const councilElectionHappyCaseFixture = new CouncilElectionHappyCaseFixture(
+  // that is smaller than the target council size if not enough members apply.
+  const electCouncilFixture = new ElectCouncilFixture(
     api,
     voters, // should be member ids
     applicants, // should be member ids
@@ -39,8 +47,7 @@ export default async function councilSetup(api: Api, env: NodeJS.ProcessEnv) {
     lesserStake
   )
 
-  await councilElectionHappyCaseFixture.runner(false)
+  await new FixtureRunner(electCouncilFixture).run()
 
-  // Elected council
-  assert((await api.getCouncil()).length)
+  debug('Done')
 }

+ 15 - 15
tests/network-tests/src/flows/membership/creatingMemberships.ts

@@ -1,4 +1,4 @@
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import {
   BuyMembershipHappyCaseFixture,
   BuyMembershipWithInsufficienFundsFixture,
@@ -6,31 +6,31 @@ import {
 import { PaidTermId } from '@joystream/types/members'
 import BN from 'bn.js'
 import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { assert } from 'chai'
 
-export default async function membershipCreation(api: Api, env: NodeJS.ProcessEnv) {
+export default async function membershipCreation({ api, env }: FlowProps): Promise<void> {
   const debug = Debugger('flow:memberships')
-  debug('started')
+  debug('Started')
+  api.enableDebugTxLogs()
 
   const N: number = +env.MEMBERSHIP_CREATION_N!
+  assert(N > 0)
   const nAccounts = api.createKeyPairs(N).map((key) => key.address)
   const aAccount = api.createKeyPairs(1)[0].address
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
 
+  // Assert membership can be bought if sufficient funds are available
   const happyCaseFixture = new BuyMembershipHappyCaseFixture(api, nAccounts, paidTerms)
-  // Buy membeship is accepted with sufficient funds
-  await happyCaseFixture.runner(false)
+  await new FixtureRunner(happyCaseFixture).run()
 
-  const insufficientFundsFixture: BuyMembershipWithInsufficienFundsFixture = new BuyMembershipWithInsufficienFundsFixture(
-    api,
-    aAccount,
-    paidTerms
-  )
-  // Account A can not buy the membership with insufficient funds
-  await insufficientFundsFixture.runner(false)
+  // Assert account can not buy the membership with insufficient funds
+  const insufficientFundsFixture = new BuyMembershipWithInsufficienFundsFixture(api, aAccount, paidTerms)
+  await new FixtureRunner(insufficientFundsFixture).run()
 
+  // Assert account was able to buy the membership with sufficient funds
   const buyMembershipAfterAccountTopUp = new BuyMembershipHappyCaseFixture(api, [aAccount], paidTerms)
+  await new FixtureRunner(buyMembershipAfterAccountTopUp).run()
 
-  // Account A was able to buy the membership with sufficient funds
-  await buyMembershipAfterAccountTopUp.runner(false)
-  debug('finished')
+  debug('Done')
 }

+ 13 - 4
tests/network-tests/src/flows/proposals/electionParametersProposal.ts

@@ -1,15 +1,24 @@
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { ElectionParametersProposalFixture } from '../../fixtures/proposalsModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource } from '../../Resources'
 
 // Election parameters proposal scenario
-export default async function electionParametersProposal(api: Api, env: NodeJS.ProcessEnv) {
+export default async function electionParametersProposal({ api, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:electionParametersProposal')
+  debug('Started')
+  await lock(Resource.Proposals)
+
   // Pre-Conditions: some members and an elected council
   const council = await api.getCouncil()
-  assert(council.length)
+  assert.notEqual(council.length, 0)
 
   const proposer = council[0].member.toString()
 
   const electionParametersProposalFixture = new ElectionParametersProposalFixture(api, proposer)
-  await electionParametersProposalFixture.runner(false)
+  await new FixtureRunner(electionParametersProposalFixture).run()
+
+  debug('Done')
 }

+ 48 - 24
tests/network-tests/src/flows/proposals/manageLeaderRole.ts

@@ -1,5 +1,6 @@
 import BN from 'bn.js'
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
 import {
   BeginWorkingGroupLeaderApplicationReviewFixture,
@@ -15,9 +16,26 @@ import { ApplyForOpeningFixture } from '../../fixtures/workingGroupModule'
 import { PaidTermId } from '@joystream/types/members'
 import { OpeningId } from '@joystream/types/hiring'
 import { ProposalId } from '@joystream/types/proposals'
+import { WorkerId } from '@joystream/types/working-group'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource, ResourceLocker } from '../../Resources'
+
+export default {
+  storage: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return manageLeaderRole(api, env, WorkingGroups.StorageWorkingGroup, lock)
+  },
+  content: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return manageLeaderRole(api, env, WorkingGroups.ContentDirectoryWorkingGroup, lock)
+  },
+}
+
+async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups, lock: ResourceLocker) {
+  const debug = Debugger(`flow:managerLeaderRole:${group}`)
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
   const leaderAccount = api.createKeyPairs(1)[0].address
 
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
@@ -45,7 +63,7 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
     paidTerms
   )
   // Buy membership for lead
-  await leaderMembershipFixture.runner(false)
+  await new FixtureRunner(leaderMembershipFixture).run()
 
   const createWorkingGroupLeaderOpeningFixture: CreateWorkingGroupLeaderOpeningFixture = new CreateWorkingGroupLeaderOpeningFixture(
     api,
@@ -55,16 +73,18 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
     api.getWorkingGroupString(group)
   )
   // Propose create leader opening
-  await createWorkingGroupLeaderOpeningFixture.runner(false)
+  await new FixtureRunner(createWorkingGroupLeaderOpeningFixture).run()
 
   // Approve add opening proposal
   const voteForCreateOpeningProposalFixture = new VoteForProposalFixture(
     api,
-    createWorkingGroupLeaderOpeningFixture.getCreatedProposalId() as OpeningId
+    createWorkingGroupLeaderOpeningFixture.getCreatedProposalId() as ProposalId
   )
 
-  await voteForCreateOpeningProposalFixture.runner(false)
-  const openingId = api.expectOpeningAddedEvent(voteForCreateOpeningProposalFixture.getEvents())
+  await new FixtureRunner(voteForCreateOpeningProposalFixture).run()
+
+  const openingId = api.findOpeningAddedEvent(voteForCreateOpeningProposalFixture.events, group) as OpeningId
+  assert(openingId)
 
   const applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
     api,
@@ -74,7 +94,7 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
     openingId,
     group
   )
-  await applyForLeaderOpeningFixture.runner(false)
+  await new FixtureRunner(applyForLeaderOpeningFixture).run()
   const applicationId = applyForLeaderOpeningFixture.getApplicationIds()[0]
 
   const beginWorkingGroupLeaderApplicationReviewFixture = new BeginWorkingGroupLeaderApplicationReviewFixture(
@@ -84,13 +104,14 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
     api.getWorkingGroupString(group)
   )
   // Propose begin leader application review
-  await beginWorkingGroupLeaderApplicationReviewFixture.runner(false)
+  await new FixtureRunner(beginWorkingGroupLeaderApplicationReviewFixture).run()
 
   const voteForBeginReviewProposal = new VoteForProposalFixture(
     api,
     beginWorkingGroupLeaderApplicationReviewFixture.getCreatedProposalId() as ProposalId
   )
-  await voteForBeginReviewProposal.runner(false)
+
+  await new FixtureRunner(voteForBeginReviewProposal).run()
 
   const fillLeaderOpeningProposalFixture = new FillLeaderOpeningProposalFixture(
     api,
@@ -103,21 +124,21 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
     group
   )
   // Propose fill leader opening
-  await fillLeaderOpeningProposalFixture.runner(false)
+  await new FixtureRunner(fillLeaderOpeningProposalFixture).run()
 
   const voteForFillLeaderProposalFixture = new VoteForProposalFixture(
     api,
     fillLeaderOpeningProposalFixture.getCreatedProposalId() as ProposalId
   )
   // Approve fill leader opening
-  await voteForFillLeaderProposalFixture.runner(false)
+  await new FixtureRunner(voteForFillLeaderProposalFixture).run()
 
   const hiredLead = await api.getGroupLead(group)
   assert(hiredLead)
 
   const setLeaderRewardProposalFixture = new SetLeaderRewardProposalFixture(api, proposer, alteredPayoutAmount, group)
   // Propose leader reward
-  await setLeaderRewardProposalFixture.runner(false)
+  await new FixtureRunner(setLeaderRewardProposalFixture).run()
 
   const voteForeLeaderRewardFixture = new VoteForProposalFixture(
     api,
@@ -125,11 +146,12 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
   )
 
   // Approve new leader reward
-  await voteForeLeaderRewardFixture.runner(false)
+  await new FixtureRunner(voteForeLeaderRewardFixture).run()
 
-  const leadId = await api.getLeadWorkerId(group)
-  // This check is prone to failure if more than one worker's reward amount was updated
-  const workerId = api.expectWorkerRewardAmountUpdatedEvent(voteForeLeaderRewardFixture.getEvents())
+  const leadId = (await api.getLeadWorkerId(group)) as WorkerId
+  assert(leadId)
+  const workerId = api.findWorkerRewardAmountUpdatedEvent(voteForeLeaderRewardFixture.events, group, leadId) as WorkerId
+  assert(workerId)
   assert(leadId!.eq(workerId))
   const rewardRelationship = await api.getWorkerRewardRelationship(leadId!, group)
   assert(rewardRelationship.amount_per_payout.eq(alteredPayoutAmount))
@@ -142,38 +164,40 @@ export default async function manageLeaderRole(api: Api, env: NodeJS.ProcessEnv,
   )
 
   // Propose decrease stake
-  await decreaseLeaderStakeProposalFixture.runner(false)
+  await new FixtureRunner(decreaseLeaderStakeProposalFixture).run()
 
-  let newStake: BN = applicationStake.sub(stakeDecrement)
+  // let newStake: BN = applicationStake.sub(stakeDecrement)
   // Approve decreased leader stake
   const voteForDecreaseStakeProposal = new VoteForProposalFixture(
     api,
     decreaseLeaderStakeProposalFixture.getCreatedProposalId() as ProposalId
   )
-  await voteForDecreaseStakeProposal.runner(false)
+  await new FixtureRunner(voteForDecreaseStakeProposal).run()
 
   const slashLeaderProposalFixture = new SlashLeaderProposalFixture(api, proposer, slashAmount, group)
   // Propose leader slash
-  await slashLeaderProposalFixture.runner(false)
+  await new FixtureRunner(slashLeaderProposalFixture).run()
 
   // Approve leader slash
-  newStake = newStake.sub(slashAmount)
+  // newStake = newStake.sub(slashAmount)
   const voteForSlashProposalFixture = new VoteForProposalFixture(
     api,
     slashLeaderProposalFixture.getCreatedProposalId() as ProposalId
   )
-  await voteForSlashProposalFixture.runner(false)
+  await new FixtureRunner(voteForSlashProposalFixture).run()
 
   const terminateLeaderRoleProposalFixture = new TerminateLeaderRoleProposalFixture(api, proposer, false, group)
   // Propose terminate leader role
-  await terminateLeaderRoleProposalFixture.runner(false)
+  await new FixtureRunner(terminateLeaderRoleProposalFixture).run()
 
   const voteForLeaderRoleTerminationFixture = new VoteForProposalFixture(
     api,
     terminateLeaderRoleProposalFixture.getCreatedProposalId() as ProposalId
   )
-  await voteForLeaderRoleTerminationFixture.runner(false)
+  await new FixtureRunner(voteForLeaderRoleTerminationFixture).run()
 
   const maybeLead = await api.getGroupLead(group)
   assert(!maybeLead)
+
+  debug('Done')
 }

+ 12 - 3
tests/network-tests/src/flows/proposals/spendingProposal.ts

@@ -1,9 +1,16 @@
 import BN from 'bn.js'
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { SpendingProposalFixture } from '../../fixtures/proposalsModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource } from '../../Resources'
+
+export default async function spendingProposal({ api, env, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:spendingProposals')
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function spendingProposal(api: Api, env: NodeJS.ProcessEnv) {
   const spendingBalance: BN = new BN(+env.SPENDING_BALANCE!)
   const mintCapacity: BN = new BN(+env.COUNCIL_MINTING_CAPACITY!)
 
@@ -16,5 +23,7 @@ export default async function spendingProposal(api: Api, env: NodeJS.ProcessEnv)
   const spendingProposalFixture = new SpendingProposalFixture(api, proposer, spendingBalance, mintCapacity)
 
   // Spending proposal test
-  await spendingProposalFixture.runner(false)
+  await new FixtureRunner(spendingProposalFixture).run()
+
+  debug('Done')
 }

+ 12 - 3
tests/network-tests/src/flows/proposals/textProposal.ts

@@ -1,8 +1,15 @@
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { TextProposalFixture } from '../../fixtures/proposalsModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource } from '../../Resources'
+
+export default async function textProposal({ api, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:textProposal')
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function textProposal(api: Api, env: NodeJS.ProcessEnv) {
   // Pre-conditions: members and council
   const council = await api.getCouncil()
   assert(council.length)
@@ -10,5 +17,7 @@ export default async function textProposal(api: Api, env: NodeJS.ProcessEnv) {
   const proposer = council[0].member.toString()
 
   const textProposalFixture: TextProposalFixture = new TextProposalFixture(api, proposer)
-  await textProposalFixture.runner(false)
+  await new FixtureRunner(textProposalFixture).run()
+
+  debug('Done')
 }

+ 13 - 4
tests/network-tests/src/flows/proposals/updateRuntime.ts

@@ -1,11 +1,18 @@
 import BN from 'bn.js'
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
 import { UpdateRuntimeFixture } from '../../fixtures/proposalsModule'
 import { PaidTermId } from '@joystream/types/members'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource } from '../../Resources'
+
+export default async function updateRuntime({ api, env, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:updateRuntime')
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function updateRuntime(api: Api, env: NodeJS.ProcessEnv) {
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
   const runtimePath: string = env.RUNTIME_WASM_PATH!
 
@@ -16,7 +23,7 @@ export default async function updateRuntime(api: Api, env: NodeJS.ProcessEnv) {
   const proposer = council[0].member.toString()
 
   const updateRuntimeFixture: UpdateRuntimeFixture = new UpdateRuntimeFixture(api, proposer, runtimePath)
-  await updateRuntimeFixture.runner(false)
+  await new FixtureRunner(updateRuntimeFixture).run()
 
   // Some tests after runtime update
   const createMembershipsFixture = new BuyMembershipHappyCaseFixture(
@@ -24,5 +31,7 @@ export default async function updateRuntime(api: Api, env: NodeJS.ProcessEnv) {
     api.createKeyPairs(1).map((key) => key.address),
     paidTerms
   )
-  await createMembershipsFixture.runner(false)
+  await new FixtureRunner(createMembershipsFixture).run()
+
+  debug('Done')
 }

+ 12 - 3
tests/network-tests/src/flows/proposals/validatorCountProposal.ts

@@ -1,9 +1,16 @@
 import BN from 'bn.js'
-import { Api } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { ValidatorCountProposalFixture } from '../../fixtures/proposalsModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource } from '../../Resources'
+
+export default async function validatorCount({ api, env, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:validatorCountProposal')
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function validatorCount(api: Api, env: NodeJS.ProcessEnv) {
   // Pre-conditions: members and council
   const council = await api.getCouncil()
   assert(council.length)
@@ -17,5 +24,7 @@ export default async function validatorCount(api: Api, env: NodeJS.ProcessEnv) {
     proposer,
     validatorCountIncrement
   )
-  await validatorCountProposalFixture.runner(false)
+  await new FixtureRunner(validatorCountProposalFixture).run()
+
+  debug('Done')
 }

+ 23 - 3
tests/network-tests/src/flows/proposals/workingGroupMintCapacityProposal.ts

@@ -1,10 +1,28 @@
 import BN from 'bn.js'
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { VoteForProposalFixture, WorkingGroupMintCapacityProposalFixture } from '../../fixtures/proposalsModule'
 import { ProposalId } from '@joystream/types/proposals'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource, ResourceLocker } from '../../Resources'
+
+export default {
+  storage: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return workingGroupMintCapactiy(api, env, WorkingGroups.StorageWorkingGroup, lock)
+  },
+
+  content: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return workingGroupMintCapactiy(api, env, WorkingGroups.ContentDirectoryWorkingGroup, lock)
+  },
+}
+
+async function workingGroupMintCapactiy(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups, lock: ResourceLocker) {
+  const debug = Debugger(`flow:workingGroupMintCapacityProposal:${group}`)
+  debug('Started')
+  await lock(Resource.Proposals)
 
-export default async function workingGroupMintCapactiy(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
   const mintCapacityIncrement: BN = new BN(env.MINT_CAPACITY_INCREMENT!)
 
   // Pre-conditions: members and council
@@ -20,7 +38,7 @@ export default async function workingGroupMintCapactiy(api: Api, env: NodeJS.Pro
     group
   )
   // Propose mint capacity
-  await workingGroupMintCapacityProposalFixture.runner(false)
+  await new FixtureRunner(workingGroupMintCapacityProposalFixture).run()
 
   const voteForProposalFixture: VoteForProposalFixture = new VoteForProposalFixture(
     api,
@@ -28,5 +46,7 @@ export default async function workingGroupMintCapactiy(api: Api, env: NodeJS.Pro
   )
 
   // Approve mint capacity
-  await voteForProposalFixture.runner(false)
+  await new FixtureRunner(voteForProposalFixture).run()
+
+  debug('Done')
 }

+ 9 - 3
tests/network-tests/src/flows/storageNode/getContentFromStorageNode.ts

@@ -3,17 +3,21 @@ import { assert } from 'chai'
 import { ContentId } from '@joystream/types/media'
 import { registry } from '@joystream/types'
 
-import { QueryNodeApi } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { Utils } from '../../utils'
+import Debugger from 'debug'
+
+export default async function getContentFromStorageNode({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:getContentFromStorageNode')
+  debug('Started')
 
-export default async function getContentFromStorageNode(api: QueryNodeApi): Promise<void> {
   const videoTitle = 'Storage node test'
 
   // Temporary solution (wait 2 minutes)
   await Utils.wait(120000)
 
   // Query video by title with where expression
-  const videoWhereQueryResult = await api.performWhereQueryByVideoTitle(videoTitle)
+  const videoWhereQueryResult = await query.performWhereQueryByVideoTitle(videoTitle)
 
   assert.equal(1, videoWhereQueryResult.data.videos.length, 'Should fetch only one video')
 
@@ -36,4 +40,6 @@ export default async function getContentFromStorageNode(api: QueryNodeApi): Prom
   const contentLenght = Number.parseInt(response.headers['content-length'])
 
   assert.equal(contentLenght, dataObject!.size_in_bytes.toJSON(), 'Content should be same size')
+
+  debug('Done')
 }

+ 26 - 8
tests/network-tests/src/flows/workingGroup/atLeastValueBug.ts

@@ -1,13 +1,17 @@
-import { Api, WorkingGroups } from '../../Api'
+import { WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import { AddWorkerOpeningFixture } from '../../fixtures/workingGroupModule'
 import BN from 'bn.js'
 import { assert } from 'chai'
 import Debugger from 'debug'
-const debug = Debugger('flow:atLeastValueBug')
+import { FixtureRunner } from '../../Fixture'
 
 // Zero at least value bug scenario
-export default async function zeroAtLeastValueBug(api: Api, env: NodeJS.ProcessEnv) {
+export default async function zeroAtLeastValueBug({ api, env }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:atLeastValueBug')
   debug('Started')
+  api.enableDebugTxLogs()
+
   const applicationStake: BN = new BN(env.WORKING_GROUP_APPLICATION_STAKE!)
   const roleStake: BN = new BN(env.WORKING_GROUP_ROLE_STAKE!)
   const unstakingPeriod: BN = new BN(env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
@@ -16,7 +20,7 @@ export default async function zeroAtLeastValueBug(api: Api, env: NodeJS.ProcessE
   // Pre-conditions
   // A hired lead
   const lead = await api.getGroupLead(WorkingGroups.StorageWorkingGroup)
-  assert(lead)
+  assert.notEqual(lead, undefined)
 
   const addWorkerOpeningWithoutStakeFixture = new AddWorkerOpeningFixture(
     api,
@@ -26,8 +30,15 @@ export default async function zeroAtLeastValueBug(api: Api, env: NodeJS.ProcessE
     unstakingPeriod,
     WorkingGroups.StorageWorkingGroup
   )
-  // Add worker opening with 0 stake, expect failure
-  await addWorkerOpeningWithoutStakeFixture.runner(true)
+
+  // Add worker opening with 0 stake, expect failure!
+  await new FixtureRunner(addWorkerOpeningWithoutStakeFixture).run()
+
+  assert.equal(
+    addWorkerOpeningWithoutStakeFixture.getCreatedOpeningId(),
+    undefined,
+    'Adding Worker Opening Should have failed'
+  )
 
   const addWorkerOpeningWithoutUnstakingPeriodFixture = new AddWorkerOpeningFixture(
     api,
@@ -37,8 +48,15 @@ export default async function zeroAtLeastValueBug(api: Api, env: NodeJS.ProcessE
     new BN(0),
     WorkingGroups.StorageWorkingGroup
   )
-  // Add worker opening with 0 unstaking period, expect failure
-  await addWorkerOpeningWithoutUnstakingPeriodFixture.runner(true)
+
+  // Add worker opening with 0 unstaking period, expect failure!
+  await new FixtureRunner(addWorkerOpeningWithoutUnstakingPeriodFixture).run()
+
+  assert.equal(
+    addWorkerOpeningWithoutUnstakingPeriodFixture.getCreatedOpeningId(),
+    undefined,
+    'Adding Worker Opening Should have failed'
+  )
 
   // TODO: close openings
   debug('Passed')

+ 24 - 11
tests/network-tests/src/flows/workingGroup/leaderSetup.ts

@@ -1,19 +1,29 @@
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import BN from 'bn.js'
 import { PaidTermId } from '@joystream/types/members'
 import { SudoHireLeadFixture } from '../../fixtures/sudoHireLead'
 import { assert } from 'chai'
-import { KeyringPair } from '@polkadot/keyring/types'
+// import { KeyringPair } from '@polkadot/keyring/types'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+
+export default {
+  storage: async function ({ api, env }: FlowProps): Promise<void> {
+    return leaderSetup(api, env, WorkingGroups.StorageWorkingGroup)
+  },
+  content: async function ({ api, env }: FlowProps): Promise<void> {
+    return leaderSetup(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
+  },
+}
 
 // Worker application happy case scenario
-export default async function leaderSetup(
-  api: Api,
-  env: NodeJS.ProcessEnv,
-  group: WorkingGroups
-): Promise<KeyringPair> {
-  const lead = await api.getGroupLead(group)
+async function leaderSetup(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups): Promise<void> {
+  const debug = Debugger(`flow:leaderSetup:${group}`)
+  debug('Started')
 
-  assert(!lead, `Lead is already set`)
+  const existingLead = await api.getGroupLead(group)
+  assert.equal(existingLead, undefined, 'Lead is already set')
 
   const leadKeyPair = api.createKeyPairs(1)[0]
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
@@ -36,11 +46,14 @@ export default async function leaderSetup(
     payoutAmount,
     group
   )
-  await leaderHiringHappyCaseFixture.runner(false)
+  await new FixtureRunner(leaderHiringHappyCaseFixture).run()
 
   const hiredLead = await api.getGroupLead(group)
-  assert(hiredLead, `${group} group Lead was not hired!`)
+  assert.notEqual(hiredLead, undefined, `${group} group Lead was not hired!`)
   assert(hiredLead!.role_account_id.eq(leadKeyPair.address))
 
-  return leadKeyPair
+  debug('Done')
+
+  // Who ever needs it will need to get it from the Api layer
+  // return leadKeyPair
 }

+ 29 - 12
tests/network-tests/src/flows/workingGroup/manageWorkerAsLead.ts

@@ -1,4 +1,5 @@
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import {
   ApplyForOpeningFixture,
   AddWorkerOpeningFixture,
@@ -13,10 +14,21 @@ import BN from 'bn.js'
 import { OpeningId } from '@joystream/types/hiring'
 import { assert } from 'chai'
 import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+
+export default {
+  storage: async function ({ api, env }: FlowProps): Promise<void> {
+    return manageWorkerAsLead(api, env, WorkingGroups.StorageWorkingGroup)
+  },
+  content: async function ({ api, env }: FlowProps): Promise<void> {
+    return manageWorkerAsLead(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
+  },
+}
+
+async function manageWorkerAsLead(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups): Promise<void> {
+  const debug = Debugger(`flow:manageWorkerAsLead:${group}`)
+  debug('Started')
 
-// Manage worker as lead scenario
-export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
-  const debug = Debugger(`manageWorker:${group}`)
   const applicationStake: BN = new BN(env.WORKING_GROUP_APPLICATION_STAKE!)
   const roleStake: BN = new BN(env.WORKING_GROUP_ROLE_STAKE!)
   const firstRewardInterval: BN = new BN(env.LONG_REWARD_INTERVAL!)
@@ -31,7 +43,7 @@ export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, gro
 
   const applicants = api.createKeyPairs(5).map((key) => key.address)
   const memberSetFixture = new BuyMembershipHappyCaseFixture(api, applicants, paidTerms)
-  await memberSetFixture.runner(false)
+  await new FixtureRunner(memberSetFixture).run()
 
   const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
     api,
@@ -42,7 +54,8 @@ export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, gro
     group
   )
   // Add worker opening
-  await addWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(addWorkerOpeningFixture).run()
+  assert(addWorkerOpeningFixture.getCreatedOpeningId())
 
   // First apply for worker opening
   const applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
@@ -53,9 +66,11 @@ export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, gro
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await applyForWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(applyForWorkerOpeningFixture).run()
+  const applicationIds = applyForWorkerOpeningFixture.getApplicationIds()
+  assert.equal(applicants.length, applicationIds.length)
 
-  const applicationIdsToHire = applyForWorkerOpeningFixture.getApplicationIds().slice(0, 2)
+  const applicationIdsToHire = applicationIds.slice(0, 2)
 
   // Begin application review
   const beginApplicationReviewFixture = new BeginApplicationReviewFixture(
@@ -63,7 +78,7 @@ export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, gro
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await beginApplicationReviewFixture.runner(false)
+  await new FixtureRunner(beginApplicationReviewFixture).run()
 
   // Fill worker opening
   const fillOpeningFixture = new FillOpeningFixture(
@@ -75,20 +90,22 @@ export default async function manageWorker(api: Api, env: NodeJS.ProcessEnv, gro
     payoutAmount,
     group
   )
-  await fillOpeningFixture.runner(false)
+  await new FixtureRunner(fillOpeningFixture).run()
 
   const firstWorkerId = fillOpeningFixture.getWorkerIds()[0]
 
   const decreaseStakeFixture = new DecreaseStakeFixture(api, firstWorkerId, group)
   // Decrease worker stake
-  await decreaseStakeFixture.runner(false)
+  await new FixtureRunner(decreaseStakeFixture).run()
 
   const slashFixture: SlashFixture = new SlashFixture(api, firstWorkerId, group)
   // Slash worker
-  await slashFixture.runner(false)
+  await new FixtureRunner(slashFixture).run()
 
   const terminateRoleFixture = new TerminateRoleFixture(api, firstWorkerId, group)
 
   // Terminate workers
-  await terminateRoleFixture.runner(false)
+  await new FixtureRunner(terminateRoleFixture).run()
+
+  debug('Done')
 }

+ 26 - 9
tests/network-tests/src/flows/workingGroup/manageWorkerAsWorker.ts

@@ -1,4 +1,5 @@
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import {
   AddWorkerOpeningFixture,
   ApplyForOpeningFixture,
@@ -11,9 +12,23 @@ import BN from 'bn.js'
 import { OpeningId } from '@joystream/types/hiring'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+
+export default {
+  storage: async function ({ api, env }: FlowProps): Promise<void> {
+    return manageWorkerAsWorker(api, env, WorkingGroups.StorageWorkingGroup)
+  },
+  content: async function ({ api, env }: FlowProps): Promise<void> {
+    return manageWorkerAsWorker(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
+  },
+}
 
 // Manage worker as worker
-export default async function manageWorkerAsWorker(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
+async function manageWorkerAsWorker(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
+  const debug = Debugger(`flow:manageWorkerAsWorker:${group}`)
+  debug('Started')
+
   const applicationStake: BN = new BN(env.WORKING_GROUP_APPLICATION_STAKE!)
   const roleStake: BN = new BN(env.WORKING_GROUP_ROLE_STAKE!)
   const firstRewardInterval: BN = new BN(env.LONG_REWARD_INTERVAL!)
@@ -30,7 +45,7 @@ export default async function manageWorkerAsWorker(api: Api, env: NodeJS.Process
 
   const memberSetFixture = new BuyMembershipHappyCaseFixture(api, newMembers, paidTerms)
   // Recreating set of members
-  await memberSetFixture.runner(false)
+  await new FixtureRunner(memberSetFixture).run()
   const applicant = newMembers[0]
 
   const addWorkerOpeningFixture = new AddWorkerOpeningFixture(
@@ -42,7 +57,7 @@ export default async function manageWorkerAsWorker(api: Api, env: NodeJS.Process
     group
   )
   // Add worker opening
-  await addWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(addWorkerOpeningFixture).run()
 
   // First apply for worker opening
   const applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
@@ -53,7 +68,7 @@ export default async function manageWorkerAsWorker(api: Api, env: NodeJS.Process
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await applyForWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(applyForWorkerOpeningFixture).run()
   const applicationIdToHire = applyForWorkerOpeningFixture.getApplicationIds()[0]
 
   // Begin application review
@@ -62,7 +77,7 @@ export default async function manageWorkerAsWorker(api: Api, env: NodeJS.Process
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await beginApplicationReviewFixture.runner(false)
+  await new FixtureRunner(beginApplicationReviewFixture).run()
 
   // Fill worker opening
   const fillOpeningFixture = new FillOpeningFixture(
@@ -74,17 +89,19 @@ export default async function manageWorkerAsWorker(api: Api, env: NodeJS.Process
     payoutAmount,
     group
   )
-  await fillOpeningFixture.runner(false)
+  await new FixtureRunner(fillOpeningFixture).run()
   const workerId = fillOpeningFixture.getWorkerIds()[0]
   const increaseStakeFixture: IncreaseStakeFixture = new IncreaseStakeFixture(api, workerId, group)
   // Increase worker stake
-  await increaseStakeFixture.runner(false)
+  await new FixtureRunner(increaseStakeFixture).run()
 
   const updateRewardAccountFixture: UpdateRewardAccountFixture = new UpdateRewardAccountFixture(api, workerId, group)
   // Update reward account
-  await updateRewardAccountFixture.runner(false)
+  await new FixtureRunner(updateRewardAccountFixture).run()
 
   const updateRoleAccountFixture: UpdateRewardAccountFixture = new UpdateRewardAccountFixture(api, workerId, group)
   // Update role account
-  await updateRoleAccountFixture.runner(false)
+  await new FixtureRunner(updateRoleAccountFixture).run()
+
+  debug('Done')
 }

+ 30 - 10
tests/network-tests/src/flows/workingGroup/workerPayout.ts

@@ -1,4 +1,5 @@
 import { Api, WorkingGroups } from '../../Api'
+import { FlowProps } from '../../Flow'
 import {
   AddWorkerOpeningFixture,
   ApplyForOpeningFixture,
@@ -13,9 +14,24 @@ import { OpeningId } from '@joystream/types/hiring'
 import { ProposalId } from '@joystream/types/proposals'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
 import { assert } from 'chai'
+import { FixtureRunner } from '../../Fixture'
+import Debugger from 'debug'
+import { Resource, ResourceLocker } from '../../Resources'
+
+export default {
+  storage: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return workerPayouts(api, env, WorkingGroups.StorageWorkingGroup, lock)
+  },
+  content: async function ({ api, env, lock }: FlowProps): Promise<void> {
+    return workerPayouts(api, env, WorkingGroups.ContentDirectoryWorkingGroup, lock)
+  },
+}
+
+async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups, lock: ResourceLocker) {
+  const debug = Debugger(`flow:workerPayout:${group}`)
+  debug('Started')
+  await lock(Resource.Proposals)
 
-// Worker payout scenario
-export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
   const applicationStake: BN = new BN(env.WORKING_GROUP_APPLICATION_STAKE!)
   const roleStake: BN = new BN(env.WORKING_GROUP_ROLE_STAKE!)
@@ -27,11 +43,13 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
   const openingActivationDelay: BN = new BN(0)
 
   const lead = await api.getGroupLead(group)
+  assert(lead)
+
   const newMembers = api.createKeyPairs(5).map((key) => key.address)
 
   const memberSetFixture = new BuyMembershipHappyCaseFixture(api, newMembers, paidTerms)
   // Recreating set of members
-  await memberSetFixture.runner(false)
+  await new FixtureRunner(memberSetFixture).run()
 
   const workingGroupMintCapacityProposalFixture = new WorkingGroupMintCapacityProposalFixture(
     api,
@@ -40,14 +58,14 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
     group
   )
   // Propose mint capacity
-  await workingGroupMintCapacityProposalFixture.runner(false)
+  await new FixtureRunner(workingGroupMintCapacityProposalFixture).run()
 
   // Approve mint capacity
   const voteForProposalFixture = new VoteForProposalFixture(
     api,
     workingGroupMintCapacityProposalFixture.getCreatedProposalId() as ProposalId
   )
-  await voteForProposalFixture.runner(false)
+  await new FixtureRunner(voteForProposalFixture).run()
 
   const addWorkerOpeningFixture = new AddWorkerOpeningFixture(
     api,
@@ -58,7 +76,7 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
     group
   )
   // Add worker opening
-  await addWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(addWorkerOpeningFixture).run()
 
   // First apply for worker opening
   const applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
@@ -69,7 +87,7 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await applyForWorkerOpeningFixture.runner(false)
+  await new FixtureRunner(applyForWorkerOpeningFixture).run()
   const applicationId = applyForWorkerOpeningFixture.getApplicationIds()[0]
 
   // Begin application review
@@ -78,7 +96,7 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
     addWorkerOpeningFixture.getCreatedOpeningId() as OpeningId,
     group
   )
-  await beginApplicationReviewFixture.runner(false)
+  await new FixtureRunner(beginApplicationReviewFixture).run()
 
   // Fill worker opening
   const fillOpeningFixture = new FillOpeningFixture(
@@ -90,9 +108,11 @@ export default async function workerPayouts(api: Api, env: NodeJS.ProcessEnv, gr
     payoutAmount,
     group
   )
-  await fillOpeningFixture.runner(false)
+  await new FixtureRunner(fillOpeningFixture).run()
   const workerId = fillOpeningFixture.getWorkerIds()[0]
   const awaitPayoutFixture: AwaitPayoutFixture = new AwaitPayoutFixture(api, workerId, group)
   // Await worker payout
-  await awaitPayoutFixture.runner(false)
+  await new FixtureRunner(awaitPayoutFixture).run()
+
+  debug('Done')
 }

+ 9 - 48
tests/network-tests/src/scenarios/content-directory.ts

@@ -1,53 +1,14 @@
-import { WsProvider } from '@polkadot/api'
-import { Api, QueryNodeApi, WorkingGroups } from '../Api'
-import { config } from 'dotenv'
 import leaderSetup from '../flows/workingGroup/leaderSetup'
 import initializeContentDirectory from '../flows/contentDirectory/contentDirectoryInitialization'
 import createChannel from '../flows/contentDirectory/creatingChannel'
 import createVideo from '../flows/contentDirectory/creatingVideo'
 import updateChannel from '../flows/contentDirectory/updatingChannel'
-import { ApolloClient, InMemoryCache } from '@apollo/client'
-
-const scenario = async () => {
-  // Load env variables
-  config()
-  const env = process.env
-
-  // Connect api to the chain
-  const nodeUrl: string = env.NODE_URL || 'ws://127.0.0.1:9944'
-  const provider = new WsProvider(nodeUrl)
-
-  const queryNodeUrl: string = env.QUERY_NODE_URL || 'http://127.0.0.1:8081/graphql'
-
-  const queryNodeProvider = new ApolloClient({
-    uri: queryNodeUrl,
-    cache: new InMemoryCache(),
-    defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
-  })
-
-  const api: QueryNodeApi = await QueryNodeApi.new(
-    provider,
-    queryNodeProvider,
-    env.TREASURY_ACCOUNT_URI || '//Alice',
-    env.SUDO_ACCOUNT_URI || '//Alice'
-  )
-
-  const leadKeyPair = await leaderSetup(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
-
-  // Some flows that use the curator lead to perform some tests...
-  //
-
-  await initializeContentDirectory(api, leadKeyPair)
-
-  await createChannel(api)
-
-  await createVideo(api)
-
-  await updateChannel(api)
-
-  // Note: disconnecting and then reconnecting to the chain in the same process
-  // doesn't seem to work!
-  api.close()
-}
-
-scenario()
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  job('setup content lead', leaderSetup.content)
+    .then(job('init-content-dir', initializeContentDirectory))
+    .then(job('create-channel', createChannel))
+    .then(job('create-video', createVideo))
+    .then(job('update-channel', updateChannel))
+})

+ 30 - 56
tests/network-tests/src/scenarios/full.ts

@@ -1,74 +1,48 @@
-import { WsProvider } from '@polkadot/api'
-import { Api, WorkingGroups } from '../Api'
-import { config } from 'dotenv'
-import Debugger from 'debug'
-
 import creatingMemberships from '../flows/membership/creatingMemberships'
-import councilSetup from '../flows/proposals/councilSetup'
+import councilSetup from '../flows/council/setup'
 import leaderSetup from '../flows/workingGroup/leaderSetup'
 import electionParametersProposal from '../flows/proposals/electionParametersProposal'
 import manageLeaderRole from '../flows/proposals/manageLeaderRole'
 import spendingProposal from '../flows/proposals/spendingProposal'
 import textProposal from '../flows/proposals/textProposal'
 import validatorCountProposal from '../flows/proposals/validatorCountProposal'
-import workingGroupMintCapacityProposal from '../flows/proposals/workingGroupMintCapacityProposal'
+import wgMintCapacityProposal from '../flows/proposals/workingGroupMintCapacityProposal'
 import atLeastValueBug from '../flows/workingGroup/atLeastValueBug'
 import manageWorkerAsLead from '../flows/workingGroup/manageWorkerAsLead'
 import manageWorkerAsWorker from '../flows/workingGroup/manageWorkerAsWorker'
 import workerPayout from '../flows/workingGroup/workerPayout'
+import { scenario } from '../Scenario'
 
-const scenario = async () => {
-  const debug = Debugger('scenario:full')
-
-  // Load env variables
-  config()
-  const env = process.env
-
-  // Connect api to the chain
-  const nodeUrl: string = env.NODE_URL || 'ws://127.0.0.1:9944'
-  const provider = new WsProvider(nodeUrl)
-  const api: Api = await Api.create(provider, env.TREASURY_ACCOUNT_URI || '//Alice', env.SUDO_ACCOUNT_URI || '//Alice')
+scenario(async ({ job }) => {
+  job('creating members', creatingMemberships)
 
-  await Promise.all([creatingMemberships(api, env), councilSetup(api, env)])
+  const councilJob = job('council setup', councilSetup)
 
-  // Runtime is configured for MaxActiveProposalLimit = 5
-  // So we should ensure we don't exceed that number of active proposals
-  // which limits the number of concurrent tests that create proposals
-  await Promise.all([
-    electionParametersProposal(api, env),
-    spendingProposal(api, env),
-    textProposal(api, env),
-    validatorCountProposal(api, env),
-  ])
+  job('proposals', [
+    electionParametersProposal,
+    spendingProposal,
+    textProposal,
+    validatorCountProposal,
+    wgMintCapacityProposal.storage,
+    wgMintCapacityProposal.content,
+    manageLeaderRole.storage,
+    manageLeaderRole.content,
+  ]).requires(councilJob)
 
-  await Promise.all([
-    workingGroupMintCapacityProposal(api, env, WorkingGroups.StorageWorkingGroup),
-    workingGroupMintCapacityProposal(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-    manageLeaderRole(api, env, WorkingGroups.StorageWorkingGroup),
-    manageLeaderRole(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-  ])
+  const manageLeadsJob = job('lead-roles', [manageLeaderRole.storage, manageLeaderRole.content]).requires(councilJob)
 
-  await Promise.all([
-    leaderSetup(api, env, WorkingGroups.StorageWorkingGroup),
-    leaderSetup(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-  ])
+  const leadSetupJob = job('setup leads', [leaderSetup.storage, leaderSetup.content]).after(manageLeadsJob)
 
-  // All tests below require an active Lead for each group
   // Test bug only on one instance of working group is sufficient
-  await atLeastValueBug(api, env)
-
-  await Promise.all([
-    manageWorkerAsLead(api, env, WorkingGroups.StorageWorkingGroup),
-    manageWorkerAsWorker(api, env, WorkingGroups.StorageWorkingGroup),
-    workerPayout(api, env, WorkingGroups.StorageWorkingGroup),
-    manageWorkerAsLead(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-    manageWorkerAsWorker(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-    workerPayout(api, env, WorkingGroups.ContentDirectoryWorkingGroup),
-  ])
-
-  // Note: disconnecting and then reconnecting to the chain in the same process
-  // doesn't seem to work!
-  api.close()
-}
-
-scenario()
+  job('at least value bug', atLeastValueBug).requires(leadSetupJob)
+
+  // tests minting payouts (requires council to set mint capacity)
+  job('worker payouts', [workerPayout.storage, workerPayout.content]).requires(leadSetupJob).requires(councilJob)
+
+  job('working group tests', [
+    manageWorkerAsLead.storage,
+    manageWorkerAsWorker.storage,
+    manageWorkerAsLead.content,
+    manageWorkerAsWorker.content,
+  ]).requires(leadSetupJob)
+})

+ 4 - 31
tests/network-tests/src/scenarios/storage-node.ts

@@ -1,33 +1,6 @@
-import { config } from 'dotenv'
-import { WsProvider } from '@polkadot/api'
-import { ApolloClient, InMemoryCache } from '@apollo/client'
-
-import { QueryNodeApi } from '../Api'
 import getContentFromStorageNode from '../flows/storageNode/getContentFromStorageNode'
+import { scenario } from '../Scenario'
 
-const scenario = async () => {
-  // Load env variables
-  config()
-  const env = process.env
-
-  const queryNodeProvider = new ApolloClient({
-    uri: env.QUERY_NODE_URL,
-    cache: new InMemoryCache(),
-    defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
-  })
-
-  const api: QueryNodeApi = await QueryNodeApi.new(
-    new WsProvider(env.NODE_URL),
-    queryNodeProvider,
-    env.TREASURY_ACCOUNT_URI || '//Alice',
-    env.SUDO_ACCOUNT_URI || '//Alice'
-  )
-
-  await getContentFromStorageNode(api)
-
-  // Note: disconnecting and then reconnecting to the chain in the same process
-  // doesn't seem to work!
-  api.close()
-}
-
-scenario()
+scenario(async ({ job }) => {
+  job('content-from-storage-node', getContentFromStorageNode)
+})

+ 11 - 0
tests/network-tests/src/scenarios/tests/resource-locks-1.ts

@@ -0,0 +1,11 @@
+import { scenario } from '../../Scenario'
+import { FlowProps } from '../../Flow'
+import { Resource } from '../../Resources'
+
+async function flow1({ lock }: FlowProps) {
+  await lock(Resource.Council)
+}
+
+scenario(async ({ job }) => {
+  job('test', [flow1, flow1])
+})

+ 14 - 0
tests/network-tests/src/scenarios/tests/resource-locks-2.ts

@@ -0,0 +1,14 @@
+import { scenario } from '../../Scenario'
+import { FlowProps } from '../../Flow'
+import { Resource } from '../../Resources'
+
+async function flow({ lock }: FlowProps) {
+  await lock(Resource.Proposals)
+}
+
+scenario(async ({ job }) => {
+  // Runtime is configured for MaxActiveProposalLimit = 5
+  // So we should ensure we don't exceed that number of active proposals
+  // which limits the number of concurrent tests that create proposals
+  job('test', [flow, flow, flow, flow, flow, flow])
+})

+ 98 - 32
tests/network-tests/src/sender.ts

@@ -1,71 +1,137 @@
 import { ApiPromise, Keyring } from '@polkadot/api'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { ISubmittableResult } from '@polkadot/types/types/'
-import { AccountId } from '@polkadot/types/interfaces'
+import { ISubmittableResult, AnyJson } from '@polkadot/types/types/'
+import { AccountId, EventRecord } from '@polkadot/types/interfaces'
+import { DispatchError, DispatchResult } from '@polkadot/types/interfaces/system'
+import { TypeRegistry } from '@polkadot/types'
 import { KeyringPair } from '@polkadot/keyring/types'
 import Debugger from 'debug'
 import AsyncLock from 'async-lock'
+import { assert } from 'chai'
 
-const debug = Debugger('sender')
+export enum LogLevel {
+  None,
+  Debug,
+  Verbose,
+}
 
 export class Sender {
   private readonly api: ApiPromise
-  private readonly asyncLock: AsyncLock
+  private static readonly asyncLock: AsyncLock = new AsyncLock()
   private readonly keyring: Keyring
+  private readonly debug: Debugger.Debugger
+  private logs: LogLevel = LogLevel.None
+  private static instance = 0
 
-  constructor(api: ApiPromise, keyring: Keyring) {
+  constructor(api: ApiPromise, keyring: Keyring, label: string) {
     this.api = api
-    this.asyncLock = new AsyncLock()
     this.keyring = keyring
+    this.debug = Debugger(`Sender:${Sender.instance++}:${label}`)
   }
 
   // Synchronize all sending of transactions into mempool, so we can always safely read
   // the next account nonce taking mempool into account. This is safe as long as all sending of transactions
-  // from same account occurs in the same process.
-  // Returns a promise that resolves or rejects only after the extrinsic is finalized into a block.
+  // from same account occurs in the same process. Returns a promise of the Extrinsic Dispatch Result ISubmittableResult.
+  // The promise resolves on tx finalization (For both Dispatch success and failure)
+  // The promise is rejected if transaction is rejected by node.
+
+  public setLogLevel(level: LogLevel): void {
+    this.logs = level
+  }
+
   public async signAndSend(
     tx: SubmittableExtrinsic<'promise'>,
-    account: AccountId | string,
-    shouldFail = false
+    account: AccountId | string
   ): Promise<ISubmittableResult> {
     const addr = this.keyring.encodeAddress(account)
     const senderKeyPair: KeyringPair = this.keyring.getPair(addr)
 
-    let finalizedResolve: { (result: ISubmittableResult): void }
-    let finalizedReject: { (err: Error): void }
-    const finalized: Promise<ISubmittableResult> = new Promise(async (resolve, reject) => {
-      finalizedResolve = resolve
-      finalizedReject = reject
+    let finalized: { (result: ISubmittableResult): void }
+    const whenFinalized: Promise<ISubmittableResult> = new Promise(async (resolve, reject) => {
+      finalized = resolve
     })
 
-    const handleEvents = (result: ISubmittableResult) => {
-      if (result.status.isInBlock && result.events !== undefined) {
-        result.events.forEach((event) => {
-          if (event.event.method === 'ExtrinsicFailed') {
-            if (shouldFail) {
-              finalizedResolve(result)
-            } else {
-              finalizedReject(new Error('Extrinsic failed unexpectedly'))
-            }
-          }
-        })
-        finalizedResolve(result)
-      }
+    // saved human representation of the signed tx, will be set before it is submitted.
+    // On error it is logged to help in debugging.
+    let sentTx: AnyJson
 
+    const handleEvents = (result: ISubmittableResult) => {
       if (result.status.isFuture) {
-        // Its virtually impossible for use to continue with tests
+        // Its virtually impossible for us to continue with tests
         // when this occurs and we don't expect the tests to handle this correctly
         // so just abort!
+        console.error('Future Tx, aborting!')
         process.exit(-1)
       }
+
+      if (!result.status.isInBlock) {
+        return
+      }
+
+      const success = result.findRecord('system', 'ExtrinsicSuccess')
+      const failed = result.findRecord('system', 'ExtrinsicFailed')
+
+      // Log failed transactions
+      if (this.logs === LogLevel.Debug || this.logs === LogLevel.Verbose) {
+        if (failed) {
+          const record = failed as EventRecord
+          assert(record)
+          const {
+            event: { data },
+          } = record
+          const err = data[0] as DispatchError
+          if (err.isModule) {
+            const { name } = (this.api.registry as TypeRegistry).findMetaError(err.asModule)
+            this.debug('Dispatch Error:', name, sentTx)
+          } else {
+            this.debug('Dispatch Error:', sentTx)
+          }
+        } else {
+          assert(success)
+          const sudid = result.findRecord('sudo', 'Sudid')
+          if (sudid) {
+            const dispatchResult = sudid.event.data[0] as DispatchResult
+            assert(dispatchResult)
+            if (dispatchResult.isError) {
+              const err = dispatchResult.asError
+              if (err.isModule) {
+                const { name } = (this.api.registry as TypeRegistry).findMetaError(err.asModule)
+                this.debug('Sudo Dispatch Failed', name, sentTx)
+              } else {
+                this.debug('Sudo Dispatch Failed', sentTx)
+              }
+            }
+          }
+        }
+      }
+
+      // Always resolve irrespective of success or failure. Error handling should
+      // be dealt with by caller.
+      if (success || failed) finalized(result)
     }
 
-    await this.asyncLock.acquire(`${senderKeyPair.address}`, async () => {
+    // We used to do this: Sender.asyncLock.acquire(`${senderKeyPair.address}` ...
+    // Instead use a single lock for all calls, to force all transactions to be submitted in same order
+    // of call to signAndSend. Otherwise it raises chance of race conditions.
+    // It happens in rare cases and has lead some tests to fail occasionally in the past
+    await Sender.asyncLock.acquire('tx-queue', async () => {
       const nonce = await this.api.rpc.system.accountNextIndex(senderKeyPair.address)
       const signedTx = tx.sign(senderKeyPair, { nonce })
-      await signedTx.send(handleEvents)
+      sentTx = signedTx.toHuman()
+      const { method, section } = signedTx.method
+      try {
+        await signedTx.send(handleEvents)
+        if (this.logs === LogLevel.Verbose) {
+          this.debug('Submitted tx:', `${section}.${method}`)
+        }
+      } catch (err) {
+        if (this.logs === LogLevel.Debug) {
+          this.debug('Submitting tx failed:', sentTx, err)
+        }
+        throw err
+      }
     })
 
-    return finalized
+    return whenFinalized
   }
 }