Browse Source

Fixtures - move to separate files

Leszek Wiesner 3 years ago
parent
commit
480a8fe3af
38 changed files with 2057 additions and 1892 deletions
  1. 21 0
      tests/integration-tests/src/consts.ts
  2. 106 0
      tests/integration-tests/src/fixtures/membership/AddStakingAccountsHappyCaseFixture.ts
  3. 21 0
      tests/integration-tests/src/fixtures/membership/BaseMembershipFixture.ts
  4. 130 0
      tests/integration-tests/src/fixtures/membership/BuyMembershipHappyCaseFixture.ts
  5. 51 0
      tests/integration-tests/src/fixtures/membership/BuyMembershipWithInsufficienFundsFixture.ts
  6. 133 0
      tests/integration-tests/src/fixtures/membership/InviteMembersHappyCaseFixture.ts
  7. 72 0
      tests/integration-tests/src/fixtures/membership/RemoveStakingAccountsHappyCaseFixture.ts
  8. 162 0
      tests/integration-tests/src/fixtures/membership/SudoUpdateMembershipSystem.ts
  9. 100 0
      tests/integration-tests/src/fixtures/membership/TransferInvitesHappyCaseFixture.ts
  10. 81 0
      tests/integration-tests/src/fixtures/membership/UpdateAccountsHappyCaseFixture.ts
  11. 87 0
      tests/integration-tests/src/fixtures/membership/UpdateProfileHappyCaseFixture.ts
  12. 10 0
      tests/integration-tests/src/fixtures/membership/index.ts
  13. 0 891
      tests/integration-tests/src/fixtures/membershipModule.ts
  14. 166 0
      tests/integration-tests/src/fixtures/workingGroups/ApplyOnOpeningsHappyCaseFixture.ts
  15. 95 0
      tests/integration-tests/src/fixtures/workingGroups/BaseCreateOpeningFixture.ts
  16. 13 0
      tests/integration-tests/src/fixtures/workingGroups/BaseWorkingGroupFixture.ts
  17. 78 0
      tests/integration-tests/src/fixtures/workingGroups/CancelOpeningsFixture.ts
  18. 95 0
      tests/integration-tests/src/fixtures/workingGroups/CreateOpeningsFixture.ts
  19. 114 0
      tests/integration-tests/src/fixtures/workingGroups/CreateUpcomingOpeningsFixture.ts
  20. 192 0
      tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts
  21. 68 0
      tests/integration-tests/src/fixtures/workingGroups/RemoveUpcomingOpeningsFixture.ts
  22. 142 0
      tests/integration-tests/src/fixtures/workingGroups/UpdateGroupStatusFixture.ts
  23. 76 0
      tests/integration-tests/src/fixtures/workingGroups/WithdrawApplicationsFixture.ts
  24. 12 0
      tests/integration-tests/src/fixtures/workingGroups/index.ts
  25. 15 0
      tests/integration-tests/src/fixtures/workingGroups/utils.ts
  26. 0 973
      tests/integration-tests/src/fixtures/workingGroupsModule.ts
  27. 1 4
      tests/integration-tests/src/flows/membership/creatingMemberships.ts
  28. 1 1
      tests/integration-tests/src/flows/membership/invitingMembers.ts
  29. 1 1
      tests/integration-tests/src/flows/membership/managingStakingAccounts.ts
  30. 1 1
      tests/integration-tests/src/flows/membership/membershipSystem.ts
  31. 1 1
      tests/integration-tests/src/flows/membership/transferringInvites.ts
  32. 1 1
      tests/integration-tests/src/flows/membership/updatingAccounts.ts
  33. 1 1
      tests/integration-tests/src/flows/membership/updatingProfile.ts
  34. 2 2
      tests/integration-tests/src/flows/working-groups/groupStatus.ts
  35. 3 3
      tests/integration-tests/src/flows/working-groups/leadOpening.ts
  36. 3 4
      tests/integration-tests/src/flows/working-groups/openingAndApplicationStatus.ts
  37. 2 2
      tests/integration-tests/src/flows/working-groups/upcomingOpenings.ts
  38. 0 7
      tests/integration-tests/src/types.ts

+ 21 - 0
tests/integration-tests/src/consts.ts

@@ -0,0 +1,21 @@
+import BN from 'bn.js'
+import { WorkingGroupModuleName } from './types'
+
+export const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
+export const MIN_APPLICATION_STAKE = new BN(2000)
+export const MIN_USTANKING_PERIOD = 43201
+export const LEADER_OPENING_STAKE = new BN(2000)
+
+export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
+  storageWorkingGroup: '0x0606060606060606',
+  contentDirectoryWorkingGroup: '0x0707070707070707',
+  forumWorkingGroup: '0x0808080808080808',
+  membershipWorkingGroup: '0x0909090909090909',
+}
+
+export const workingGroups: WorkingGroupModuleName[] = [
+  'storageWorkingGroup',
+  'contentDirectoryWorkingGroup',
+  'forumWorkingGroup',
+  'membershipWorkingGroup',
+]

+ 106 - 0
tests/integration-tests/src/fixtures/membership/AddStakingAccountsHappyCaseFixture.ts

@@ -0,0 +1,106 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { MemberContext, EventDetails } from '../../types'
+import {
+  StakingAccountAddedEventFieldsFragment,
+  StakingAccountConfirmedEventFieldsFragment,
+} from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
+
+export class AddStakingAccountsHappyCaseFixture extends BaseMembershipFixture {
+  private memberContext: MemberContext
+  private accounts: string[]
+
+  private addExtrinsics: SubmittableExtrinsic<'promise'>[] = []
+  private confirmExtrinsics: SubmittableExtrinsic<'promise'>[] = []
+  private addEvents: EventDetails[] = []
+  private confirmEvents: EventDetails[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
+    super(api, query)
+    this.memberContext = memberContext
+    this.accounts = accounts
+  }
+
+  private assertQueryNodeAddAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountAddedEventFieldsFragment[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountAddedEvent)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
+  private assertQueryNodeConfirmAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountConfirmedEventFieldsFragment[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountConfirmed)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
+  async execute(): Promise<void> {
+    const { memberContext, accounts } = this
+    this.addExtrinsics = accounts.map(() => this.api.tx.members.addStakingAccountCandidate(memberContext.memberId))
+    this.confirmExtrinsics = accounts.map((a) => this.api.tx.members.confirmStakingAccount(memberContext.memberId, a))
+    const addStakingCandidateFee = await this.api.estimateTxFee(this.addExtrinsics[0], accounts[0])
+    const confirmStakingAccountFee = await this.api.estimateTxFee(this.confirmExtrinsics[0], memberContext.account)
+
+    await this.api.treasuryTransferBalance(memberContext.account, confirmStakingAccountFee.muln(accounts.length))
+    const stakingAccountRequiredBalance = addStakingCandidateFee.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, stakingAccountRequiredBalance)))
+    // Add staking account candidates
+    const addResults = await Promise.all(accounts.map((a, i) => this.api.signAndSend(this.addExtrinsics[i], a)))
+    this.addEvents = await Promise.all(
+      addResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountAdded'))
+    )
+    // Confirm staking accounts
+    const confirmResults = await Promise.all(
+      this.confirmExtrinsics.map((tx) => this.api.signAndSend(tx, memberContext.account))
+    )
+    this.confirmEvents = await Promise.all(
+      confirmResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountConfirmed'))
+    )
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const { memberContext, accounts, addEvents, confirmEvents, addExtrinsics, confirmExtrinsics } = this
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(memberContext.memberId),
+      (qMember) => {
+        if (!qMember) {
+          throw new Error('Query node: Member not found')
+        }
+        assert.isNotEmpty(qMember.boundAccounts)
+        assert.includeMembers(qMember.boundAccounts, accounts)
+      }
+    )
+
+    // Check events
+    const qAddedEvents = await this.query.getStakingAccountAddedEvents(memberContext.memberId)
+    const qConfirmedEvents = await this.query.getStakingAccountConfirmedEvents(memberContext.memberId)
+    accounts.forEach(async (account, i) => {
+      this.assertQueryNodeAddAccountEventIsValid(addEvents[i], account, addExtrinsics[i].hash.toString(), qAddedEvents)
+      this.assertQueryNodeConfirmAccountEventIsValid(
+        confirmEvents[i],
+        account,
+        confirmExtrinsics[i].hash.toString(),
+        qConfirmedEvents
+      )
+    })
+  }
+}

+ 21 - 0
tests/integration-tests/src/fixtures/membership/BaseMembershipFixture.ts

@@ -0,0 +1,21 @@
+import { BaseQueryNodeFixture } from '../../Fixture'
+import { MembershipMetadata } from '@joystream/metadata-protobuf'
+import { CreateInterface, createType } from '@joystream/types'
+import { BuyMembershipParameters } from '@joystream/types/members'
+
+// Common code for Membership fixtures
+// TODO: Refactor to use StandardizedFixture?
+export abstract class BaseMembershipFixture extends BaseQueryNodeFixture {
+  generateParamsFromAccountId(accountId: string): CreateInterface<BuyMembershipParameters> {
+    const metadata = new MembershipMetadata()
+    metadata.setName(`name${accountId.substring(0, 14)}`)
+    metadata.setAbout(`about${accountId.substring(0, 14)}`)
+    // TODO: avatar
+    return {
+      root_account: accountId,
+      controller_account: accountId,
+      handle: `handle${accountId.substring(0, 14)}`,
+      metadata: createType('Bytes', '0x' + Buffer.from(metadata.serializeBinary()).toString('hex')),
+    }
+  }
+}

+ 130 - 0
tests/integration-tests/src/fixtures/membership/BuyMembershipHappyCaseFixture.ts

@@ -0,0 +1,130 @@
+import BN from 'bn.js'
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { MemberId } from '@joystream/types/common'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { Membership } from '@joystream/types/members'
+import { EventType, MembershipEntryMethod } from '../../graphql/generated/schema'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { MembershipMetadata } from '@joystream/metadata-protobuf'
+import { MembershipBoughtEventDetails } from '../../types'
+import { MembershipBoughtEventFieldsFragment, MembershipFieldsFragment } from '../../graphql/generated/queries'
+
+export class BuyMembershipHappyCaseFixture extends BaseMembershipFixture {
+  private accounts: string[]
+  private memberIds: MemberId[] = []
+
+  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
+  private events: MembershipBoughtEventDetails[] = []
+  private members: Membership[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, accounts: string[]) {
+    super(api, query)
+    this.accounts = accounts
+  }
+
+  private generateBuyMembershipTx(accountId: string): SubmittableExtrinsic<'promise'> {
+    return this.api.tx.members.buyMembership(this.generateParamsFromAccountId(accountId))
+  }
+
+  public getCreatedMembers(): MemberId[] {
+    return this.memberIds.slice()
+  }
+
+  private assertMemberMatchQueriedResult(member: Membership, qMember: MembershipFieldsFragment | null) {
+    if (!qMember) {
+      throw new Error('Query node: Membership not found!')
+    }
+    const {
+      handle,
+      rootAccount,
+      controllerAccount,
+      metadata: { name, about },
+      isVerified,
+      entry,
+    } = qMember
+    const txParams = this.generateParamsFromAccountId(rootAccount)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(blake2AsHex(handle), member.handle_hash.toString())
+    assert.equal(handle, txParams.handle)
+    assert.equal(rootAccount, member.root_account.toString())
+    assert.equal(controllerAccount, member.controller_account.toString())
+    assert.equal(name, metadata.getName())
+    assert.equal(about, metadata.getAbout())
+    // TODO: avatar
+    assert.equal(isVerified, false)
+    assert.equal(entry, MembershipEntryMethod.Paid)
+  }
+
+  private assertEventMatchQueriedResult(
+    eventDetails: MembershipBoughtEventDetails,
+    account: string,
+    txHash: string,
+    qEvent: MembershipBoughtEventFieldsFragment | null
+  ) {
+    if (!qEvent) {
+      throw new Error('Query node: MembershipBought event not found!')
+    }
+    const txParams = this.generateParamsFromAccountId(account)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(qEvent.event.inBlock.number, eventDetails.blockNumber)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
+    assert.equal(qEvent.event.type, EventType.MembershipBought)
+    assert.equal(qEvent.newMember.id, eventDetails.memberId.toString())
+    assert.equal(qEvent.handle, txParams.handle)
+    assert.equal(qEvent.rootAccount, txParams.root_account.toString())
+    assert.equal(qEvent.controllerAccount, txParams.controller_account.toString())
+    assert.equal(qEvent.metadata.name, metadata.getName())
+    assert.equal(qEvent.metadata.about, metadata.getAbout())
+    // TODO: avatar
+  }
+
+  async execute(): Promise<void> {
+    // Fee estimation and transfer
+    const membershipFee = await this.api.getMembershipFee()
+    const membershipTransactionFee = await this.api.estimateTxFee(
+      this.generateBuyMembershipTx(this.accounts[0]),
+      this.accounts[0]
+    )
+    const estimatedFee = membershipTransactionFee.add(new BN(membershipFee))
+
+    await this.api.treasuryTransferBalanceToAccounts(this.accounts, estimatedFee)
+
+    this.extrinsics = this.accounts.map((a) => this.generateBuyMembershipTx(a))
+    const results = await Promise.all(this.accounts.map((a, i) => this.api.signAndSend(this.extrinsics[i], a)))
+    this.events = await Promise.all(results.map((r) => this.api.retrieveMembershipBoughtEventDetails(r)))
+    this.memberIds = this.events.map((e) => e.memberId)
+
+    this.debug(`Registered ${this.memberIds.length} new members`)
+
+    assert.equal(this.memberIds.length, this.accounts.length)
+
+    // Assert that created members have expected root and controller accounts
+    this.members = await Promise.all(this.memberIds.map((id) => this.api.query.members.membershipById(id)))
+
+    this.members.forEach((member, index) => {
+      assert(member.root_account.eq(this.accounts[index]))
+      assert(member.controller_account.eq(this.accounts[index]))
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Ensure newly created members were parsed by query node
+    await Promise.all(
+      this.members.map(async (member, i) => {
+        const memberId = this.memberIds[i]
+        await this.query.tryQueryWithTimeout(
+          () => this.query.getMemberById(memberId),
+          (qMember) => this.assertMemberMatchQueriedResult(member, qMember)
+        )
+        // Ensure the query node event is valid
+        const qEvent = await this.query.getMembershipBoughtEvent(memberId)
+        this.assertEventMatchQueriedResult(this.events[i], this.accounts[i], this.extrinsics[i].hash.toString(), qEvent)
+      })
+    )
+  }
+}

+ 51 - 0
tests/integration-tests/src/fixtures/membership/BuyMembershipWithInsufficienFundsFixture.ts

@@ -0,0 +1,51 @@
+import BN from 'bn.js'
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+
+export class BuyMembershipWithInsufficienFundsFixture extends BaseMembershipFixture {
+  private account: string
+
+  public constructor(api: Api, query: QueryNodeApi, account: string) {
+    super(api, query)
+    this.account = account
+  }
+
+  private generateBuyMembershipTx(accountId: string): SubmittableExtrinsic<'promise'> {
+    return this.api.tx.members.buyMembership(this.generateParamsFromAccountId(accountId))
+  }
+
+  async execute(): Promise<void> {
+    // It is acceptable for same account to register a new member account
+    // So no need to assert that account is not already used as a controller or root for another 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()
+    const membershipTransactionFee: BN = await this.api.estimateTxFee(
+      this.generateBuyMembershipTx(this.account),
+      this.account
+    )
+
+    // Only provide enough funds for transaction fee but not enough to cover the membership fee
+    await this.api.treasuryTransferBalance(this.account, membershipTransactionFee)
+
+    const balance = await this.api.getBalance(this.account)
+
+    assert.isBelow(
+      balance.toNumber(),
+      membershipFee.add(membershipTransactionFee).toNumber(),
+      'Account already has sufficient balance to purchase membership'
+    )
+
+    const result = await this.api.signAndSend(this.generateBuyMembershipTx(this.account), this.account)
+
+    this.expectDispatchError(result, 'Buying membership with insufficient funds should fail.')
+
+    // Assert that failure occured for expected reason
+    assert.equal(this.api.getErrorNameFromExtrinsicFailedRecord(result), 'NotEnoughBalanceToBuyMembership')
+  }
+}

+ 133 - 0
tests/integration-tests/src/fixtures/membership/InviteMembersHappyCaseFixture.ts

@@ -0,0 +1,133 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { MemberContext, MemberInvitedEventDetails } from '../../types'
+import { MemberInvitedEventFieldsFragment, MembershipFieldsFragment } from '../../graphql/generated/queries'
+import { EventType, MembershipEntryMethod } from '../../graphql/generated/schema'
+import { MemberId } from '@joystream/types/common'
+import { MembershipMetadata } from '@joystream/metadata-protobuf'
+
+export class InviteMembersHappyCaseFixture extends BaseMembershipFixture {
+  private inviterContext: MemberContext
+  private accounts: string[]
+
+  private initialInvitesCount?: number
+  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
+  private events: MemberInvitedEventDetails[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, inviterContext: MemberContext, accounts: string[]) {
+    super(api, query)
+    this.inviterContext = inviterContext
+    this.accounts = accounts
+  }
+
+  generateInviteMemberTx(memberId: MemberId, inviteeAccountId: string): SubmittableExtrinsic<'promise'> {
+    return this.api.tx.members.inviteMember({
+      ...this.generateParamsFromAccountId(inviteeAccountId),
+      inviting_member_id: memberId,
+    })
+  }
+
+  private assertMemberCorrectlyInvited(account: string, qMember: MembershipFieldsFragment | null) {
+    if (!qMember) {
+      throw new Error('Query node: Membership not found!')
+    }
+    const {
+      handle,
+      rootAccount,
+      controllerAccount,
+      metadata: { name, about },
+      isVerified,
+      entry,
+      invitedBy,
+    } = qMember
+    const txParams = this.generateParamsFromAccountId(account)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(handle, txParams.handle)
+    assert.equal(rootAccount, txParams.root_account)
+    assert.equal(controllerAccount, txParams.controller_account)
+    assert.equal(name, metadata.getName())
+    assert.equal(about, metadata.getAbout())
+    // TODO: avatar
+    assert.equal(isVerified, false)
+    assert.equal(entry, MembershipEntryMethod.Invited)
+    assert.isOk(invitedBy)
+    assert.equal(invitedBy!.id, this.inviterContext.memberId.toString())
+  }
+
+  private aseertQueryNodeEventIsValid(
+    eventDetails: MemberInvitedEventDetails,
+    account: string,
+    txHash: string,
+    qEvent: MemberInvitedEventFieldsFragment | null
+  ) {
+    if (!qEvent) {
+      throw new Error('Query node: MemberInvitedEvent not found!')
+    }
+    const txParams = this.generateParamsFromAccountId(account)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(qEvent.event.inBlock.number, eventDetails.blockNumber)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
+    assert.equal(qEvent.event.type, EventType.MemberInvited)
+    assert.equal(qEvent.newMember.id, eventDetails.newMemberId.toString())
+    assert.equal(qEvent.handle, txParams.handle)
+    assert.equal(qEvent.rootAccount, txParams.root_account)
+    assert.equal(qEvent.controllerAccount, txParams.controller_account)
+    assert.equal(qEvent.metadata.name, metadata.getName())
+    assert.equal(qEvent.metadata.about, metadata.getAbout())
+    // TODO: avatar
+  }
+
+  async execute(): Promise<void> {
+    this.extrinsics = this.accounts.map((a) => this.generateInviteMemberTx(this.inviterContext.memberId, a))
+    const feePerTx = await this.api.estimateTxFee(this.extrinsics[0], this.inviterContext.account)
+    await this.api.treasuryTransferBalance(this.inviterContext.account, feePerTx.muln(this.accounts.length))
+
+    const initialInvitationBalance = await this.api.query.members.initialInvitationBalance()
+    // Top up working group budget to allow funding invited members
+    await this.api.makeSudoCall(
+      this.api.tx.membershipWorkingGroup.setBudget(initialInvitationBalance.muln(this.accounts.length))
+    )
+
+    const { invites } = await this.api.query.members.membershipById(this.inviterContext.memberId)
+    this.initialInvitesCount = invites.toNumber()
+
+    const txResults = await Promise.all(
+      this.extrinsics.map((tx) => this.api.signAndSend(tx, this.inviterContext.account))
+    )
+    this.events = await Promise.all(txResults.map((res) => this.api.retrieveMemberInvitedEventDetails(res)))
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const invitedMembersIds = this.events.map((e) => e.newMemberId)
+    await Promise.all(
+      this.accounts.map(async (account, i) => {
+        const memberId = invitedMembersIds[i]
+        await this.query.tryQueryWithTimeout(
+          () => this.query.getMemberById(memberId),
+          (qMember) => this.assertMemberCorrectlyInvited(account, qMember)
+        )
+        const qEvent = await this.query.getMemberInvitedEvent(memberId)
+        this.aseertQueryNodeEventIsValid(this.events[i], account, this.extrinsics[i].hash.toString(), qEvent)
+      })
+    )
+
+    const qInviter = await this.query.getMemberById(this.inviterContext.memberId)
+    if (!qInviter) {
+      throw new Error('Query node: Inviter member not found!')
+    }
+    const { inviteCount, invitees } = qInviter
+    // Assert that inviteCount was correctly updated
+    assert.equal(inviteCount, this.initialInvitesCount! - this.accounts.length)
+    // Assert that all invited members are part of "invetees" field
+    assert.isNotEmpty(invitees)
+    assert.includeMembers(
+      invitees.map(({ id }) => id),
+      invitedMembersIds.map((id) => id.toString())
+    )
+  }
+}

+ 72 - 0
tests/integration-tests/src/fixtures/membership/RemoveStakingAccountsHappyCaseFixture.ts

@@ -0,0 +1,72 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { EventDetails, MemberContext } from '../../types'
+import { StakingAccountRemovedEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+
+export class RemoveStakingAccountsHappyCaseFixture extends BaseMembershipFixture {
+  private memberContext: MemberContext
+  private accounts: string[]
+
+  private events: EventDetails[] = []
+  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
+    super(api, query)
+    this.memberContext = memberContext
+    this.accounts = accounts
+  }
+
+  private assertQueryNodeRemoveAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountRemovedEventFieldsFragment[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountRemoved)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
+  async execute(): Promise<void> {
+    const { memberContext, accounts } = this
+    this.extrinsics = accounts.map(() => this.api.tx.members.removeStakingAccount(memberContext.memberId))
+
+    const removeStakingAccountFee = await this.api.estimateTxFee(this.extrinsics[0], accounts[0])
+
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, removeStakingAccountFee)))
+    // Remove staking accounts
+    const results = await Promise.all(accounts.map((a, i) => this.api.signAndSend(this.extrinsics[i], a)))
+    this.events = await Promise.all(
+      results.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountRemoved'))
+    )
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const { memberContext, accounts, events, extrinsics } = this
+    // Check member
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(memberContext.memberId),
+      (qMember) => {
+        if (!qMember) {
+          throw new Error('Query node: Membership not found!')
+        }
+        accounts.forEach((a) => assert.notInclude(qMember.boundAccounts, a))
+      }
+    )
+
+    // Check events
+    const qEvents = await this.query.getStakingAccountRemovedEvents(memberContext.memberId)
+    await Promise.all(
+      accounts.map(async (account, i) => {
+        this.assertQueryNodeRemoveAccountEventIsValid(events[i], account, extrinsics[i].hash.toString(), qEvents)
+      })
+    )
+  }
+}

+ 162 - 0
tests/integration-tests/src/fixtures/membership/SudoUpdateMembershipSystem.ts

@@ -0,0 +1,162 @@
+import BN from 'bn.js'
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { AnyQueryNodeEvent, EventDetails, MembershipEventName } from '../../types'
+import { MembershipSystemSnapshotFieldsFragment } from '../../graphql/generated/queries'
+
+type MembershipSystemValues = {
+  referralCut: number
+  defaultInviteCount: number
+  membershipPrice: BN
+  invitedInitialBalance: BN
+}
+
+export class SudoUpdateMembershipSystem extends BaseMembershipFixture {
+  private newValues: Partial<MembershipSystemValues>
+
+  private events: EventDetails[] = []
+  private eventNames: MembershipEventName[] = []
+  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, newValues: Partial<MembershipSystemValues>) {
+    super(api, query)
+    this.newValues = newValues
+  }
+
+  private async getMembershipSystemValuesAt(blockNumber: number): Promise<MembershipSystemValues> {
+    const blockHash = await this.api.getBlockHash(blockNumber)
+    return {
+      referralCut: (await this.api.query.members.referralCut.at(blockHash)).toNumber(),
+      defaultInviteCount: (await this.api.query.members.initialInvitationCount.at(blockHash)).toNumber(),
+      invitedInitialBalance: await this.api.query.members.initialInvitationBalance.at(blockHash),
+      membershipPrice: await this.api.query.members.membershipPrice.at(blockHash),
+    }
+  }
+
+  private async assertBeforeSnapshotIsValid(beforeSnapshot: MembershipSystemSnapshotFieldsFragment) {
+    assert.isNumber(beforeSnapshot.snapshotBlock.number)
+    const chainValues = await this.getMembershipSystemValuesAt(beforeSnapshot.snapshotBlock.number)
+    assert.equal(beforeSnapshot.referralCut, chainValues.referralCut)
+    assert.equal(beforeSnapshot.invitedInitialBalance, chainValues.invitedInitialBalance.toString())
+    assert.equal(beforeSnapshot.membershipPrice, chainValues.membershipPrice.toString())
+    assert.equal(beforeSnapshot.defaultInviteCount, chainValues.defaultInviteCount)
+  }
+
+  private assertAfterSnapshotIsValid(
+    beforeSnapshot: MembershipSystemSnapshotFieldsFragment,
+    afterSnapshot: MembershipSystemSnapshotFieldsFragment
+  ) {
+    const { newValues } = this
+    const expectedValue = (field: keyof MembershipSystemValues) => {
+      const newValue = newValues[field]
+      return newValue === undefined ? beforeSnapshot[field] : newValue instanceof BN ? newValue.toString() : newValue
+    }
+    assert.equal(afterSnapshot.referralCut, expectedValue('referralCut'))
+    assert.equal(afterSnapshot.invitedInitialBalance, expectedValue('invitedInitialBalance'))
+    assert.equal(afterSnapshot.membershipPrice, expectedValue('membershipPrice'))
+    assert.equal(afterSnapshot.defaultInviteCount, expectedValue('defaultInviteCount'))
+  }
+
+  private checkEvent<T extends AnyQueryNodeEvent>(qEvent: T | null, txHash: string): T {
+    if (!qEvent) {
+      throw new Error('Missing query-node event')
+    }
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    return qEvent
+  }
+
+  async execute(): Promise<void> {
+    if (this.newValues.referralCut !== undefined) {
+      this.extrinsics.push(this.api.tx.sudo.sudo(this.api.tx.members.setReferralCut(this.newValues.referralCut)))
+      this.eventNames.push('ReferralCutUpdated')
+    }
+    if (this.newValues.defaultInviteCount !== undefined) {
+      this.extrinsics.push(
+        this.api.tx.sudo.sudo(this.api.tx.members.setInitialInvitationCount(this.newValues.defaultInviteCount))
+      )
+      this.eventNames.push('InitialInvitationCountUpdated')
+    }
+    if (this.newValues.membershipPrice !== undefined) {
+      this.extrinsics.push(
+        this.api.tx.sudo.sudo(this.api.tx.members.setMembershipPrice(this.newValues.membershipPrice))
+      )
+      this.eventNames.push('MembershipPriceUpdated')
+    }
+    if (this.newValues.invitedInitialBalance !== undefined) {
+      this.extrinsics.push(
+        this.api.tx.sudo.sudo(this.api.tx.members.setInitialInvitationBalance(this.newValues.invitedInitialBalance))
+      )
+      this.eventNames.push('InitialInvitationBalanceUpdated')
+    }
+
+    // We don't use api.makeSudoCall, since we cannot(?) then access tx hashes
+    const sudo = await this.api.query.sudo.key()
+    const results = await Promise.all(this.extrinsics.map((tx) => this.api.signAndSend(tx, sudo)))
+    this.events = await Promise.all(
+      results.map((r, i) => this.api.retrieveMembershipEventDetails(r, this.eventNames[i]))
+    )
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const { events, extrinsics, eventNames } = this
+    const afterSnapshotBlockTimestamp = Math.max(...events.map((e) => e.blockTimestamp))
+
+    // Fetch "afterSnapshot" first to make sure query node has progressed enough
+    const afterSnapshot = (await this.query.tryQueryWithTimeout(
+      () => this.query.getMembershipSystemSnapshotAt(afterSnapshotBlockTimestamp),
+      (snapshot) => assert.isOk(snapshot)
+    )) as MembershipSystemSnapshotFieldsFragment
+
+    const beforeSnapshot = await this.query.getMembershipSystemSnapshotBefore(afterSnapshotBlockTimestamp)
+
+    if (!beforeSnapshot) {
+      throw new Error(`Query node: MembershipSystemSnapshot before timestamp ${afterSnapshotBlockTimestamp} not found!`)
+    }
+
+    // Validate snapshots
+    await this.assertBeforeSnapshotIsValid(beforeSnapshot)
+    this.assertAfterSnapshotIsValid(beforeSnapshot, afterSnapshot)
+
+    // Check events
+    await Promise.all(
+      events.map(async (event, i) => {
+        const tx = extrinsics[i]
+        const eventName = eventNames[i]
+        const txHash = tx.hash.toString()
+        const { blockNumber, indexInBlock } = event
+        if (eventName === 'ReferralCutUpdated') {
+          const { newValue } = this.checkEvent(
+            await this.query.getReferralCutUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newValue, this.newValues.referralCut)
+        }
+        if (eventName === 'MembershipPriceUpdated') {
+          const { newPrice } = this.checkEvent(
+            await this.query.getMembershipPriceUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newPrice, this.newValues.membershipPrice!.toString())
+        }
+        if (eventName === 'InitialInvitationBalanceUpdated') {
+          const { newInitialBalance } = this.checkEvent(
+            await this.query.getInitialInvitationBalanceUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newInitialBalance, this.newValues.invitedInitialBalance!.toString())
+        }
+        if (eventName === 'InitialInvitationCountUpdated') {
+          const { newInitialInvitationCount } = this.checkEvent(
+            await this.query.getInitialInvitationCountUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newInitialInvitationCount, this.newValues.defaultInviteCount)
+        }
+      })
+    )
+  }
+}

+ 100 - 0
tests/integration-tests/src/fixtures/membership/TransferInvitesHappyCaseFixture.ts

@@ -0,0 +1,100 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { MemberContext, EventDetails } from '../../types'
+import { InvitesTransferredEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { Membership } from '@joystream/types/members'
+
+export class TransferInvitesHappyCaseFixture extends BaseMembershipFixture {
+  private fromContext: MemberContext
+  private toContext: MemberContext
+  private invitesToTransfer: number
+
+  private fromMemberInitialInvites?: number
+  private toMemberInitialInvites?: number
+  private event?: EventDetails
+  private tx?: SubmittableExtrinsic<'promise'>
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    fromContext: MemberContext,
+    toContext: MemberContext,
+    invitesToTransfer = 2
+  ) {
+    super(api, query)
+    this.fromContext = fromContext
+    this.toContext = toContext
+    this.invitesToTransfer = invitesToTransfer
+  }
+
+  private assertQueryNodeEventIsValid(
+    eventDetails: EventDetails,
+    txHash: string,
+    qEvent: InvitesTransferredEventFieldsFragment | null
+  ) {
+    if (!qEvent) {
+      throw new Error('Query node: InvitesTransferredEvent not found!')
+    }
+    const {
+      event: { inExtrinsic, type },
+      sourceMember,
+      targetMember,
+      numberOfInvites,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.InvitesTransferred)
+    assert.equal(sourceMember.id, this.fromContext.memberId.toString())
+    assert.equal(targetMember.id, this.toContext.memberId.toString())
+    assert.equal(numberOfInvites, this.invitesToTransfer)
+  }
+
+  async execute(): Promise<void> {
+    const { fromContext, toContext, invitesToTransfer } = this
+    this.tx = this.api.tx.members.transferInvites(fromContext.memberId, toContext.memberId, invitesToTransfer)
+    const txFee = await this.api.estimateTxFee(this.tx, fromContext.account)
+    await this.api.treasuryTransferBalance(fromContext.account, txFee)
+
+    const [fromMember, toMember] = await this.api.query.members.membershipById.multi<Membership>([
+      fromContext.memberId,
+      toContext.memberId,
+    ])
+
+    this.fromMemberInitialInvites = fromMember.invites.toNumber()
+    this.toMemberInitialInvites = toMember.invites.toNumber()
+
+    // Send transfer invites extrinsic
+    const txRes = await this.api.signAndSend(this.tx, fromContext.account)
+    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'InvitesTransferred')
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const { fromContext, toContext, invitesToTransfer } = this
+    // Check "from" member
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(fromContext.memberId),
+      (qSourceMember) => {
+        if (!qSourceMember) {
+          throw new Error('Query node: Source member not found')
+        }
+        assert.equal(qSourceMember.inviteCount, this.fromMemberInitialInvites! - invitesToTransfer)
+      }
+    )
+
+    // Check "to" member
+    const qTargetMember = await this.query.getMemberById(toContext.memberId)
+    if (!qTargetMember) {
+      throw new Error('Query node: Target member not found')
+    }
+    assert.equal(qTargetMember.inviteCount, this.toMemberInitialInvites! + invitesToTransfer)
+
+    // Check event
+    const qEvent = await this.query.getInvitesTransferredEvent(fromContext.memberId)
+
+    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvent)
+  }
+}

+ 81 - 0
tests/integration-tests/src/fixtures/membership/UpdateAccountsHappyCaseFixture.ts

@@ -0,0 +1,81 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { EventDetails, MemberContext } from '../../types'
+import { MemberAccountsUpdatedEventFieldsFragment, MembershipFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+
+export class UpdateAccountsHappyCaseFixture extends BaseMembershipFixture {
+  private memberContext: MemberContext
+  // Update data
+  private newRootAccount: string
+  private newControllerAccount: string
+
+  private tx?: SubmittableExtrinsic<'promise'>
+  private event?: EventDetails
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    memberContext: MemberContext,
+    newRootAccount: string,
+    newControllerAccount: string
+  ) {
+    super(api, query)
+    this.memberContext = memberContext
+    this.newRootAccount = newRootAccount
+    this.newControllerAccount = newControllerAccount
+  }
+
+  private assertAccountsUpdateSuccesful(qMember: MembershipFieldsFragment | null) {
+    if (!qMember) {
+      throw new Error('Query node: Membership not found!')
+    }
+    const { rootAccount, controllerAccount } = qMember
+    assert.equal(rootAccount, this.newRootAccount)
+    assert.equal(controllerAccount, this.newControllerAccount)
+  }
+
+  private assertQueryNodeEventIsValid(
+    eventDetails: EventDetails,
+    txHash: string,
+    qEvents: MemberAccountsUpdatedEventFieldsFragment[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    const {
+      event: { inExtrinsic, type },
+      member: { id: memberId },
+      newControllerAccount,
+      newRootAccount,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.MemberAccountsUpdated)
+    assert.equal(memberId, this.memberContext.memberId.toString())
+    assert.equal(newControllerAccount, this.newControllerAccount)
+    assert.equal(newRootAccount, this.newRootAccount)
+  }
+
+  async execute(): Promise<void> {
+    this.tx = this.api.tx.members.updateAccounts(
+      this.memberContext.memberId,
+      this.newRootAccount,
+      this.newControllerAccount
+    )
+    const txFee = await this.api.estimateTxFee(this.tx, this.memberContext.account)
+    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
+    const txRes = await this.api.signAndSend(this.tx, this.memberContext.account)
+    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'MemberAccountsUpdated')
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(this.memberContext.memberId),
+      (qMember) => this.assertAccountsUpdateSuccesful(qMember)
+    )
+    const qEvents = await this.query.getMemberAccountsUpdatedEvents(this.memberContext.memberId)
+    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvents)
+  }
+}

+ 87 - 0
tests/integration-tests/src/fixtures/membership/UpdateProfileHappyCaseFixture.ts

@@ -0,0 +1,87 @@
+import { Api } from '../../Api'
+import { assert } from 'chai'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { BaseMembershipFixture } from './BaseMembershipFixture'
+import { MemberContext, EventDetails } from '../../types'
+import { MembershipFieldsFragment, MemberProfileUpdatedEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { MembershipMetadata } from '@joystream/metadata-protobuf'
+
+// TODO: Add partial update to make sure it works too
+export class UpdateProfileHappyCaseFixture extends BaseMembershipFixture {
+  private memberContext: MemberContext
+  // Update data
+  private newName = 'New name'
+  private newHandle = 'New handle'
+  private newAbout = 'New about'
+
+  private event?: EventDetails
+  private tx?: SubmittableExtrinsic<'promise'>
+
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext) {
+    super(api, query)
+    this.memberContext = memberContext
+  }
+
+  private assertProfileUpdateSuccesful(qMember: MembershipFieldsFragment | null) {
+    if (!qMember) {
+      throw new Error('Query node: Membership not found!')
+    }
+    const {
+      handle,
+      metadata: { name, about },
+    } = qMember
+    assert.equal(name, this.newName)
+    assert.equal(handle, this.newHandle)
+    // TODO: avatar
+    assert.equal(about, this.newAbout)
+  }
+
+  private assertQueryNodeEventIsValid(
+    eventDetails: EventDetails,
+    txHash: string,
+    qEvents: MemberProfileUpdatedEventFieldsFragment[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    const {
+      event: { inExtrinsic, type },
+      member: { id: memberId },
+      newHandle,
+      newMetadata,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.MemberProfileUpdated)
+    assert.equal(memberId, this.memberContext.memberId.toString())
+    assert.equal(newHandle, this.newHandle)
+    assert.equal(newMetadata.name, this.newName)
+    assert.equal(newMetadata.about, this.newAbout)
+    // TODO: avatar
+  }
+
+  async execute(): Promise<void> {
+    const metadata = new MembershipMetadata()
+    metadata.setName(this.newName)
+    metadata.setAbout(this.newAbout)
+    // TODO: avatar
+    this.tx = this.api.tx.members.updateProfile(
+      this.memberContext.memberId,
+      this.newHandle,
+      '0x' + Buffer.from(metadata.serializeBinary()).toString('hex')
+    )
+    const txFee = await this.api.estimateTxFee(this.tx, this.memberContext.account)
+    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
+    const txRes = await this.api.signAndSend(this.tx, this.memberContext.account)
+    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'MemberProfileUpdated')
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(this.memberContext.memberId),
+      (qMember) => this.assertProfileUpdateSuccesful(qMember)
+    )
+    const qEvents = await this.query.getMemberProfileUpdatedEvents(this.memberContext.memberId)
+    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvents)
+  }
+}

+ 10 - 0
tests/integration-tests/src/fixtures/membership/index.ts

@@ -0,0 +1,10 @@
+export { AddStakingAccountsHappyCaseFixture } from './AddStakingAccountsHappyCaseFixture'
+export { BaseMembershipFixture } from './BaseMembershipFixture'
+export { BuyMembershipHappyCaseFixture } from './BuyMembershipHappyCaseFixture'
+export { BuyMembershipWithInsufficienFundsFixture } from './BuyMembershipWithInsufficienFundsFixture'
+export { InviteMembersHappyCaseFixture } from './InviteMembersHappyCaseFixture'
+export { RemoveStakingAccountsHappyCaseFixture } from './RemoveStakingAccountsHappyCaseFixture'
+export { SudoUpdateMembershipSystem } from './SudoUpdateMembershipSystem'
+export { TransferInvitesHappyCaseFixture } from './TransferInvitesHappyCaseFixture'
+export { UpdateAccountsHappyCaseFixture } from './UpdateAccountsHappyCaseFixture'
+export { UpdateProfileHappyCaseFixture } from './UpdateProfileHappyCaseFixture'

+ 0 - 891
tests/integration-tests/src/fixtures/membershipModule.ts

@@ -1,891 +0,0 @@
-import { Api } from '../Api'
-import BN from 'bn.js'
-import { assert } from 'chai'
-import { BaseQueryNodeFixture } from '../Fixture'
-import { MemberId } from '@joystream/types/common'
-import { QueryNodeApi } from '../QueryNodeApi'
-import { BuyMembershipParameters, Membership } from '@joystream/types/members'
-import { EventType, MembershipEntryMethod } from '../graphql/generated/schema'
-import { blake2AsHex } from '@polkadot/util-crypto'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { CreateInterface, createType } from '@joystream/types'
-import { MembershipMetadata } from '@joystream/metadata-protobuf'
-import {
-  MemberContext,
-  AnyQueryNodeEvent,
-  EventDetails,
-  MemberInvitedEventDetails,
-  MembershipBoughtEventDetails,
-  MembershipEventName,
-} from '../types'
-import {
-  InvitesTransferredEventFieldsFragment,
-  MemberAccountsUpdatedEventFieldsFragment,
-  MemberInvitedEventFieldsFragment,
-  MemberProfileUpdatedEventFieldsFragment,
-  MembershipBoughtEventFieldsFragment,
-  MembershipFieldsFragment,
-  MembershipSystemSnapshotFieldsFragment,
-  StakingAccountAddedEventFieldsFragment,
-  StakingAccountConfirmedEventFieldsFragment,
-  StakingAccountRemovedEventFieldsFragment,
-} from '../graphql/generated/queries'
-
-// FIXME: Retrieve from runtime when possible!
-const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
-
-// common code for fixtures
-// TODO: Refactor to use StandardizedFixture?
-abstract class BaseMembershipFixture extends BaseQueryNodeFixture {
-  generateParamsFromAccountId(accountId: string): CreateInterface<BuyMembershipParameters> {
-    const metadata = new MembershipMetadata()
-    metadata.setName(`name${accountId.substring(0, 14)}`)
-    metadata.setAbout(`about${accountId.substring(0, 14)}`)
-    // TODO: avatar
-    return {
-      root_account: accountId,
-      controller_account: accountId,
-      handle: `handle${accountId.substring(0, 14)}`,
-      metadata: createType('Bytes', '0x' + Buffer.from(metadata.serializeBinary()).toString('hex')),
-    }
-  }
-}
-
-export class BuyMembershipHappyCaseFixture extends BaseMembershipFixture {
-  private accounts: string[]
-  private memberIds: MemberId[] = []
-
-  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private events: MembershipBoughtEventDetails[] = []
-  private members: Membership[] = []
-
-  public constructor(api: Api, query: QueryNodeApi, accounts: string[]) {
-    super(api, query)
-    this.accounts = accounts
-  }
-
-  private generateBuyMembershipTx(accountId: string): SubmittableExtrinsic<'promise'> {
-    return this.api.tx.members.buyMembership(this.generateParamsFromAccountId(accountId))
-  }
-
-  public getCreatedMembers(): MemberId[] {
-    return this.memberIds.slice()
-  }
-
-  private assertMemberMatchQueriedResult(member: Membership, qMember: MembershipFieldsFragment | null) {
-    if (!qMember) {
-      throw new Error('Query node: Membership not found!')
-    }
-    const {
-      handle,
-      rootAccount,
-      controllerAccount,
-      metadata: { name, about },
-      isVerified,
-      entry,
-    } = qMember
-    const txParams = this.generateParamsFromAccountId(rootAccount)
-    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
-    assert.equal(blake2AsHex(handle), member.handle_hash.toString())
-    assert.equal(handle, txParams.handle)
-    assert.equal(rootAccount, member.root_account.toString())
-    assert.equal(controllerAccount, member.controller_account.toString())
-    assert.equal(name, metadata.getName())
-    assert.equal(about, metadata.getAbout())
-    // TODO: avatar
-    assert.equal(isVerified, false)
-    assert.equal(entry, MembershipEntryMethod.Paid)
-  }
-
-  private assertEventMatchQueriedResult(
-    eventDetails: MembershipBoughtEventDetails,
-    account: string,
-    txHash: string,
-    qEvent: MembershipBoughtEventFieldsFragment | null
-  ) {
-    if (!qEvent) {
-      throw new Error('Query node: MembershipBought event not found!')
-    }
-    const txParams = this.generateParamsFromAccountId(account)
-    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
-    assert.equal(qEvent.event.inBlock.number, eventDetails.blockNumber)
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
-    assert.equal(qEvent.event.type, EventType.MembershipBought)
-    assert.equal(qEvent.newMember.id, eventDetails.memberId.toString())
-    assert.equal(qEvent.handle, txParams.handle)
-    assert.equal(qEvent.rootAccount, txParams.root_account.toString())
-    assert.equal(qEvent.controllerAccount, txParams.controller_account.toString())
-    assert.equal(qEvent.metadata.name, metadata.getName())
-    assert.equal(qEvent.metadata.about, metadata.getAbout())
-    // TODO: avatar
-  }
-
-  async execute(): Promise<void> {
-    // Fee estimation and transfer
-    const membershipFee = await this.api.getMembershipFee()
-    const membershipTransactionFee = await this.api.estimateTxFee(
-      this.generateBuyMembershipTx(this.accounts[0]),
-      this.accounts[0]
-    )
-    const estimatedFee = membershipTransactionFee.add(new BN(membershipFee))
-
-    await this.api.treasuryTransferBalanceToAccounts(this.accounts, estimatedFee)
-
-    this.extrinsics = this.accounts.map((a) => this.generateBuyMembershipTx(a))
-    const results = await Promise.all(this.accounts.map((a, i) => this.api.signAndSend(this.extrinsics[i], a)))
-    this.events = await Promise.all(results.map((r) => this.api.retrieveMembershipBoughtEventDetails(r)))
-    this.memberIds = this.events.map((e) => e.memberId)
-
-    this.debug(`Registered ${this.memberIds.length} new members`)
-
-    assert.equal(this.memberIds.length, this.accounts.length)
-
-    // Assert that created members have expected root and controller accounts
-    this.members = await Promise.all(this.memberIds.map((id) => this.api.query.members.membershipById(id)))
-
-    this.members.forEach((member, index) => {
-      assert(member.root_account.eq(this.accounts[index]))
-      assert(member.controller_account.eq(this.accounts[index]))
-    })
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Ensure newly created members were parsed by query node
-    await Promise.all(
-      this.members.map(async (member, i) => {
-        const memberId = this.memberIds[i]
-        await this.query.tryQueryWithTimeout(
-          () => this.query.getMemberById(memberId),
-          (qMember) => this.assertMemberMatchQueriedResult(member, qMember)
-        )
-        // Ensure the query node event is valid
-        const qEvent = await this.query.getMembershipBoughtEvent(memberId)
-        this.assertEventMatchQueriedResult(this.events[i], this.accounts[i], this.extrinsics[i].hash.toString(), qEvent)
-      })
-    )
-  }
-}
-
-export class BuyMembershipWithInsufficienFundsFixture extends BaseMembershipFixture {
-  private account: string
-
-  public constructor(api: Api, query: QueryNodeApi, account: string) {
-    super(api, query)
-    this.account = account
-  }
-
-  private generateBuyMembershipTx(accountId: string): SubmittableExtrinsic<'promise'> {
-    return this.api.tx.members.buyMembership(this.generateParamsFromAccountId(accountId))
-  }
-
-  async execute(): Promise<void> {
-    // It is acceptable for same account to register a new member account
-    // So no need to assert that account is not already used as a controller or root for another 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()
-    const membershipTransactionFee: BN = await this.api.estimateTxFee(
-      this.generateBuyMembershipTx(this.account),
-      this.account
-    )
-
-    // Only provide enough funds for transaction fee but not enough to cover the membership fee
-    await this.api.treasuryTransferBalance(this.account, membershipTransactionFee)
-
-    const balance = await this.api.getBalance(this.account)
-
-    assert.isBelow(
-      balance.toNumber(),
-      membershipFee.add(membershipTransactionFee).toNumber(),
-      'Account already has sufficient balance to purchase membership'
-    )
-
-    const result = await this.api.signAndSend(this.generateBuyMembershipTx(this.account), this.account)
-
-    this.expectDispatchError(result, 'Buying membership with insufficient funds should fail.')
-
-    // Assert that failure occured for expected reason
-    assert.equal(this.api.getErrorNameFromExtrinsicFailedRecord(result), 'NotEnoughBalanceToBuyMembership')
-  }
-}
-
-// TODO: Add partial update to make sure it works too
-export class UpdateProfileHappyCaseFixture extends BaseMembershipFixture {
-  private memberContext: MemberContext
-  // Update data
-  private newName = 'New name'
-  private newHandle = 'New handle'
-  private newAbout = 'New about'
-
-  private event?: EventDetails
-  private tx?: SubmittableExtrinsic<'promise'>
-
-  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext) {
-    super(api, query)
-    this.memberContext = memberContext
-  }
-
-  private assertProfileUpdateSuccesful(qMember: MembershipFieldsFragment | null) {
-    if (!qMember) {
-      throw new Error('Query node: Membership not found!')
-    }
-    const {
-      handle,
-      metadata: { name, about },
-    } = qMember
-    assert.equal(name, this.newName)
-    assert.equal(handle, this.newHandle)
-    // TODO: avatar
-    assert.equal(about, this.newAbout)
-  }
-
-  private assertQueryNodeEventIsValid(
-    eventDetails: EventDetails,
-    txHash: string,
-    qEvents: MemberProfileUpdatedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    const {
-      event: { inExtrinsic, type },
-      member: { id: memberId },
-      newHandle,
-      newMetadata,
-    } = qEvent
-    assert.equal(inExtrinsic, txHash)
-    assert.equal(type, EventType.MemberProfileUpdated)
-    assert.equal(memberId, this.memberContext.memberId.toString())
-    assert.equal(newHandle, this.newHandle)
-    assert.equal(newMetadata.name, this.newName)
-    assert.equal(newMetadata.about, this.newAbout)
-    // TODO: avatar
-  }
-
-  async execute(): Promise<void> {
-    const metadata = new MembershipMetadata()
-    metadata.setName(this.newName)
-    metadata.setAbout(this.newAbout)
-    // TODO: avatar
-    this.tx = this.api.tx.members.updateProfile(
-      this.memberContext.memberId,
-      this.newHandle,
-      '0x' + Buffer.from(metadata.serializeBinary()).toString('hex')
-    )
-    const txFee = await this.api.estimateTxFee(this.tx, this.memberContext.account)
-    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
-    const txRes = await this.api.signAndSend(this.tx, this.memberContext.account)
-    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'MemberProfileUpdated')
-  }
-
-  async runQueryNodeChecks() {
-    await super.runQueryNodeChecks()
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(this.memberContext.memberId),
-      (qMember) => this.assertProfileUpdateSuccesful(qMember)
-    )
-    const qEvents = await this.query.getMemberProfileUpdatedEvents(this.memberContext.memberId)
-    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvents)
-  }
-}
-
-export class UpdateAccountsHappyCaseFixture extends BaseMembershipFixture {
-  private memberContext: MemberContext
-  // Update data
-  private newRootAccount: string
-  private newControllerAccount: string
-
-  private tx?: SubmittableExtrinsic<'promise'>
-  private event?: EventDetails
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    memberContext: MemberContext,
-    newRootAccount: string,
-    newControllerAccount: string
-  ) {
-    super(api, query)
-    this.memberContext = memberContext
-    this.newRootAccount = newRootAccount
-    this.newControllerAccount = newControllerAccount
-  }
-
-  private assertAccountsUpdateSuccesful(qMember: MembershipFieldsFragment | null) {
-    if (!qMember) {
-      throw new Error('Query node: Membership not found!')
-    }
-    const { rootAccount, controllerAccount } = qMember
-    assert.equal(rootAccount, this.newRootAccount)
-    assert.equal(controllerAccount, this.newControllerAccount)
-  }
-
-  private assertQueryNodeEventIsValid(
-    eventDetails: EventDetails,
-    txHash: string,
-    qEvents: MemberAccountsUpdatedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    const {
-      event: { inExtrinsic, type },
-      member: { id: memberId },
-      newControllerAccount,
-      newRootAccount,
-    } = qEvent
-    assert.equal(inExtrinsic, txHash)
-    assert.equal(type, EventType.MemberAccountsUpdated)
-    assert.equal(memberId, this.memberContext.memberId.toString())
-    assert.equal(newControllerAccount, this.newControllerAccount)
-    assert.equal(newRootAccount, this.newRootAccount)
-  }
-
-  async execute(): Promise<void> {
-    this.tx = this.api.tx.members.updateAccounts(
-      this.memberContext.memberId,
-      this.newRootAccount,
-      this.newControllerAccount
-    )
-    const txFee = await this.api.estimateTxFee(this.tx, this.memberContext.account)
-    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
-    const txRes = await this.api.signAndSend(this.tx, this.memberContext.account)
-    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'MemberAccountsUpdated')
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(this.memberContext.memberId),
-      (qMember) => this.assertAccountsUpdateSuccesful(qMember)
-    )
-    const qEvents = await this.query.getMemberAccountsUpdatedEvents(this.memberContext.memberId)
-    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvents)
-  }
-}
-
-export class InviteMembersHappyCaseFixture extends BaseMembershipFixture {
-  private inviterContext: MemberContext
-  private accounts: string[]
-
-  private initialInvitesCount?: number
-  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private events: MemberInvitedEventDetails[] = []
-
-  public constructor(api: Api, query: QueryNodeApi, inviterContext: MemberContext, accounts: string[]) {
-    super(api, query)
-    this.inviterContext = inviterContext
-    this.accounts = accounts
-  }
-
-  generateInviteMemberTx(memberId: MemberId, inviteeAccountId: string): SubmittableExtrinsic<'promise'> {
-    return this.api.tx.members.inviteMember({
-      ...this.generateParamsFromAccountId(inviteeAccountId),
-      inviting_member_id: memberId,
-    })
-  }
-
-  private assertMemberCorrectlyInvited(account: string, qMember: MembershipFieldsFragment | null) {
-    if (!qMember) {
-      throw new Error('Query node: Membership not found!')
-    }
-    const {
-      handle,
-      rootAccount,
-      controllerAccount,
-      metadata: { name, about },
-      isVerified,
-      entry,
-      invitedBy,
-    } = qMember
-    const txParams = this.generateParamsFromAccountId(account)
-    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
-    assert.equal(handle, txParams.handle)
-    assert.equal(rootAccount, txParams.root_account)
-    assert.equal(controllerAccount, txParams.controller_account)
-    assert.equal(name, metadata.getName())
-    assert.equal(about, metadata.getAbout())
-    // TODO: avatar
-    assert.equal(isVerified, false)
-    assert.equal(entry, MembershipEntryMethod.Invited)
-    assert.isOk(invitedBy)
-    assert.equal(invitedBy!.id, this.inviterContext.memberId.toString())
-  }
-
-  private aseertQueryNodeEventIsValid(
-    eventDetails: MemberInvitedEventDetails,
-    account: string,
-    txHash: string,
-    qEvent: MemberInvitedEventFieldsFragment | null
-  ) {
-    if (!qEvent) {
-      throw new Error('Query node: MemberInvitedEvent not found!')
-    }
-    const txParams = this.generateParamsFromAccountId(account)
-    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
-    assert.equal(qEvent.event.inBlock.number, eventDetails.blockNumber)
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
-    assert.equal(qEvent.event.type, EventType.MemberInvited)
-    assert.equal(qEvent.newMember.id, eventDetails.newMemberId.toString())
-    assert.equal(qEvent.handle, txParams.handle)
-    assert.equal(qEvent.rootAccount, txParams.root_account)
-    assert.equal(qEvent.controllerAccount, txParams.controller_account)
-    assert.equal(qEvent.metadata.name, metadata.getName())
-    assert.equal(qEvent.metadata.about, metadata.getAbout())
-    // TODO: avatar
-  }
-
-  async execute(): Promise<void> {
-    this.extrinsics = this.accounts.map((a) => this.generateInviteMemberTx(this.inviterContext.memberId, a))
-    const feePerTx = await this.api.estimateTxFee(this.extrinsics[0], this.inviterContext.account)
-    await this.api.treasuryTransferBalance(this.inviterContext.account, feePerTx.muln(this.accounts.length))
-
-    const initialInvitationBalance = await this.api.query.members.initialInvitationBalance()
-    // Top up working group budget to allow funding invited members
-    await this.api.makeSudoCall(
-      this.api.tx.membershipWorkingGroup.setBudget(initialInvitationBalance.muln(this.accounts.length))
-    )
-
-    const { invites } = await this.api.query.members.membershipById(this.inviterContext.memberId)
-    this.initialInvitesCount = invites.toNumber()
-
-    const txResults = await Promise.all(
-      this.extrinsics.map((tx) => this.api.signAndSend(tx, this.inviterContext.account))
-    )
-    this.events = await Promise.all(txResults.map((res) => this.api.retrieveMemberInvitedEventDetails(res)))
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    const invitedMembersIds = this.events.map((e) => e.newMemberId)
-    await Promise.all(
-      this.accounts.map(async (account, i) => {
-        const memberId = invitedMembersIds[i]
-        await this.query.tryQueryWithTimeout(
-          () => this.query.getMemberById(memberId),
-          (qMember) => this.assertMemberCorrectlyInvited(account, qMember)
-        )
-        const qEvent = await this.query.getMemberInvitedEvent(memberId)
-        this.aseertQueryNodeEventIsValid(this.events[i], account, this.extrinsics[i].hash.toString(), qEvent)
-      })
-    )
-
-    const qInviter = await this.query.getMemberById(this.inviterContext.memberId)
-    if (!qInviter) {
-      throw new Error('Query node: Inviter member not found!')
-    }
-    const { inviteCount, invitees } = qInviter
-    // Assert that inviteCount was correctly updated
-    assert.equal(inviteCount, this.initialInvitesCount! - this.accounts.length)
-    // Assert that all invited members are part of "invetees" field
-    assert.isNotEmpty(invitees)
-    assert.includeMembers(
-      invitees.map(({ id }) => id),
-      invitedMembersIds.map((id) => id.toString())
-    )
-  }
-}
-
-export class TransferInvitesHappyCaseFixture extends BaseMembershipFixture {
-  private fromContext: MemberContext
-  private toContext: MemberContext
-  private invitesToTransfer: number
-
-  private fromMemberInitialInvites?: number
-  private toMemberInitialInvites?: number
-  private event?: EventDetails
-  private tx?: SubmittableExtrinsic<'promise'>
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    fromContext: MemberContext,
-    toContext: MemberContext,
-    invitesToTransfer = 2
-  ) {
-    super(api, query)
-    this.fromContext = fromContext
-    this.toContext = toContext
-    this.invitesToTransfer = invitesToTransfer
-  }
-
-  private assertQueryNodeEventIsValid(
-    eventDetails: EventDetails,
-    txHash: string,
-    qEvent: InvitesTransferredEventFieldsFragment | null
-  ) {
-    if (!qEvent) {
-      throw new Error('Query node: InvitesTransferredEvent not found!')
-    }
-    const {
-      event: { inExtrinsic, type },
-      sourceMember,
-      targetMember,
-      numberOfInvites,
-    } = qEvent
-    assert.equal(inExtrinsic, txHash)
-    assert.equal(type, EventType.InvitesTransferred)
-    assert.equal(sourceMember.id, this.fromContext.memberId.toString())
-    assert.equal(targetMember.id, this.toContext.memberId.toString())
-    assert.equal(numberOfInvites, this.invitesToTransfer)
-  }
-
-  async execute(): Promise<void> {
-    const { fromContext, toContext, invitesToTransfer } = this
-    this.tx = this.api.tx.members.transferInvites(fromContext.memberId, toContext.memberId, invitesToTransfer)
-    const txFee = await this.api.estimateTxFee(this.tx, fromContext.account)
-    await this.api.treasuryTransferBalance(fromContext.account, txFee)
-
-    const [fromMember, toMember] = await this.api.query.members.membershipById.multi<Membership>([
-      fromContext.memberId,
-      toContext.memberId,
-    ])
-
-    this.fromMemberInitialInvites = fromMember.invites.toNumber()
-    this.toMemberInitialInvites = toMember.invites.toNumber()
-
-    // Send transfer invites extrinsic
-    const txRes = await this.api.signAndSend(this.tx, fromContext.account)
-    this.event = await this.api.retrieveMembershipEventDetails(txRes, 'InvitesTransferred')
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    const { fromContext, toContext, invitesToTransfer } = this
-    // Check "from" member
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(fromContext.memberId),
-      (qSourceMember) => {
-        if (!qSourceMember) {
-          throw new Error('Query node: Source member not found')
-        }
-        assert.equal(qSourceMember.inviteCount, this.fromMemberInitialInvites! - invitesToTransfer)
-      }
-    )
-
-    // Check "to" member
-    const qTargetMember = await this.query.getMemberById(toContext.memberId)
-    if (!qTargetMember) {
-      throw new Error('Query node: Target member not found')
-    }
-    assert.equal(qTargetMember.inviteCount, this.toMemberInitialInvites! + invitesToTransfer)
-
-    // Check event
-    const qEvent = await this.query.getInvitesTransferredEvent(fromContext.memberId)
-
-    this.assertQueryNodeEventIsValid(this.event!, this.tx!.hash.toString(), qEvent)
-  }
-}
-
-export class AddStakingAccountsHappyCaseFixture extends BaseMembershipFixture {
-  private memberContext: MemberContext
-  private accounts: string[]
-
-  private addExtrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private confirmExtrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private addEvents: EventDetails[] = []
-  private confirmEvents: EventDetails[] = []
-
-  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
-    super(api, query)
-    this.memberContext = memberContext
-    this.accounts = accounts
-  }
-
-  private assertQueryNodeAddAccountEventIsValid(
-    eventDetails: EventDetails,
-    account: string,
-    txHash: string,
-    qEvents: StakingAccountAddedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    assert.equal(qEvent.event.type, EventType.StakingAccountAddedEvent)
-    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
-    assert.equal(qEvent.account, account)
-  }
-
-  private assertQueryNodeConfirmAccountEventIsValid(
-    eventDetails: EventDetails,
-    account: string,
-    txHash: string,
-    qEvents: StakingAccountConfirmedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    assert.equal(qEvent.event.type, EventType.StakingAccountConfirmed)
-    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
-    assert.equal(qEvent.account, account)
-  }
-
-  async execute(): Promise<void> {
-    const { memberContext, accounts } = this
-    this.addExtrinsics = accounts.map(() => this.api.tx.members.addStakingAccountCandidate(memberContext.memberId))
-    this.confirmExtrinsics = accounts.map((a) => this.api.tx.members.confirmStakingAccount(memberContext.memberId, a))
-    const addStakingCandidateFee = await this.api.estimateTxFee(this.addExtrinsics[0], accounts[0])
-    const confirmStakingAccountFee = await this.api.estimateTxFee(this.confirmExtrinsics[0], memberContext.account)
-
-    await this.api.treasuryTransferBalance(memberContext.account, confirmStakingAccountFee.muln(accounts.length))
-    const stakingAccountRequiredBalance = addStakingCandidateFee.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
-    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, stakingAccountRequiredBalance)))
-    // Add staking account candidates
-    const addResults = await Promise.all(accounts.map((a, i) => this.api.signAndSend(this.addExtrinsics[i], a)))
-    this.addEvents = await Promise.all(
-      addResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountAdded'))
-    )
-    // Confirm staking accounts
-    const confirmResults = await Promise.all(
-      this.confirmExtrinsics.map((tx) => this.api.signAndSend(tx, memberContext.account))
-    )
-    this.confirmEvents = await Promise.all(
-      confirmResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountConfirmed'))
-    )
-  }
-
-  async runQueryNodeChecks() {
-    await super.runQueryNodeChecks()
-    const { memberContext, accounts, addEvents, confirmEvents, addExtrinsics, confirmExtrinsics } = this
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(memberContext.memberId),
-      (qMember) => {
-        if (!qMember) {
-          throw new Error('Query node: Member not found')
-        }
-        assert.isNotEmpty(qMember.boundAccounts)
-        assert.includeMembers(qMember.boundAccounts, accounts)
-      }
-    )
-
-    // Check events
-    const qAddedEvents = await this.query.getStakingAccountAddedEvents(memberContext.memberId)
-    const qConfirmedEvents = await this.query.getStakingAccountConfirmedEvents(memberContext.memberId)
-    accounts.forEach(async (account, i) => {
-      this.assertQueryNodeAddAccountEventIsValid(addEvents[i], account, addExtrinsics[i].hash.toString(), qAddedEvents)
-      this.assertQueryNodeConfirmAccountEventIsValid(
-        confirmEvents[i],
-        account,
-        confirmExtrinsics[i].hash.toString(),
-        qConfirmedEvents
-      )
-    })
-  }
-}
-
-export class RemoveStakingAccountsHappyCaseFixture extends BaseMembershipFixture {
-  private memberContext: MemberContext
-  private accounts: string[]
-
-  private events: EventDetails[] = []
-  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
-
-  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
-    super(api, query)
-    this.memberContext = memberContext
-    this.accounts = accounts
-  }
-
-  private assertQueryNodeRemoveAccountEventIsValid(
-    eventDetails: EventDetails,
-    account: string,
-    txHash: string,
-    qEvents: StakingAccountRemovedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    assert.equal(qEvent.event.type, EventType.StakingAccountRemoved)
-    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
-    assert.equal(qEvent.account, account)
-  }
-
-  async execute(): Promise<void> {
-    const { memberContext, accounts } = this
-    this.extrinsics = accounts.map(() => this.api.tx.members.removeStakingAccount(memberContext.memberId))
-
-    const removeStakingAccountFee = await this.api.estimateTxFee(this.extrinsics[0], accounts[0])
-
-    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, removeStakingAccountFee)))
-    // Remove staking accounts
-    const results = await Promise.all(accounts.map((a, i) => this.api.signAndSend(this.extrinsics[i], a)))
-    this.events = await Promise.all(
-      results.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountRemoved'))
-    )
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    const { memberContext, accounts, events, extrinsics } = this
-    // Check member
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(memberContext.memberId),
-      (qMember) => {
-        if (!qMember) {
-          throw new Error('Query node: Membership not found!')
-        }
-        accounts.forEach((a) => assert.notInclude(qMember.boundAccounts, a))
-      }
-    )
-
-    // Check events
-    const qEvents = await this.query.getStakingAccountRemovedEvents(memberContext.memberId)
-    await Promise.all(
-      accounts.map(async (account, i) => {
-        this.assertQueryNodeRemoveAccountEventIsValid(events[i], account, extrinsics[i].hash.toString(), qEvents)
-      })
-    )
-  }
-}
-
-type MembershipSystemValues = {
-  referralCut: number
-  defaultInviteCount: number
-  membershipPrice: BN
-  invitedInitialBalance: BN
-}
-
-export class SudoUpdateMembershipSystem extends BaseMembershipFixture {
-  private newValues: Partial<MembershipSystemValues>
-
-  private events: EventDetails[] = []
-  private eventNames: MembershipEventName[] = []
-  private extrinsics: SubmittableExtrinsic<'promise'>[] = []
-
-  public constructor(api: Api, query: QueryNodeApi, newValues: Partial<MembershipSystemValues>) {
-    super(api, query)
-    this.newValues = newValues
-  }
-
-  private async getMembershipSystemValuesAt(blockNumber: number): Promise<MembershipSystemValues> {
-    const blockHash = await this.api.getBlockHash(blockNumber)
-    return {
-      referralCut: (await this.api.query.members.referralCut.at(blockHash)).toNumber(),
-      defaultInviteCount: (await this.api.query.members.initialInvitationCount.at(blockHash)).toNumber(),
-      invitedInitialBalance: await this.api.query.members.initialInvitationBalance.at(blockHash),
-      membershipPrice: await this.api.query.members.membershipPrice.at(blockHash),
-    }
-  }
-
-  private async assertBeforeSnapshotIsValid(beforeSnapshot: MembershipSystemSnapshotFieldsFragment) {
-    assert.isNumber(beforeSnapshot.snapshotBlock.number)
-    const chainValues = await this.getMembershipSystemValuesAt(beforeSnapshot.snapshotBlock.number)
-    assert.equal(beforeSnapshot.referralCut, chainValues.referralCut)
-    assert.equal(beforeSnapshot.invitedInitialBalance, chainValues.invitedInitialBalance.toString())
-    assert.equal(beforeSnapshot.membershipPrice, chainValues.membershipPrice.toString())
-    assert.equal(beforeSnapshot.defaultInviteCount, chainValues.defaultInviteCount)
-  }
-
-  private assertAfterSnapshotIsValid(
-    beforeSnapshot: MembershipSystemSnapshotFieldsFragment,
-    afterSnapshot: MembershipSystemSnapshotFieldsFragment
-  ) {
-    const { newValues } = this
-    const expectedValue = (field: keyof MembershipSystemValues) => {
-      const newValue = newValues[field]
-      return newValue === undefined ? beforeSnapshot[field] : newValue instanceof BN ? newValue.toString() : newValue
-    }
-    assert.equal(afterSnapshot.referralCut, expectedValue('referralCut'))
-    assert.equal(afterSnapshot.invitedInitialBalance, expectedValue('invitedInitialBalance'))
-    assert.equal(afterSnapshot.membershipPrice, expectedValue('membershipPrice'))
-    assert.equal(afterSnapshot.defaultInviteCount, expectedValue('defaultInviteCount'))
-  }
-
-  private checkEvent<T extends AnyQueryNodeEvent>(qEvent: T | null, txHash: string): T {
-    if (!qEvent) {
-      throw new Error('Missing query-node event')
-    }
-    assert.equal(qEvent.event.inExtrinsic, txHash)
-    return qEvent
-  }
-
-  async execute(): Promise<void> {
-    if (this.newValues.referralCut !== undefined) {
-      this.extrinsics.push(this.api.tx.sudo.sudo(this.api.tx.members.setReferralCut(this.newValues.referralCut)))
-      this.eventNames.push('ReferralCutUpdated')
-    }
-    if (this.newValues.defaultInviteCount !== undefined) {
-      this.extrinsics.push(
-        this.api.tx.sudo.sudo(this.api.tx.members.setInitialInvitationCount(this.newValues.defaultInviteCount))
-      )
-      this.eventNames.push('InitialInvitationCountUpdated')
-    }
-    if (this.newValues.membershipPrice !== undefined) {
-      this.extrinsics.push(
-        this.api.tx.sudo.sudo(this.api.tx.members.setMembershipPrice(this.newValues.membershipPrice))
-      )
-      this.eventNames.push('MembershipPriceUpdated')
-    }
-    if (this.newValues.invitedInitialBalance !== undefined) {
-      this.extrinsics.push(
-        this.api.tx.sudo.sudo(this.api.tx.members.setInitialInvitationBalance(this.newValues.invitedInitialBalance))
-      )
-      this.eventNames.push('InitialInvitationBalanceUpdated')
-    }
-
-    // We don't use api.makeSudoCall, since we cannot(?) then access tx hashes
-    const sudo = await this.api.query.sudo.key()
-    const results = await Promise.all(this.extrinsics.map((tx) => this.api.signAndSend(tx, sudo)))
-    this.events = await Promise.all(
-      results.map((r, i) => this.api.retrieveMembershipEventDetails(r, this.eventNames[i]))
-    )
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    const { events, extrinsics, eventNames } = this
-    const afterSnapshotBlockTimestamp = Math.max(...events.map((e) => e.blockTimestamp))
-
-    // Fetch "afterSnapshot" first to make sure query node has progressed enough
-    const afterSnapshot = (await this.query.tryQueryWithTimeout(
-      () => this.query.getMembershipSystemSnapshotAt(afterSnapshotBlockTimestamp),
-      (snapshot) => assert.isOk(snapshot)
-    )) as MembershipSystemSnapshotFieldsFragment
-
-    const beforeSnapshot = await this.query.getMembershipSystemSnapshotBefore(afterSnapshotBlockTimestamp)
-
-    if (!beforeSnapshot) {
-      throw new Error(`Query node: MembershipSystemSnapshot before timestamp ${afterSnapshotBlockTimestamp} not found!`)
-    }
-
-    // Validate snapshots
-    await this.assertBeforeSnapshotIsValid(beforeSnapshot)
-    this.assertAfterSnapshotIsValid(beforeSnapshot, afterSnapshot)
-
-    // Check events
-    await Promise.all(
-      events.map(async (event, i) => {
-        const tx = extrinsics[i]
-        const eventName = eventNames[i]
-        const txHash = tx.hash.toString()
-        const { blockNumber, indexInBlock } = event
-        if (eventName === 'ReferralCutUpdated') {
-          const { newValue } = this.checkEvent(
-            await this.query.getReferralCutUpdatedEvent(blockNumber, indexInBlock),
-            txHash
-          )
-          assert.equal(newValue, this.newValues.referralCut)
-        }
-        if (eventName === 'MembershipPriceUpdated') {
-          const { newPrice } = this.checkEvent(
-            await this.query.getMembershipPriceUpdatedEvent(blockNumber, indexInBlock),
-            txHash
-          )
-          assert.equal(newPrice, this.newValues.membershipPrice!.toString())
-        }
-        if (eventName === 'InitialInvitationBalanceUpdated') {
-          const { newInitialBalance } = this.checkEvent(
-            await this.query.getInitialInvitationBalanceUpdatedEvent(blockNumber, indexInBlock),
-            txHash
-          )
-          assert.equal(newInitialBalance, this.newValues.invitedInitialBalance!.toString())
-        }
-        if (eventName === 'InitialInvitationCountUpdated') {
-          const { newInitialInvitationCount } = this.checkEvent(
-            await this.query.getInitialInvitationCountUpdatedEvent(blockNumber, indexInBlock),
-            txHash
-          )
-          assert.equal(newInitialInvitationCount, this.newValues.defaultInviteCount)
-        }
-      })
-    )
-  }
-}

+ 166 - 0
tests/integration-tests/src/fixtures/workingGroups/ApplyOnOpeningsHappyCaseFixture.ts

@@ -0,0 +1,166 @@
+import { ApplicationMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { ApplicationFieldsFragment, AppliedOnOpeningEventFieldsFragment } from '../../graphql/generated/queries'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { AppliedOnOpeningEventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import _ from 'lodash'
+import { MemberId } from '@joystream/types/common'
+import { ApplicationId, Opening, OpeningId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+
+export type ApplicantDetails = {
+  memberId: MemberId
+  roleAccount: string
+  rewardAccount: string
+  stakingAccount: string
+}
+
+export type OpeningApplications = {
+  openingId: OpeningId
+  openingMetadata: OpeningMetadata.AsObject
+  applicants: ApplicantDetails[]
+}
+
+export type OpeningApplicationsFlattened = {
+  openingId: OpeningId
+  openingMetadata: OpeningMetadata.AsObject
+  applicant: ApplicantDetails
+}[]
+
+export class ApplyOnOpeningsHappyCaseFixture extends BaseWorkingGroupFixture {
+  protected applications: OpeningApplicationsFlattened
+  protected events: AppliedOnOpeningEventDetails[] = []
+  protected createdApplicationsByOpeningId: Map<number, ApplicationId[]> = new Map<number, ApplicationId[]>()
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingsApplications: OpeningApplications[]
+  ) {
+    super(api, query, group)
+    this.applications = this.flattenOpeningApplicationsData(openingsApplications)
+  }
+
+  protected flattenOpeningApplicationsData(openingsApplications: OpeningApplications[]): OpeningApplicationsFlattened {
+    return openingsApplications.reduce(
+      (curr, details) =>
+        curr.concat(
+          details.applicants.map((a) => ({
+            openingId: details.openingId,
+            openingMetadata: details.openingMetadata,
+            applicant: a,
+          }))
+        ),
+      [] as OpeningApplicationsFlattened
+    )
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.applications.map((a) => a.applicant.roleAccount)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const openingIds = _.uniq(this.applications.map((a) => a.openingId.toString()))
+    const openings = await this.api.query[this.group].openingById.multi<Opening>(openingIds)
+    return this.applications.map((a, i) => {
+      const openingIndex = openingIds.findIndex((id) => id === a.openingId.toString())
+      const opening = openings[openingIndex]
+      return this.api.tx[this.group].applyOnOpening({
+        member_id: a.applicant.memberId,
+        description: Utils.metadataToBytes(this.getApplicationMetadata(a.openingMetadata, i)),
+        opening_id: a.openingId,
+        reward_account_id: a.applicant.rewardAccount,
+        role_account_id: a.applicant.roleAccount,
+        stake_parameters: {
+          stake: opening.stake_policy.stake_amount,
+          staking_account_id: a.applicant.stakingAccount,
+        },
+      })
+    })
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<AppliedOnOpeningEventDetails> {
+    return this.api.retrieveAppliedOnOpeningEventDetails(result, this.group)
+  }
+
+  public async execute(): Promise<void> {
+    await super.execute()
+    this.applications.map(({ openingId }, i) => {
+      this.createdApplicationsByOpeningId.set(openingId.toNumber(), [
+        ...(this.createdApplicationsByOpeningId.get(openingId.toNumber()) || []),
+        this.events[i].applicationId,
+      ])
+    })
+  }
+
+  public getCreatedApplicationsByOpeningId(openingId: OpeningId): ApplicationId[] {
+    const applicationIds = this.createdApplicationsByOpeningId.get(openingId.toNumber())
+    if (!applicationIds) {
+      throw new Error(`No created application ids by opening id ${openingId.toString()} found!`)
+    }
+    return applicationIds
+  }
+
+  protected getApplicationMetadata(openingMetadata: OpeningMetadata.AsObject, i: number): ApplicationMetadata {
+    const metadata = new ApplicationMetadata()
+    openingMetadata.applicationFormQuestionsList.forEach((question, j) => {
+      metadata.addAnswers(`Answer to question ${j} by applicant number ${i}`)
+    })
+    return metadata
+  }
+
+  protected assertQueriedApplicationsAreValid(qApplications: ApplicationFieldsFragment[]): void {
+    this.events.map((e, i) => {
+      const applicationDetails = this.applications[i]
+      const qApplication = qApplications.find((a) => a.runtimeId === e.applicationId.toNumber())
+      Utils.assert(qApplication, 'Query node: Application not found!')
+      assert.equal(qApplication.runtimeId, e.applicationId.toNumber())
+      assert.equal(qApplication.createdAtBlock.number, e.blockNumber)
+      assert.equal(qApplication.opening.runtimeId, applicationDetails.openingId.toNumber())
+      assert.equal(qApplication.applicant.id, applicationDetails.applicant.memberId.toString())
+      assert.equal(qApplication.roleAccount, applicationDetails.applicant.roleAccount)
+      assert.equal(qApplication.rewardAccount, applicationDetails.applicant.rewardAccount)
+      assert.equal(qApplication.stakingAccount, applicationDetails.applicant.stakingAccount)
+      assert.equal(qApplication.status.__typename, 'ApplicationStatusPending')
+      assert.equal(qApplication.stake, e.params.stake_parameters.stake)
+
+      const applicationMetadata = this.getApplicationMetadata(applicationDetails.openingMetadata, i)
+      assert.deepEqual(
+        qApplication.answers.map(({ question: { question }, answer }) => ({ question, answer })),
+        applicationDetails.openingMetadata.applicationFormQuestionsList.map(({ question }, index) => ({
+          question,
+          answer: applicationMetadata.getAnswersList()[index],
+        }))
+      )
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: AppliedOnOpeningEventFieldsFragment, i: number): void {
+    const applicationDetails = this.applications[i]
+    assert.equal(qEvent.event.type, EventType.AppliedOnOpening)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.opening.runtimeId, applicationDetails.openingId.toNumber())
+    assert.equal(qEvent.application.runtimeId, this.events[i].applicationId.toNumber())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getAppliedOnOpeningEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+    // Query the applications
+    const qApplications = await this.query.getApplicationsByIds(
+      this.events.map((e) => e.applicationId),
+      this.group
+    )
+    this.assertQueriedApplicationsAreValid(qApplications)
+  }
+}

+ 95 - 0
tests/integration-tests/src/fixtures/workingGroups/BaseCreateOpeningFixture.ts

@@ -0,0 +1,95 @@
+import BN from 'bn.js'
+import { OpeningMetadata } from '@joystream/metadata-protobuf'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { MIN_APPLICATION_STAKE, MIN_USTANKING_PERIOD } from '../../consts'
+import { OpeningMetadataFieldsFragment } from '../../graphql/generated/queries'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { queryNodeQuestionTypeToMetadataQuestionType } from './utils'
+import _ from 'lodash'
+
+export type OpeningParams = {
+  stake: BN
+  unstakingPeriod: number
+  reward: BN
+  metadata: OpeningMetadata.AsObject
+}
+
+export type UpcomingOpeningParams = OpeningParams & {
+  expectedStartTs: number
+}
+
+export abstract class BaseCreateOpeningFixture extends BaseWorkingGroupFixture {
+  protected openingsParams: OpeningParams[]
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingsParams?: Partial<OpeningParams>[]
+  ) {
+    super(api, query, group)
+    this.openingsParams = (openingsParams || [{}]).map((params) => _.merge(this.defaultOpeningParams, params))
+  }
+
+  protected defaultOpeningParams: OpeningParams = {
+    stake: MIN_APPLICATION_STAKE,
+    unstakingPeriod: MIN_USTANKING_PERIOD,
+    reward: new BN(10),
+    metadata: {
+      shortDescription: 'Test opening',
+      description: '# Test opening',
+      expectedEndingTimestamp: Date.now() + 60,
+      hiringLimit: 1,
+      applicationDetails: '- This is automatically created opening, do not apply!',
+      applicationFormQuestionsList: [
+        { question: 'Question 1?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXT },
+        { question: 'Question 2?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA },
+      ],
+    },
+  }
+
+  public getDefaultOpeningParams(): OpeningParams {
+    return this.defaultOpeningParams
+  }
+
+  protected getMetadata(openingParams: OpeningParams): OpeningMetadata {
+    const metadataObj = openingParams.metadata as Required<OpeningMetadata.AsObject>
+    const metadata = new OpeningMetadata()
+    metadata.setShortDescription(metadataObj.shortDescription)
+    metadata.setDescription(metadataObj.description)
+    metadata.setExpectedEndingTimestamp(metadataObj.expectedEndingTimestamp)
+    metadata.setHiringLimit(metadataObj.hiringLimit)
+    metadata.setApplicationDetails(metadataObj.applicationDetails)
+    metadataObj.applicationFormQuestionsList.forEach(({ question, type }) => {
+      const applicationFormQuestion = new OpeningMetadata.ApplicationFormQuestion()
+      applicationFormQuestion.setQuestion(question!)
+      applicationFormQuestion.setType(type!)
+      metadata.addApplicationFormQuestions(applicationFormQuestion)
+    })
+
+    return metadata
+  }
+
+  protected assertQueriedOpeningMetadataIsValid(
+    openingParams: OpeningParams,
+    qOpeningMeta: OpeningMetadataFieldsFragment
+  ): void {
+    assert.equal(qOpeningMeta.shortDescription, openingParams.metadata.shortDescription)
+    assert.equal(qOpeningMeta.description, openingParams.metadata.description)
+    assert.equal(new Date(qOpeningMeta.expectedEnding).getTime(), openingParams.metadata.expectedEndingTimestamp)
+    assert.equal(qOpeningMeta.hiringLimit, openingParams.metadata.hiringLimit)
+    assert.equal(qOpeningMeta.applicationDetails, openingParams.metadata.applicationDetails)
+    assert.deepEqual(
+      qOpeningMeta.applicationFormQuestions
+        .sort((a, b) => a.index - b.index)
+        .map(({ question, type }) => ({
+          question,
+          type: queryNodeQuestionTypeToMetadataQuestionType(type),
+        })),
+      openingParams.metadata.applicationFormQuestionsList
+    )
+  }
+}

+ 13 - 0
tests/integration-tests/src/fixtures/workingGroups/BaseWorkingGroupFixture.ts

@@ -0,0 +1,13 @@
+import { Api } from '../../Api'
+import { StandardizedFixture } from '../../Fixture'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { WorkingGroupModuleName } from '../../types'
+
+export abstract class BaseWorkingGroupFixture extends StandardizedFixture {
+  protected group: WorkingGroupModuleName
+
+  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName) {
+    super(api, query)
+    this.group = group
+  }
+}

+ 78 - 0
tests/integration-tests/src/fixtures/workingGroups/CancelOpeningsFixture.ts

@@ -0,0 +1,78 @@
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { OpeningId } from '@joystream/types/working-group'
+import {
+  ApplicationBasicFieldsFragment,
+  OpeningCanceledEventFieldsFragment,
+  OpeningFieldsFragment,
+} from '../../graphql/generated/queries'
+
+export class CancelOpeningsFixture extends BaseWorkingGroupFixture {
+  protected openingIds: OpeningId[]
+
+  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, openingIds: OpeningId[]) {
+    super(api, query, group)
+    this.openingIds = openingIds
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.openingIds.map((id) => this.api.tx[this.group].cancelOpening(id))
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'OpeningCanceled')
+  }
+
+  protected assertQueriedOpeningsAreValid(
+    qEvents: OpeningCanceledEventFieldsFragment[],
+    qOpenings: OpeningFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const openingId = this.openingIds[i]
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
+      Utils.assert(qOpening)
+      Utils.assert(qOpening.status.__typename === 'OpeningStatusCancelled', 'Query node: Invalid opening status')
+      assert.equal(qOpening.status.openingCancelledEventId, qEvent.id)
+      qOpening.applications.forEach((a) => this.assertApplicationStatusIsValid(qEvent, a))
+    })
+  }
+
+  protected assertApplicationStatusIsValid(
+    qEvent: OpeningCanceledEventFieldsFragment,
+    qApplication: ApplicationBasicFieldsFragment
+  ): void {
+    // It's possible that some of the applications have been withdrawn
+    assert.oneOf(qApplication.status.__typename, ['ApplicationStatusWithdrawn', 'ApplicationStatusCancelled'])
+    if (qApplication.status.__typename === 'ApplicationStatusCancelled') {
+      assert.equal(qApplication.status.openingCancelledEventId, qEvent.id)
+    }
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: OpeningCanceledEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.OpeningCanceled)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.opening.runtimeId, this.openingIds[i].toNumber())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getOpeningCancelledEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+    const qOpenings = await this.query.getOpeningsByIds(this.openingIds, this.group)
+    this.assertQueriedOpeningsAreValid(qEvents, qOpenings)
+  }
+}

+ 95 - 0
tests/integration-tests/src/fixtures/workingGroups/CreateOpeningsFixture.ts

@@ -0,0 +1,95 @@
+import { Api } from '../../Api'
+import { BaseCreateOpeningFixture, OpeningParams } from './BaseCreateOpeningFixture'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { OpeningAddedEventDetails, WorkingGroupModuleName } from '../../types'
+import { OpeningId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { OpeningAddedEventFieldsFragment, OpeningFieldsFragment } from '../../graphql/generated/queries'
+import { EventType, WorkingGroupOpeningType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+
+export class CreateOpeningsFixture extends BaseCreateOpeningFixture {
+  protected asSudo: boolean
+  protected events: OpeningAddedEventDetails[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingsParams?: Partial<OpeningParams>[],
+    asSudo = false
+  ) {
+    super(api, query, group, openingsParams)
+    this.asSudo = asSudo
+  }
+
+  public getCreatedOpeningIds(): OpeningId[] {
+    if (!this.events.length) {
+      throw new Error('Trying to get created opening ids before they were created!')
+    }
+    return this.events.map((e) => e.openingId)
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.openingsParams.map((params) =>
+      this.api.tx[this.group].addOpening(
+        Utils.metadataToBytes(this.getMetadata(params)),
+        this.asSudo ? 'Leader' : 'Regular',
+        { stake_amount: params.stake, leaving_unstaking_period: params.unstakingPeriod },
+        params.reward
+      )
+    )
+
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<OpeningAddedEventDetails> {
+    return this.api.retrieveOpeningAddedEventDetails(result, this.group)
+  }
+
+  protected assertQueriedOpeningsAreValid(qOpenings: OpeningFieldsFragment[]): void {
+    this.events.map((e, i) => {
+      const qOpening = qOpenings.find((o) => o.runtimeId === e.openingId.toNumber())
+      const openingParams = this.openingsParams[i]
+      Utils.assert(qOpening, 'Query node: Opening not found')
+      assert.equal(qOpening.runtimeId, e.openingId.toNumber())
+      assert.equal(qOpening.createdAtBlock.number, e.blockNumber)
+      assert.equal(qOpening.group.name, this.group)
+      assert.equal(qOpening.rewardPerBlock, openingParams.reward.toString())
+      assert.equal(qOpening.type, this.asSudo ? WorkingGroupOpeningType.Leader : WorkingGroupOpeningType.Regular)
+      assert.equal(qOpening.status.__typename, 'OpeningStatusOpen')
+      assert.equal(qOpening.stakeAmount, openingParams.stake.toString())
+      assert.equal(qOpening.unstakingPeriod, openingParams.unstakingPeriod)
+      // Metadata
+      this.assertQueriedOpeningMetadataIsValid(openingParams, qOpening.metadata)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: OpeningAddedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.OpeningAdded)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.opening.runtimeId, this.events[i].openingId.toNumber())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getOpeningAddedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the openings
+    const qOpenings = await this.query.getOpeningsByIds(
+      this.events.map((e) => e.openingId),
+      this.group
+    )
+    this.assertQueriedOpeningsAreValid(qOpenings)
+  }
+}

+ 114 - 0
tests/integration-tests/src/fixtures/workingGroups/CreateUpcomingOpeningsFixture.ts

@@ -0,0 +1,114 @@
+import { Api } from '../../Api'
+import { BaseCreateOpeningFixture, UpcomingOpeningParams } from './BaseCreateOpeningFixture'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { StatusTextChangedEventFieldsFragment, UpcomingOpeningFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { AddUpcomingOpening, UpcomingOpeningMetadata, WorkingGroupMetadataAction } from '@joystream/metadata-protobuf'
+import _ from 'lodash'
+
+export class CreateUpcomingOpeningsFixture extends BaseCreateOpeningFixture {
+  protected openingsParams: UpcomingOpeningParams[]
+  protected createdUpcomingOpeningIds: string[] = []
+
+  public getDefaultOpeningParams(): UpcomingOpeningParams {
+    return {
+      ...super.getDefaultOpeningParams(),
+      expectedStartTs: Date.now() + 3600,
+    }
+  }
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingsParams?: Partial<UpcomingOpeningParams>[]
+  ) {
+    super(api, query, group, openingsParams)
+    this.openingsParams = (openingsParams || [{}]).map((params) => _.merge(this.getDefaultOpeningParams(), params))
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.openingsParams.map((params) => {
+      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(params))
+      return this.api.tx[this.group].setStatusText(metaBytes)
+    })
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StatusTextChanged')
+  }
+
+  public getCreatedUpcomingOpeningIds(): string[] {
+    if (!this.createdUpcomingOpeningIds.length) {
+      throw new Error('Trying to get created UpcomingOpening ids before they are known')
+    }
+    return this.createdUpcomingOpeningIds
+  }
+
+  protected getActionMetadata(openingParams: UpcomingOpeningParams): WorkingGroupMetadataAction {
+    const actionMeta = new WorkingGroupMetadataAction()
+    const addUpcomingOpeningMeta = new AddUpcomingOpening()
+
+    const upcomingOpeningMeta = new UpcomingOpeningMetadata()
+    const openingMeta = this.getMetadata(openingParams)
+    upcomingOpeningMeta.setMetadata(openingMeta)
+    upcomingOpeningMeta.setExpectedStart(openingParams.expectedStartTs)
+    upcomingOpeningMeta.setMinApplicationStake(openingParams.stake.toNumber())
+    upcomingOpeningMeta.setRewardPerBlock(openingParams.reward.toNumber())
+
+    addUpcomingOpeningMeta.setMetadata(upcomingOpeningMeta)
+    actionMeta.setAddUpcomingOpening(addUpcomingOpeningMeta)
+
+    return actionMeta
+  }
+
+  protected assertQueriedUpcomingOpeningsAreValid(
+    qUpcomingOpenings: UpcomingOpeningFieldsFragment[],
+    qEvents: StatusTextChangedEventFieldsFragment[]
+  ): void {
+    this.events.forEach((e, i) => {
+      const openingParams = this.openingsParams[i]
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const qUpcomingOpening = qUpcomingOpenings.find((o) => o.createdInEvent.id === qEvent.id)
+      Utils.assert(qUpcomingOpening)
+      assert.equal(new Date(qUpcomingOpening.expectedStart).getTime(), openingParams.expectedStartTs)
+      assert.equal(qUpcomingOpening.group.name, this.group)
+      assert.equal(qUpcomingOpening.rewardPerBlock, openingParams.reward.toString())
+      assert.equal(qUpcomingOpening.stakeAmount, openingParams.stake.toString())
+      assert.equal(qUpcomingOpening.createdAtBlock.number, e.blockNumber)
+      this.assertQueriedOpeningMetadataIsValid(openingParams, qUpcomingOpening.metadata)
+      Utils.assert(qEvent.result.__typename === 'UpcomingOpeningAdded')
+      assert.equal(qEvent.result.upcomingOpeningId, qUpcomingOpening.id)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.openingsParams[i])).toString())
+    assert.equal(qEvent.result.__typename, 'UpcomingOpeningAdded')
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the event
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getStatusTextChangedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+    // Query the opening
+    const qUpcomingOpenings = await this.query.getUpcomingOpeningsByCreatedInEventIds(qEvents.map((e) => e.id))
+    this.assertQueriedUpcomingOpeningsAreValid(qUpcomingOpenings, qEvents)
+
+    this.createdUpcomingOpeningIds = qUpcomingOpenings.map((o) => o.id)
+  }
+}

+ 192 - 0
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -0,0 +1,192 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { OpeningFilledEventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { Application, ApplicationId, OpeningId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { JoyBTreeSet } from '@joystream/types/common'
+import { registry } from '@joystream/types'
+import { lockIdByWorkingGroup } from '../../consts'
+import {
+  OpeningFieldsFragment,
+  OpeningFilledEventFieldsFragment,
+  WorkerFieldsFragment,
+} from '../../graphql/generated/queries'
+
+export class FillOpeningsFixture extends BaseWorkingGroupFixture {
+  protected events: OpeningFilledEventDetails[] = []
+  protected asSudo: boolean
+
+  protected openingIds: OpeningId[]
+  protected acceptedApplicationsIdsArrays: ApplicationId[][]
+
+  protected acceptedApplicationsArrays: Application[][] = []
+  protected applicationStakesArrays: BN[][] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingIds: OpeningId[],
+    acceptedApplicationsIdsArrays: ApplicationId[][],
+    asSudo = false
+  ) {
+    super(api, query, group)
+    this.openingIds = openingIds
+    this.acceptedApplicationsIdsArrays = acceptedApplicationsIdsArrays
+    this.asSudo = asSudo
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.openingIds.map((openingId, i) =>
+      this.api.tx[this.group].fillOpening(
+        openingId,
+        new (JoyBTreeSet(ApplicationId))(registry, this.acceptedApplicationsIdsArrays[i])
+      )
+    )
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<OpeningFilledEventDetails> {
+    return this.api.retrieveOpeningFilledEventDetails(result, this.group)
+  }
+
+  protected async loadApplicationsData(): Promise<void> {
+    this.acceptedApplicationsArrays = await Promise.all(
+      this.acceptedApplicationsIdsArrays.map((acceptedApplicationIds) =>
+        this.api.query[this.group].applicationById.multi<Application>(acceptedApplicationIds)
+      )
+    )
+    this.applicationStakesArrays = await Promise.all(
+      this.acceptedApplicationsArrays.map((acceptedApplications) =>
+        Promise.all(
+          acceptedApplications.map((a) =>
+            this.api.getStakedBalance(a.staking_account_id, lockIdByWorkingGroup[this.group])
+          )
+        )
+      )
+    )
+  }
+
+  async execute(): Promise<void> {
+    await this.loadApplicationsData()
+    await super.execute()
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: OpeningFilledEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.OpeningFilled)
+    assert.equal(qEvent.opening.runtimeId, this.openingIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    this.acceptedApplicationsIdsArrays[i].forEach((acceptedApplId, j) => {
+      // Cannot use "applicationIdToWorkerIdMap.get" here,
+      // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
+      const [, workerId] =
+        Array.from(this.events[i].applicationIdToWorkerIdMap.entries()).find(([applicationId]) =>
+          applicationId.eq(acceptedApplId)
+        ) || []
+      Utils.assert(
+        workerId,
+        `WorkerId for application id ${acceptedApplId.toString()} not found in OpeningFilled event!`
+      )
+      const qWorker = qEvent.workersHired.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(qWorker, `Query node: Worker not found in OpeningFilled.hiredWorkers (id: ${workerId.toString()})`)
+      this.assertHiredWorkerIsValid(
+        this.events[i],
+        this.acceptedApplicationsIdsArrays[i][j],
+        this.acceptedApplicationsArrays[i][j],
+        this.applicationStakesArrays[i][j],
+        qWorker
+      )
+    })
+  }
+
+  protected assertHiredWorkerIsValid(
+    eventDetails: OpeningFilledEventDetails,
+    applicationId: ApplicationId,
+    application: Application,
+    applicationStake: BN,
+    qWorker: WorkerFieldsFragment
+  ): void {
+    assert.equal(qWorker.group.name, this.group)
+    assert.equal(qWorker.membership.id, application.member_id.toString())
+    assert.equal(qWorker.roleAccount, application.role_account_id.toString())
+    assert.equal(qWorker.rewardAccount, application.reward_account_id.toString())
+    assert.equal(qWorker.stakeAccount, application.staking_account_id.toString())
+    assert.equal(qWorker.status.__typename, 'WorkerStatusActive')
+    assert.equal(qWorker.isLead, true)
+    assert.equal(qWorker.stake, applicationStake.toString())
+    assert.equal(qWorker.hiredAtBlock.number, eventDetails.blockNumber)
+    assert.equal(qWorker.application.runtimeId, applicationId.toNumber())
+  }
+
+  protected assertOpeningsStatusesAreValid(
+    qEvents: OpeningFilledEventFieldsFragment[],
+    qOpenings: OpeningFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const openingId = this.openingIds[i]
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
+      Utils.assert(qOpening, 'Query node: Opening not found')
+      Utils.assert(qOpening.status.__typename === 'OpeningStatusFilled', 'Query node: Invalid opening status')
+      assert.equal(qOpening.status.openingFilledEventId, qEvent.id)
+    })
+  }
+
+  protected assertApplicationStatusesAreValid(
+    qEvents: OpeningFilledEventFieldsFragment[],
+    qOpenings: OpeningFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const openingId = this.openingIds[i]
+      const acceptedApplicationsIds = this.acceptedApplicationsIdsArrays[i]
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
+      Utils.assert(qOpening, 'Query node: Opening not found')
+      qOpening.applications.forEach((qApplication) => {
+        const isAccepted = acceptedApplicationsIds.some((id) => id.toNumber() === qApplication.runtimeId)
+        if (isAccepted) {
+          Utils.assert(qApplication.status.__typename === 'ApplicationStatusAccepted', 'Invalid application status')
+          assert.equal(qApplication.status.openingFilledEventId, qEvent.id)
+        } else {
+          assert.oneOf(qApplication.status.__typename, ['ApplicationStatusRejected', 'ApplicationStatusWithdrawn'])
+          if (qApplication.status.__typename === 'ApplicationStatusRejected') {
+            assert.equal(qApplication.status.openingFilledEventId, qEvent.id)
+          }
+        }
+      })
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the event and check event + hiredWorkers
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getOpeningFilledEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check openings statuses
+    const qOpenings = await this.query.getOpeningsByIds(this.openingIds, this.group)
+    this.assertOpeningsStatusesAreValid(qEvents, qOpenings)
+
+    // Check application statuses
+    this.assertApplicationStatusesAreValid(qEvents, qOpenings)
+
+    if (this.asSudo) {
+      const qGroup = await this.query.getWorkingGroup(this.group)
+      Utils.assert(qGroup, 'Query node: Working group not found!')
+      Utils.assert(qGroup.leader, 'Query node: Working group leader not set!')
+      assert.equal(qGroup.leader.runtimeId, qEvents[0].workersHired[0].runtimeId)
+    }
+  }
+}

+ 68 - 0
tests/integration-tests/src/fixtures/workingGroups/RemoveUpcomingOpeningsFixture.ts

@@ -0,0 +1,68 @@
+import { Api } from '../../Api'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { StatusTextChangedEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { RemoveUpcomingOpening, WorkingGroupMetadataAction } from '@joystream/metadata-protobuf'
+
+export class RemoveUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
+  protected upcomingOpeningIds: string[]
+
+  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, upcomingOpeningIds: string[]) {
+    super(api, query, group)
+    this.upcomingOpeningIds = upcomingOpeningIds
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.upcomingOpeningIds.map((id) => {
+      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(id))
+      return this.api.tx[this.group].setStatusText(metaBytes)
+    })
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StatusTextChanged')
+  }
+
+  protected getActionMetadata(upcomingOpeningId: string): WorkingGroupMetadataAction {
+    const actionMeta = new WorkingGroupMetadataAction()
+    const removeUpcomingOpeningMeta = new RemoveUpcomingOpening()
+    removeUpcomingOpeningMeta.setId(upcomingOpeningId)
+    actionMeta.setRemoveUpcomingOpening(removeUpcomingOpeningMeta)
+
+    return actionMeta
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.upcomingOpeningIds[i])).toString())
+    Utils.assert(qEvent.result.__typename === 'UpcomingOpeningRemoved', 'Unexpected StatuxTextChangedEvent result type')
+    assert.equal(qEvent.result.upcomingOpeningId, this.upcomingOpeningIds[i])
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query & check the event
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getStatusTextChangedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+    // Query the openings and make sure they no longer exist
+    await Promise.all(
+      this.upcomingOpeningIds.map(async (id) => {
+        const qUpcomingOpening = await this.query.getUpcomingOpeningById(id)
+        assert.isNull(qUpcomingOpening)
+      })
+    )
+  }
+}

+ 142 - 0
tests/integration-tests/src/fixtures/workingGroups/UpdateGroupStatusFixture.ts

@@ -0,0 +1,142 @@
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { SetGroupMetadata, WorkingGroupMetadata, WorkingGroupMetadataAction } from '@joystream/metadata-protobuf'
+import {
+  StatusTextChangedEventFieldsFragment,
+  WorkingGroupFieldsFragment,
+  WorkingGroupMetadataFieldsFragment,
+} from '../../graphql/generated/queries'
+import _ from 'lodash'
+
+export class UpdateGroupStatusFixture extends BaseWorkingGroupFixture {
+  protected updates: WorkingGroupMetadata.AsObject[]
+  protected areExtrinsicsOrderSensitive = true
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    updates: WorkingGroupMetadata.AsObject[]
+  ) {
+    super(api, query, group)
+    this.updates = updates
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.updates.map((update) => {
+      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(update))
+      return this.api.tx[this.group].setStatusText(metaBytes)
+    })
+  }
+
+  protected async getEventFromResult(r: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(r, this.group, 'StatusTextChanged')
+  }
+
+  protected getActionMetadata(update: WorkingGroupMetadata.AsObject): WorkingGroupMetadataAction {
+    const actionMeta = new WorkingGroupMetadataAction()
+    const setGroupMeta = new SetGroupMetadata()
+    const newGroupMeta = new WorkingGroupMetadata()
+
+    newGroupMeta.setAbout(update.about!)
+    newGroupMeta.setDescription(update.description!)
+    newGroupMeta.setStatus(update.status!)
+    newGroupMeta.setStatusMessage(update.statusMessage!)
+
+    setGroupMeta.setNewMetadata(newGroupMeta)
+    actionMeta.setSetGroupMetadata(setGroupMeta)
+
+    return actionMeta
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.updates[i])).toString())
+    assert.equal(qEvent.result.__typename, 'WorkingGroupMetadataSet')
+  }
+
+  protected assertQueriedGroupIsValid(
+    qGroup: WorkingGroupFieldsFragment,
+    qMeta: WorkingGroupMetadataFieldsFragment
+  ): void {
+    if (!qGroup.metadata) {
+      throw new Error(`Query node: Group metadata is empty!`)
+    }
+    assert.equal(qGroup.metadata.id, qMeta.id)
+  }
+
+  protected assertQueriedMetadataSnapshotsAreValid(
+    eventDetails: EventDetails,
+    preUpdateSnapshot: WorkingGroupMetadataFieldsFragment | null,
+    postUpdateSnapshot: WorkingGroupMetadataFieldsFragment | null,
+    update: WorkingGroupMetadata.AsObject
+  ): asserts postUpdateSnapshot is WorkingGroupMetadataFieldsFragment {
+    if (!postUpdateSnapshot) {
+      throw new Error('Query node: WorkingGroupMetadata snapshot not found!')
+    }
+    const expectedMeta = _.merge(preUpdateSnapshot, update)
+    assert.equal(postUpdateSnapshot.status, expectedMeta.status)
+    assert.equal(postUpdateSnapshot.statusMessage, expectedMeta.statusMessage)
+    assert.equal(postUpdateSnapshot.description, expectedMeta.description)
+    assert.equal(postUpdateSnapshot.about, expectedMeta.about)
+    assert.equal(postUpdateSnapshot.setAtBlock.number, eventDetails.blockNumber)
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query & check the event
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getStatusTextChangedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the group
+    const qGroup = await this.query.getWorkingGroup(this.group)
+    if (!qGroup) {
+      throw new Error('Query node: Working group not found!')
+    }
+
+    // Query & check the metadata snapshots
+    const snapshots = await this.query.getGroupMetaSnapshotsByTimeAsc(qGroup.id)
+    let lastSnapshot: WorkingGroupMetadataFieldsFragment | null = null
+    this.events.forEach((postUpdateEvent, i) => {
+      const postUpdateSnapshotIndex = snapshots.findIndex(
+        (s) =>
+          s.setInEvent.event.id ===
+          this.query.getQueryNodeEventId(postUpdateEvent.blockNumber, postUpdateEvent.indexInBlock)
+      )
+      const postUpdateSnapshot = postUpdateSnapshotIndex > -1 ? snapshots[postUpdateSnapshotIndex] : null
+      const preUpdateSnapshot = postUpdateSnapshotIndex > 0 ? snapshots[postUpdateSnapshotIndex - 1] : null
+      this.assertQueriedMetadataSnapshotsAreValid(
+        postUpdateEvent,
+        preUpdateSnapshot,
+        postUpdateSnapshot,
+        this.updates[i]
+      )
+      const qEvent = qEvents[i]
+      Utils.assert(
+        qEvent.result.__typename === 'WorkingGroupMetadataSet',
+        'Invalid StatusTextChanged event result type'
+      )
+      assert(qEvent.result.metadataId, postUpdateSnapshot.id)
+      lastSnapshot = postUpdateSnapshot
+    })
+
+    // Check the group
+    if (lastSnapshot) {
+      this.assertQueriedGroupIsValid(qGroup, lastSnapshot)
+    }
+  }
+}

+ 76 - 0
tests/integration-tests/src/fixtures/workingGroups/WithdrawApplicationsFixture.ts

@@ -0,0 +1,76 @@
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { ApplicationId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { ApplicationFieldsFragment, ApplicationWithdrawnEventFieldsFragment } from '../../graphql/generated/queries'
+
+export class WithdrawApplicationsFixture extends BaseWorkingGroupFixture {
+  protected applicationIds: ApplicationId[]
+  protected accounts: string[]
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    accounts: string[],
+    applicationIds: ApplicationId[]
+  ) {
+    super(api, query, group)
+    this.accounts = accounts
+    this.applicationIds = applicationIds
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.accounts
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.applicationIds.map((id) => this.api.tx[this.group].withdrawApplication(id))
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'ApplicationWithdrawn')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ApplicationWithdrawnEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.ApplicationWithdrawn)
+    assert.equal(qEvent.application.runtimeId, this.applicationIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+  }
+
+  protected assertApplicationStatusesAreValid(
+    qEvents: ApplicationWithdrawnEventFieldsFragment[],
+    qApplications: ApplicationFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const qApplication = qApplications.find((a) => a.runtimeId === this.applicationIds[i].toNumber())
+      Utils.assert(qApplication, 'Query node: Application not found!')
+      Utils.assert(
+        qApplication.status.__typename === 'ApplicationStatusWithdrawn',
+        'Query node: Invalid application status!'
+      )
+      assert.equal(qApplication.status.applicationWithdrawnEventId, qEvent.id)
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+
+    // Query the evens
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getApplicationWithdrawnEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check application statuses
+    const qApplciations = await this.query.getApplicationsByIds(this.applicationIds, this.group)
+    this.assertApplicationStatusesAreValid(qEvents, qApplciations)
+  }
+}

+ 12 - 0
tests/integration-tests/src/fixtures/workingGroups/index.ts

@@ -0,0 +1,12 @@
+export {
+  ApplyOnOpeningsHappyCaseFixture,
+  ApplicantDetails,
+  OpeningApplications,
+} from './ApplyOnOpeningsHappyCaseFixture'
+export { CancelOpeningsFixture } from './CancelOpeningsFixture'
+export { CreateOpeningsFixture } from './CreateOpeningsFixture'
+export { CreateUpcomingOpeningsFixture } from './CreateUpcomingOpeningsFixture'
+export { FillOpeningsFixture } from './FillOpeningsFixture'
+export { RemoveUpcomingOpeningsFixture } from './RemoveUpcomingOpeningsFixture'
+export { UpdateGroupStatusFixture } from './UpdateGroupStatusFixture'
+export { WithdrawApplicationsFixture } from './WithdrawApplicationsFixture'

+ 15 - 0
tests/integration-tests/src/fixtures/workingGroups/utils.ts

@@ -0,0 +1,15 @@
+import { OpeningMetadata } from '@joystream/metadata-protobuf'
+import { ApplicationFormQuestionType } from '../../graphql/generated/schema'
+
+type InputTypeMap = OpeningMetadata.ApplicationFormQuestion.InputTypeMap
+const InputType = OpeningMetadata.ApplicationFormQuestion.InputType
+
+export const queryNodeQuestionTypeToMetadataQuestionType = (
+  type: ApplicationFormQuestionType
+): InputTypeMap[keyof InputTypeMap] => {
+  if (type === ApplicationFormQuestionType.Text) {
+    return InputType.TEXT
+  }
+
+  return InputType.TEXTAREA
+}

+ 0 - 973
tests/integration-tests/src/fixtures/workingGroupsModule.ts

@@ -1,973 +0,0 @@
-import { Api } from '../Api'
-import BN from 'bn.js'
-import { assert } from 'chai'
-import { StandardizedFixture } from '../Fixture'
-import { QueryNodeApi } from '../QueryNodeApi'
-import { ApplicationFormQuestionType, EventType, WorkingGroupOpeningType } from '../graphql/generated/schema'
-import {
-  AddUpcomingOpening,
-  ApplicationMetadata,
-  OpeningMetadata,
-  RemoveUpcomingOpening,
-  UpcomingOpeningMetadata,
-  WorkingGroupMetadataAction,
-  WorkingGroupMetadata,
-  SetGroupMetadata,
-} from '@joystream/metadata-protobuf'
-import {
-  WorkingGroupModuleName,
-  AppliedOnOpeningEventDetails,
-  OpeningAddedEventDetails,
-  OpeningFilledEventDetails,
-  lockIdByWorkingGroup,
-  EventDetails,
-} from '../types'
-import { Application, ApplicationId, Opening, OpeningId } from '@joystream/types/working-group'
-import { Utils } from '../utils'
-import _ from 'lodash'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { JoyBTreeSet, MemberId } from '@joystream/types/common'
-import { registry } from '@joystream/types'
-import {
-  OpeningFieldsFragment,
-  OpeningMetadataFieldsFragment,
-  OpeningAddedEventFieldsFragment,
-  ApplicationFieldsFragment,
-  AppliedOnOpeningEventFieldsFragment,
-  OpeningFilledEventFieldsFragment,
-  WorkerFieldsFragment,
-  ApplicationWithdrawnEventFieldsFragment,
-  OpeningCanceledEventFieldsFragment,
-  StatusTextChangedEventFieldsFragment,
-  UpcomingOpeningFieldsFragment,
-  WorkingGroupMetadataFieldsFragment,
-  WorkingGroupFieldsFragment,
-  ApplicationBasicFieldsFragment,
-} from '../graphql/generated/queries'
-import { ISubmittableResult } from '@polkadot/types/types/'
-
-// TODO: Fetch from runtime when possible!
-const MIN_APPLICATION_STAKE = new BN(2000)
-const MIN_USTANKING_PERIOD = 43201
-export const LEADER_OPENING_STAKE = new BN(2000)
-
-export type OpeningParams = {
-  stake: BN
-  unstakingPeriod: number
-  reward: BN
-  metadata: OpeningMetadata.AsObject
-}
-
-export type UpcomingOpeningParams = OpeningParams & {
-  expectedStartTs: number
-}
-
-const queryNodeQuestionTypeToMetadataQuestionType = (type: ApplicationFormQuestionType) => {
-  if (type === ApplicationFormQuestionType.Text) {
-    return OpeningMetadata.ApplicationFormQuestion.InputType.TEXT
-  }
-
-  return OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA
-}
-
-abstract class BaseWorkingGroupFixture extends StandardizedFixture {
-  protected group: WorkingGroupModuleName
-
-  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName) {
-    super(api, query)
-    this.group = group
-  }
-}
-
-abstract class BaseCreateOpeningFixture extends BaseWorkingGroupFixture {
-  protected openingsParams: OpeningParams[]
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    openingsParams?: Partial<OpeningParams>[]
-  ) {
-    super(api, query, group)
-    this.openingsParams = (openingsParams || [{}]).map((params) => _.merge(this.defaultOpeningParams, params))
-  }
-
-  protected defaultOpeningParams: OpeningParams = {
-    stake: MIN_APPLICATION_STAKE,
-    unstakingPeriod: MIN_USTANKING_PERIOD,
-    reward: new BN(10),
-    metadata: {
-      shortDescription: 'Test opening',
-      description: '# Test opening',
-      expectedEndingTimestamp: Date.now() + 60,
-      hiringLimit: 1,
-      applicationDetails: '- This is automatically created opening, do not apply!',
-      applicationFormQuestionsList: [
-        { question: 'Question 1?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXT },
-        { question: 'Question 2?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA },
-      ],
-    },
-  }
-
-  public getDefaultOpeningParams(): OpeningParams {
-    return this.defaultOpeningParams
-  }
-
-  protected getMetadata(openingParams: OpeningParams): OpeningMetadata {
-    const metadataObj = openingParams.metadata as Required<OpeningMetadata.AsObject>
-    const metadata = new OpeningMetadata()
-    metadata.setShortDescription(metadataObj.shortDescription)
-    metadata.setDescription(metadataObj.description)
-    metadata.setExpectedEndingTimestamp(metadataObj.expectedEndingTimestamp)
-    metadata.setHiringLimit(metadataObj.hiringLimit)
-    metadata.setApplicationDetails(metadataObj.applicationDetails)
-    metadataObj.applicationFormQuestionsList.forEach(({ question, type }) => {
-      const applicationFormQuestion = new OpeningMetadata.ApplicationFormQuestion()
-      applicationFormQuestion.setQuestion(question!)
-      applicationFormQuestion.setType(type!)
-      metadata.addApplicationFormQuestions(applicationFormQuestion)
-    })
-
-    return metadata
-  }
-
-  protected assertQueriedOpeningMetadataIsValid(
-    openingParams: OpeningParams,
-    qOpeningMeta: OpeningMetadataFieldsFragment
-  ) {
-    assert.equal(qOpeningMeta.shortDescription, openingParams.metadata.shortDescription)
-    assert.equal(qOpeningMeta.description, openingParams.metadata.description)
-    assert.equal(new Date(qOpeningMeta.expectedEnding).getTime(), openingParams.metadata.expectedEndingTimestamp)
-    assert.equal(qOpeningMeta.hiringLimit, openingParams.metadata.hiringLimit)
-    assert.equal(qOpeningMeta.applicationDetails, openingParams.metadata.applicationDetails)
-    assert.deepEqual(
-      qOpeningMeta.applicationFormQuestions
-        .sort((a, b) => a.index - b.index)
-        .map(({ question, type }) => ({
-          question,
-          type: queryNodeQuestionTypeToMetadataQuestionType(type),
-        })),
-      openingParams.metadata.applicationFormQuestionsList
-    )
-  }
-}
-export class CreateOpeningsFixture extends BaseCreateOpeningFixture {
-  protected asSudo: boolean
-  protected events: OpeningAddedEventDetails[] = []
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    openingsParams?: Partial<OpeningParams>[],
-    asSudo = false
-  ) {
-    super(api, query, group, openingsParams)
-    this.asSudo = asSudo
-  }
-
-  public getCreatedOpeningIds(): OpeningId[] {
-    if (!this.events.length) {
-      throw new Error('Trying to get created opening ids before they were created!')
-    }
-    return this.events.map((e) => e.openingId)
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    const extrinsics = this.openingsParams.map((params) =>
-      this.api.tx[this.group].addOpening(
-        Utils.metadataToBytes(this.getMetadata(params)),
-        this.asSudo ? 'Leader' : 'Regular',
-        { stake_amount: params.stake, leaving_unstaking_period: params.unstakingPeriod },
-        params.reward
-      )
-    )
-
-    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<OpeningAddedEventDetails> {
-    return this.api.retrieveOpeningAddedEventDetails(result, this.group)
-  }
-
-  protected assertQueriedOpeningsAreValid(qOpenings: OpeningFieldsFragment[]): void {
-    this.events.map((e, i) => {
-      const qOpening = qOpenings.find((o) => o.runtimeId === e.openingId.toNumber())
-      const openingParams = this.openingsParams[i]
-      Utils.assert(qOpening, 'Query node: Opening not found')
-      assert.equal(qOpening.runtimeId, e.openingId.toNumber())
-      assert.equal(qOpening.createdAtBlock.number, e.blockNumber)
-      assert.equal(qOpening.group.name, this.group)
-      assert.equal(qOpening.rewardPerBlock, openingParams.reward.toString())
-      assert.equal(qOpening.type, this.asSudo ? WorkingGroupOpeningType.Leader : WorkingGroupOpeningType.Regular)
-      assert.equal(qOpening.status.__typename, 'OpeningStatusOpen')
-      assert.equal(qOpening.stakeAmount, openingParams.stake.toString())
-      assert.equal(qOpening.unstakingPeriod, openingParams.unstakingPeriod)
-      // Metadata
-      this.assertQueriedOpeningMetadataIsValid(openingParams, qOpening.metadata)
-    })
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: OpeningAddedEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.OpeningAdded)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.opening.runtimeId, this.events[i].openingId.toNumber())
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query the events
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getOpeningAddedEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-
-    // Query the openings
-    const qOpenings = await this.query.getOpeningsByIds(
-      this.events.map((e) => e.openingId),
-      this.group
-    )
-    this.assertQueriedOpeningsAreValid(qOpenings)
-  }
-}
-
-export type ApplicantDetails = {
-  memberId: MemberId
-  roleAccount: string
-  rewardAccount: string
-  stakingAccount: string
-}
-
-export type OpeningApplications = {
-  openingId: OpeningId
-  openingMetadata: OpeningMetadata.AsObject
-  applicants: ApplicantDetails[]
-}
-
-export type OpeningApplicationsFlattened = {
-  openingId: OpeningId
-  openingMetadata: OpeningMetadata.AsObject
-  applicant: ApplicantDetails
-}[]
-
-export class ApplyOnOpeningsHappyCaseFixture extends BaseWorkingGroupFixture {
-  protected applications: OpeningApplicationsFlattened
-  protected events: AppliedOnOpeningEventDetails[] = []
-  protected createdApplicationsByOpeningId: Map<number, ApplicationId[]> = new Map<number, ApplicationId[]>()
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    openingsApplications: OpeningApplications[]
-  ) {
-    super(api, query, group)
-    this.applications = this.flattenOpeningApplicationsData(openingsApplications)
-  }
-
-  protected flattenOpeningApplicationsData(openingsApplications: OpeningApplications[]): OpeningApplicationsFlattened {
-    return openingsApplications.reduce(
-      (curr, details) =>
-        curr.concat(
-          details.applicants.map((a) => ({
-            openingId: details.openingId,
-            openingMetadata: details.openingMetadata,
-            applicant: a,
-          }))
-        ),
-      [] as OpeningApplicationsFlattened
-    )
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string[]> {
-    return this.applications.map((a) => a.applicant.roleAccount)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    const openingIds = _.uniq(this.applications.map((a) => a.openingId.toString()))
-    const openings = await this.api.query[this.group].openingById.multi<Opening>(openingIds)
-    return this.applications.map((a, i) => {
-      const openingIndex = openingIds.findIndex((id) => id === a.openingId.toString())
-      const opening = openings[openingIndex]
-      return this.api.tx[this.group].applyOnOpening({
-        member_id: a.applicant.memberId,
-        description: Utils.metadataToBytes(this.getApplicationMetadata(a.openingMetadata, i)),
-        opening_id: a.openingId,
-        reward_account_id: a.applicant.rewardAccount,
-        role_account_id: a.applicant.roleAccount,
-        stake_parameters: {
-          stake: opening.stake_policy.stake_amount,
-          staking_account_id: a.applicant.stakingAccount,
-        },
-      })
-    })
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<AppliedOnOpeningEventDetails> {
-    return this.api.retrieveAppliedOnOpeningEventDetails(result, this.group)
-  }
-
-  public async execute(): Promise<void> {
-    await super.execute()
-    this.applications.map(({ openingId }, i) => {
-      this.createdApplicationsByOpeningId.set(openingId.toNumber(), [
-        ...(this.createdApplicationsByOpeningId.get(openingId.toNumber()) || []),
-        this.events[i].applicationId,
-      ])
-    })
-  }
-
-  public getCreatedApplicationsByOpeningId(openingId: OpeningId): ApplicationId[] {
-    const applicationIds = this.createdApplicationsByOpeningId.get(openingId.toNumber())
-    if (!applicationIds) {
-      throw new Error(`No created application ids by opening id ${openingId.toString()} found!`)
-    }
-    return applicationIds
-  }
-
-  protected getApplicationMetadata(openingMetadata: OpeningMetadata.AsObject, i: number): ApplicationMetadata {
-    const metadata = new ApplicationMetadata()
-    openingMetadata.applicationFormQuestionsList.forEach((question, j) => {
-      metadata.addAnswers(`Answer to question ${j} by applicant number ${i}`)
-    })
-    return metadata
-  }
-
-  protected assertQueriedApplicationsAreValid(qApplications: ApplicationFieldsFragment[]): void {
-    this.events.map((e, i) => {
-      const applicationDetails = this.applications[i]
-      const qApplication = qApplications.find((a) => a.runtimeId === e.applicationId.toNumber())
-      Utils.assert(qApplication, 'Query node: Application not found!')
-      assert.equal(qApplication.runtimeId, e.applicationId.toNumber())
-      assert.equal(qApplication.createdAtBlock.number, e.blockNumber)
-      assert.equal(qApplication.opening.runtimeId, applicationDetails.openingId.toNumber())
-      assert.equal(qApplication.applicant.id, applicationDetails.applicant.memberId.toString())
-      assert.equal(qApplication.roleAccount, applicationDetails.applicant.roleAccount)
-      assert.equal(qApplication.rewardAccount, applicationDetails.applicant.rewardAccount)
-      assert.equal(qApplication.stakingAccount, applicationDetails.applicant.stakingAccount)
-      assert.equal(qApplication.status.__typename, 'ApplicationStatusPending')
-      assert.equal(qApplication.stake, e.params.stake_parameters.stake)
-
-      const applicationMetadata = this.getApplicationMetadata(applicationDetails.openingMetadata, i)
-      assert.deepEqual(
-        qApplication.answers.map(({ question: { question }, answer }) => ({ question, answer })),
-        applicationDetails.openingMetadata.applicationFormQuestionsList.map(({ question }, index) => ({
-          question,
-          answer: applicationMetadata.getAnswersList()[index],
-        }))
-      )
-    })
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: AppliedOnOpeningEventFieldsFragment, i: number): void {
-    const applicationDetails = this.applications[i]
-    assert.equal(qEvent.event.type, EventType.AppliedOnOpening)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.opening.runtimeId, applicationDetails.openingId.toNumber())
-    assert.equal(qEvent.application.runtimeId, this.events[i].applicationId.toNumber())
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query the events
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getAppliedOnOpeningEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-    // Query the applications
-    const qApplications = await this.query.getApplicationsByIds(
-      this.events.map((e) => e.applicationId),
-      this.group
-    )
-    this.assertQueriedApplicationsAreValid(qApplications)
-  }
-}
-
-export class FillOpeningsFixture extends BaseWorkingGroupFixture {
-  protected events: OpeningFilledEventDetails[] = []
-  protected asSudo: boolean
-
-  protected openingIds: OpeningId[]
-  protected acceptedApplicationsIdsArrays: ApplicationId[][]
-
-  protected acceptedApplicationsArrays: Application[][] = []
-  protected applicationStakesArrays: BN[][] = []
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    openingIds: OpeningId[],
-    acceptedApplicationsIdsArrays: ApplicationId[][],
-    asSudo = false
-  ) {
-    super(api, query, group)
-    this.openingIds = openingIds
-    this.acceptedApplicationsIdsArrays = acceptedApplicationsIdsArrays
-    this.asSudo = asSudo
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    const extrinsics = this.openingIds.map((openingId, i) =>
-      this.api.tx[this.group].fillOpening(
-        openingId,
-        new (JoyBTreeSet(ApplicationId))(registry, this.acceptedApplicationsIdsArrays[i])
-      )
-    )
-    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
-  }
-
-  protected getEventFromResult(result: ISubmittableResult): Promise<OpeningFilledEventDetails> {
-    return this.api.retrieveOpeningFilledEventDetails(result, this.group)
-  }
-
-  protected async loadApplicationsData(): Promise<void> {
-    this.acceptedApplicationsArrays = await Promise.all(
-      this.acceptedApplicationsIdsArrays.map((acceptedApplicationIds) =>
-        this.api.query[this.group].applicationById.multi<Application>(acceptedApplicationIds)
-      )
-    )
-    this.applicationStakesArrays = await Promise.all(
-      this.acceptedApplicationsArrays.map((acceptedApplications) =>
-        Promise.all(
-          acceptedApplications.map((a) =>
-            this.api.getStakedBalance(a.staking_account_id, lockIdByWorkingGroup[this.group])
-          )
-        )
-      )
-    )
-  }
-
-  async execute(): Promise<void> {
-    await this.loadApplicationsData()
-    await super.execute()
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: OpeningFilledEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.OpeningFilled)
-    assert.equal(qEvent.opening.runtimeId, this.openingIds[i].toNumber())
-    assert.equal(qEvent.group.name, this.group)
-    this.acceptedApplicationsIdsArrays[i].forEach((acceptedApplId, j) => {
-      // Cannot use "applicationIdToWorkerIdMap.get" here,
-      // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
-      const [, workerId] =
-        Array.from(this.events[i].applicationIdToWorkerIdMap.entries()).find(([applicationId]) =>
-          applicationId.eq(acceptedApplId)
-        ) || []
-      Utils.assert(
-        workerId,
-        `WorkerId for application id ${acceptedApplId.toString()} not found in OpeningFilled event!`
-      )
-      const qWorker = qEvent.workersHired.find((w) => w.runtimeId === workerId.toNumber())
-      Utils.assert(qWorker, `Query node: Worker not found in OpeningFilled.hiredWorkers (id: ${workerId.toString()})`)
-      this.assertHiredWorkerIsValid(
-        this.events[i],
-        this.acceptedApplicationsIdsArrays[i][j],
-        this.acceptedApplicationsArrays[i][j],
-        this.applicationStakesArrays[i][j],
-        qWorker
-      )
-    })
-  }
-
-  protected assertHiredWorkerIsValid(
-    eventDetails: OpeningFilledEventDetails,
-    applicationId: ApplicationId,
-    application: Application,
-    applicationStake: BN,
-    qWorker: WorkerFieldsFragment
-  ): void {
-    assert.equal(qWorker.group.name, this.group)
-    assert.equal(qWorker.membership.id, application.member_id.toString())
-    assert.equal(qWorker.roleAccount, application.role_account_id.toString())
-    assert.equal(qWorker.rewardAccount, application.reward_account_id.toString())
-    assert.equal(qWorker.stakeAccount, application.staking_account_id.toString())
-    assert.equal(qWorker.status.__typename, 'WorkerStatusActive')
-    assert.equal(qWorker.isLead, true)
-    assert.equal(qWorker.stake, applicationStake.toString())
-    assert.equal(qWorker.hiredAtBlock.number, eventDetails.blockNumber)
-    assert.equal(qWorker.application.runtimeId, applicationId.toNumber())
-  }
-
-  protected assertOpeningsStatusesAreValid(
-    qEvents: OpeningFilledEventFieldsFragment[],
-    qOpenings: OpeningFieldsFragment[]
-  ): void {
-    this.events.map((e, i) => {
-      const openingId = this.openingIds[i]
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
-      Utils.assert(qOpening, 'Query node: Opening not found')
-      Utils.assert(qOpening.status.__typename === 'OpeningStatusFilled', 'Query node: Invalid opening status')
-      assert.equal(qOpening.status.openingFilledEventId, qEvent.id)
-    })
-  }
-
-  protected assertApplicationStatusesAreValid(
-    qEvents: OpeningFilledEventFieldsFragment[],
-    qOpenings: OpeningFieldsFragment[]
-  ): void {
-    this.events.map((e, i) => {
-      const openingId = this.openingIds[i]
-      const acceptedApplicationsIds = this.acceptedApplicationsIdsArrays[i]
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
-      Utils.assert(qOpening, 'Query node: Opening not found')
-      qOpening.applications.forEach((qApplication) => {
-        const isAccepted = acceptedApplicationsIds.some((id) => id.toNumber() === qApplication.runtimeId)
-        if (isAccepted) {
-          Utils.assert(qApplication.status.__typename === 'ApplicationStatusAccepted', 'Invalid application status')
-          assert.equal(qApplication.status.openingFilledEventId, qEvent.id)
-        } else {
-          assert.oneOf(qApplication.status.__typename, ['ApplicationStatusRejected', 'ApplicationStatusWithdrawn'])
-          if (qApplication.status.__typename === 'ApplicationStatusRejected') {
-            assert.equal(qApplication.status.openingFilledEventId, qEvent.id)
-          }
-        }
-      })
-    })
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query the event and check event + hiredWorkers
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getOpeningFilledEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-
-    // Check openings statuses
-    const qOpenings = await this.query.getOpeningsByIds(this.openingIds, this.group)
-    this.assertOpeningsStatusesAreValid(qEvents, qOpenings)
-
-    // Check application statuses
-    this.assertApplicationStatusesAreValid(qEvents, qOpenings)
-
-    if (this.asSudo) {
-      const qGroup = await this.query.getWorkingGroup(this.group)
-      Utils.assert(qGroup, 'Query node: Working group not found!')
-      Utils.assert(qGroup.leader, 'Query node: Working group leader not set!')
-      assert.equal(qGroup.leader.runtimeId, qEvents[0].workersHired[0].runtimeId)
-    }
-  }
-}
-
-export class WithdrawApplicationsFixture extends BaseWorkingGroupFixture {
-  protected applicationIds: ApplicationId[]
-  protected accounts: string[]
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    accounts: string[],
-    applicationIds: ApplicationId[]
-  ) {
-    super(api, query, group)
-    this.accounts = accounts
-    this.applicationIds = applicationIds
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string[]> {
-    return this.accounts
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.applicationIds.map((id) => this.api.tx[this.group].withdrawApplication(id))
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'ApplicationWithdrawn')
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: ApplicationWithdrawnEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.ApplicationWithdrawn)
-    assert.equal(qEvent.application.runtimeId, this.applicationIds[i].toNumber())
-    assert.equal(qEvent.group.name, this.group)
-  }
-
-  protected assertApplicationStatusesAreValid(
-    qEvents: ApplicationWithdrawnEventFieldsFragment[],
-    qApplications: ApplicationFieldsFragment[]
-  ): void {
-    this.events.map((e, i) => {
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      const qApplication = qApplications.find((a) => a.runtimeId === this.applicationIds[i].toNumber())
-      Utils.assert(qApplication, 'Query node: Application not found!')
-      Utils.assert(
-        qApplication.status.__typename === 'ApplicationStatusWithdrawn',
-        'Query node: Invalid application status!'
-      )
-      assert.equal(qApplication.status.applicationWithdrawnEventId, qEvent.id)
-    })
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-
-    // Query the evens
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getApplicationWithdrawnEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-
-    // Check application statuses
-    const qApplciations = await this.query.getApplicationsByIds(this.applicationIds, this.group)
-    this.assertApplicationStatusesAreValid(qEvents, qApplciations)
-  }
-}
-
-export class CancelOpeningsFixture extends BaseWorkingGroupFixture {
-  protected openingIds: OpeningId[]
-
-  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, openingIds: OpeningId[]) {
-    super(api, query, group)
-    this.openingIds = openingIds
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.openingIds.map((id) => this.api.tx[this.group].cancelOpening(id))
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'OpeningCanceled')
-  }
-
-  protected assertQueriedOpeningsAreValid(
-    qEvents: OpeningCanceledEventFieldsFragment[],
-    qOpenings: OpeningFieldsFragment[]
-  ): void {
-    this.events.map((e, i) => {
-      const openingId = this.openingIds[i]
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      const qOpening = qOpenings.find((o) => o.runtimeId === openingId.toNumber())
-      Utils.assert(qOpening)
-      Utils.assert(qOpening.status.__typename === 'OpeningStatusCancelled', 'Query node: Invalid opening status')
-      assert.equal(qOpening.status.openingCancelledEventId, qEvent.id)
-      qOpening.applications.forEach((a) => this.assertApplicationStatusIsValid(qEvent, a))
-    })
-  }
-
-  protected assertApplicationStatusIsValid(
-    qEvent: OpeningCanceledEventFieldsFragment,
-    qApplication: ApplicationBasicFieldsFragment
-  ): void {
-    // It's possible that some of the applications have been withdrawn
-    assert.oneOf(qApplication.status.__typename, ['ApplicationStatusWithdrawn', 'ApplicationStatusCancelled'])
-    if (qApplication.status.__typename === 'ApplicationStatusCancelled') {
-      assert.equal(qApplication.status.openingCancelledEventId, qEvent.id)
-    }
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: OpeningCanceledEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.OpeningCanceled)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.opening.runtimeId, this.openingIds[i].toNumber())
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getOpeningCancelledEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-    const qOpenings = await this.query.getOpeningsByIds(this.openingIds, this.group)
-    this.assertQueriedOpeningsAreValid(qEvents, qOpenings)
-  }
-}
-export class CreateUpcomingOpeningsFixture extends BaseCreateOpeningFixture {
-  protected openingsParams: UpcomingOpeningParams[]
-  protected createdUpcomingOpeningIds: string[] = []
-
-  public getDefaultOpeningParams(): UpcomingOpeningParams {
-    return {
-      ...super.getDefaultOpeningParams(),
-      expectedStartTs: Date.now() + 3600,
-    }
-  }
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    openingsParams?: Partial<UpcomingOpeningParams>[]
-  ) {
-    super(api, query, group, openingsParams)
-    this.openingsParams = (openingsParams || [{}]).map((params) => _.merge(this.getDefaultOpeningParams(), params))
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.openingsParams.map((params) => {
-      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(params))
-      return this.api.tx[this.group].setStatusText(metaBytes)
-    })
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StatusTextChanged')
-  }
-
-  public getCreatedUpcomingOpeningIds(): string[] {
-    if (!this.createdUpcomingOpeningIds.length) {
-      throw new Error('Trying to get created UpcomingOpening ids before they are known')
-    }
-    return this.createdUpcomingOpeningIds
-  }
-
-  protected getActionMetadata(openingParams: UpcomingOpeningParams): WorkingGroupMetadataAction {
-    const actionMeta = new WorkingGroupMetadataAction()
-    const addUpcomingOpeningMeta = new AddUpcomingOpening()
-
-    const upcomingOpeningMeta = new UpcomingOpeningMetadata()
-    const openingMeta = this.getMetadata(openingParams)
-    upcomingOpeningMeta.setMetadata(openingMeta)
-    upcomingOpeningMeta.setExpectedStart(openingParams.expectedStartTs)
-    upcomingOpeningMeta.setMinApplicationStake(openingParams.stake.toNumber())
-    upcomingOpeningMeta.setRewardPerBlock(openingParams.reward.toNumber())
-
-    addUpcomingOpeningMeta.setMetadata(upcomingOpeningMeta)
-    actionMeta.setAddUpcomingOpening(addUpcomingOpeningMeta)
-
-    return actionMeta
-  }
-
-  protected assertQueriedUpcomingOpeningsAreValid(
-    qUpcomingOpenings: UpcomingOpeningFieldsFragment[],
-    qEvents: StatusTextChangedEventFieldsFragment[]
-  ): void {
-    this.events.forEach((e, i) => {
-      const openingParams = this.openingsParams[i]
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      const qUpcomingOpening = qUpcomingOpenings.find((o) => o.createdInEvent.id === qEvent.id)
-      Utils.assert(qUpcomingOpening)
-      assert.equal(new Date(qUpcomingOpening.expectedStart).getTime(), openingParams.expectedStartTs)
-      assert.equal(qUpcomingOpening.group.name, this.group)
-      assert.equal(qUpcomingOpening.rewardPerBlock, openingParams.reward.toString())
-      assert.equal(qUpcomingOpening.stakeAmount, openingParams.stake.toString())
-      assert.equal(qUpcomingOpening.createdAtBlock.number, e.blockNumber)
-      this.assertQueriedOpeningMetadataIsValid(openingParams, qUpcomingOpening.metadata)
-      Utils.assert(qEvent.result.__typename === 'UpcomingOpeningAdded')
-      assert.equal(qEvent.result.upcomingOpeningId, qUpcomingOpening.id)
-    })
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.openingsParams[i])).toString())
-    assert.equal(qEvent.result.__typename, 'UpcomingOpeningAdded')
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query the event
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getStatusTextChangedEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-    // Query the opening
-    const qUpcomingOpenings = await this.query.getUpcomingOpeningsByCreatedInEventIds(qEvents.map((e) => e.id))
-    this.assertQueriedUpcomingOpeningsAreValid(qUpcomingOpenings, qEvents)
-
-    this.createdUpcomingOpeningIds = qUpcomingOpenings.map((o) => o.id)
-  }
-}
-
-export class RemoveUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
-  protected upcomingOpeningIds: string[]
-
-  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, upcomingOpeningIds: string[]) {
-    super(api, query, group)
-    this.upcomingOpeningIds = upcomingOpeningIds
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.upcomingOpeningIds.map((id) => {
-      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(id))
-      return this.api.tx[this.group].setStatusText(metaBytes)
-    })
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StatusTextChanged')
-  }
-
-  protected getActionMetadata(upcomingOpeningId: string): WorkingGroupMetadataAction {
-    const actionMeta = new WorkingGroupMetadataAction()
-    const removeUpcomingOpeningMeta = new RemoveUpcomingOpening()
-    removeUpcomingOpeningMeta.setId(upcomingOpeningId)
-    actionMeta.setRemoveUpcomingOpening(removeUpcomingOpeningMeta)
-
-    return actionMeta
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.upcomingOpeningIds[i])).toString())
-    Utils.assert(qEvent.result.__typename === 'UpcomingOpeningRemoved', 'Unexpected StatuxTextChangedEvent result type')
-    assert.equal(qEvent.result.upcomingOpeningId, this.upcomingOpeningIds[i])
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query & check the event
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getStatusTextChangedEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-    // Query the openings and make sure they no longer exist
-    await Promise.all(
-      this.upcomingOpeningIds.map(async (id) => {
-        const qUpcomingOpening = await this.query.getUpcomingOpeningById(id)
-        assert.isNull(qUpcomingOpening)
-      })
-    )
-  }
-}
-
-export class UpdateGroupStatusFixture extends BaseWorkingGroupFixture {
-  protected updates: WorkingGroupMetadata.AsObject[]
-  protected areExtrinsicsOrderSensitive = true
-
-  public constructor(
-    api: Api,
-    query: QueryNodeApi,
-    group: WorkingGroupModuleName,
-    updates: WorkingGroupMetadata.AsObject[]
-  ) {
-    super(api, query, group)
-    this.updates = updates
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string> {
-    return this.api.getLeadRoleKey(this.group)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.updates.map((update) => {
-      const metaBytes = Utils.metadataToBytes(this.getActionMetadata(update))
-      return this.api.tx[this.group].setStatusText(metaBytes)
-    })
-  }
-
-  protected async getEventFromResult(r: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveWorkingGroupsEventDetails(r, this.group, 'StatusTextChanged')
-  }
-
-  protected getActionMetadata(update: WorkingGroupMetadata.AsObject): WorkingGroupMetadataAction {
-    const actionMeta = new WorkingGroupMetadataAction()
-    const setGroupMeta = new SetGroupMetadata()
-    const newGroupMeta = new WorkingGroupMetadata()
-
-    newGroupMeta.setAbout(update.about!)
-    newGroupMeta.setDescription(update.description!)
-    newGroupMeta.setStatus(update.status!)
-    newGroupMeta.setStatusMessage(update.statusMessage!)
-
-    setGroupMeta.setNewMetadata(newGroupMeta)
-    actionMeta.setSetGroupMetadata(setGroupMeta)
-
-    return actionMeta
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: StatusTextChangedEventFieldsFragment, i: number): void {
-    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
-    assert.equal(qEvent.group.name, this.group)
-    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata(this.updates[i])).toString())
-    assert.equal(qEvent.result.__typename, 'WorkingGroupMetadataSet')
-  }
-
-  protected assertQueriedGroupIsValid(
-    qGroup: WorkingGroupFieldsFragment,
-    qMeta: WorkingGroupMetadataFieldsFragment
-  ): void {
-    if (!qGroup.metadata) {
-      throw new Error(`Query node: Group metadata is empty!`)
-    }
-    assert.equal(qGroup.metadata.id, qMeta.id)
-  }
-
-  protected assertQueriedMetadataSnapshotsAreValid(
-    eventDetails: EventDetails,
-    preUpdateSnapshot: WorkingGroupMetadataFieldsFragment | null,
-    postUpdateSnapshot: WorkingGroupMetadataFieldsFragment | null,
-    update: WorkingGroupMetadata.AsObject
-  ): asserts postUpdateSnapshot is WorkingGroupMetadataFieldsFragment {
-    if (!postUpdateSnapshot) {
-      throw new Error('Query node: WorkingGroupMetadata snapshot not found!')
-    }
-    const expectedMeta = _.merge(preUpdateSnapshot, update)
-    assert.equal(postUpdateSnapshot.status, expectedMeta.status)
-    assert.equal(postUpdateSnapshot.statusMessage, expectedMeta.statusMessage)
-    assert.equal(postUpdateSnapshot.description, expectedMeta.description)
-    assert.equal(postUpdateSnapshot.about, expectedMeta.about)
-    assert.equal(postUpdateSnapshot.setAtBlock.number, eventDetails.blockNumber)
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query & check the event
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getStatusTextChangedEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-
-    // Query the group
-    const qGroup = await this.query.getWorkingGroup(this.group)
-    if (!qGroup) {
-      throw new Error('Query node: Working group not found!')
-    }
-
-    // Query & check the metadata snapshots
-    const snapshots = await this.query.getGroupMetaSnapshotsByTimeAsc(qGroup.id)
-    let lastSnapshot: WorkingGroupMetadataFieldsFragment | null = null
-    this.events.forEach((postUpdateEvent, i) => {
-      const postUpdateSnapshotIndex = snapshots.findIndex(
-        (s) =>
-          s.setInEvent.event.id ===
-          this.query.getQueryNodeEventId(postUpdateEvent.blockNumber, postUpdateEvent.indexInBlock)
-      )
-      const postUpdateSnapshot = postUpdateSnapshotIndex > -1 ? snapshots[postUpdateSnapshotIndex] : null
-      const preUpdateSnapshot = postUpdateSnapshotIndex > 0 ? snapshots[postUpdateSnapshotIndex - 1] : null
-      this.assertQueriedMetadataSnapshotsAreValid(
-        postUpdateEvent,
-        preUpdateSnapshot,
-        postUpdateSnapshot,
-        this.updates[i]
-      )
-      const qEvent = qEvents[i]
-      Utils.assert(
-        qEvent.result.__typename === 'WorkingGroupMetadataSet',
-        'Invalid StatusTextChanged event result type'
-      )
-      assert(qEvent.result.metadataId, postUpdateSnapshot.id)
-      lastSnapshot = postUpdateSnapshot
-    })
-
-    // Check the group
-    if (lastSnapshot) {
-      this.assertQueriedGroupIsValid(qGroup, lastSnapshot)
-    }
-  }
-}

+ 1 - 4
tests/integration-tests/src/flows/membership/creatingMemberships.ts

@@ -1,8 +1,5 @@
 import { FlowProps } from '../../Flow'
-import {
-  BuyMembershipHappyCaseFixture,
-  BuyMembershipWithInsufficienFundsFixture,
-} from '../../fixtures/membershipModule'
+import { BuyMembershipHappyCaseFixture, BuyMembershipWithInsufficienFundsFixture } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/invitingMembers.ts

@@ -1,5 +1,5 @@
 import { FlowProps } from '../../Flow'
-import { BuyMembershipHappyCaseFixture, InviteMembersHappyCaseFixture } from '../../fixtures/membershipModule'
+import { BuyMembershipHappyCaseFixture, InviteMembersHappyCaseFixture } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/managingStakingAccounts.ts

@@ -3,7 +3,7 @@ import {
   AddStakingAccountsHappyCaseFixture,
   BuyMembershipHappyCaseFixture,
   RemoveStakingAccountsHappyCaseFixture,
-} from '../../fixtures/membershipModule'
+} from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/membershipSystem.ts

@@ -1,5 +1,5 @@
 import { FlowProps } from '../../Flow'
-import { SudoUpdateMembershipSystem } from '../../fixtures/membershipModule'
+import { SudoUpdateMembershipSystem } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/transferringInvites.ts

@@ -1,5 +1,5 @@
 import { FlowProps } from '../../Flow'
-import { BuyMembershipHappyCaseFixture, TransferInvitesHappyCaseFixture } from '../../fixtures/membershipModule'
+import { BuyMembershipHappyCaseFixture, TransferInvitesHappyCaseFixture } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/updatingAccounts.ts

@@ -1,5 +1,5 @@
 import { FlowProps } from '../../Flow'
-import { BuyMembershipHappyCaseFixture, UpdateAccountsHappyCaseFixture } from '../../fixtures/membershipModule'
+import { BuyMembershipHappyCaseFixture, UpdateAccountsHappyCaseFixture } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 1 - 1
tests/integration-tests/src/flows/membership/updatingProfile.ts

@@ -1,5 +1,5 @@
 import { FlowProps } from '../../Flow'
-import { BuyMembershipHappyCaseFixture, UpdateProfileHappyCaseFixture } from '../../fixtures/membershipModule'
+import { BuyMembershipHappyCaseFixture, UpdateProfileHappyCaseFixture } from '../../fixtures/membership'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'

+ 2 - 2
tests/integration-tests/src/flows/working-groups/groupStatus.ts

@@ -1,9 +1,9 @@
 import { FlowProps } from '../../Flow'
-import { UpdateGroupStatusFixture } from '../../fixtures/workingGroupsModule'
+import { UpdateGroupStatusFixture } from '../../fixtures/workingGroups'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { workingGroups } from '../../types'
+import { workingGroups } from '../../consts'
 import { WorkingGroupMetadata } from '@joystream/metadata-protobuf'
 import _ from 'lodash'
 

+ 3 - 3
tests/integration-tests/src/flows/working-groups/leadOpening.ts

@@ -4,12 +4,12 @@ import {
   CreateOpeningsFixture,
   FillOpeningsFixture,
   ApplicantDetails,
-} from '../../fixtures/workingGroupsModule'
+} from '../../fixtures/workingGroups'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
-import { workingGroups } from '../../types'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { workingGroups } from '../../consts'
 
 export default async function leadOpening({ api, query, env }: FlowProps): Promise<void> {
   await Promise.all(

+ 3 - 4
tests/integration-tests/src/flows/working-groups/openingAndApplicationStatus.ts

@@ -4,14 +4,13 @@ import {
   CancelOpeningsFixture,
   CreateOpeningsFixture,
   WithdrawApplicationsFixture,
-  LEADER_OPENING_STAKE,
   ApplicantDetails,
-} from '../../fixtures/workingGroupsModule'
+} from '../../fixtures/workingGroups'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../../fixtures/membershipModule'
-import { workingGroups } from '../../types'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { workingGroups, LEADER_OPENING_STAKE } from '../../consts'
 import { assert } from 'chai'
 
 export default async function openingAndApplicationStatusFlow({ api, query, env }: FlowProps): Promise<void> {

+ 2 - 2
tests/integration-tests/src/flows/working-groups/upcomingOpenings.ts

@@ -1,9 +1,9 @@
 import { FlowProps } from '../../Flow'
-import { CreateUpcomingOpeningsFixture, RemoveUpcomingOpeningsFixture } from '../../fixtures/workingGroupsModule'
+import { CreateUpcomingOpeningsFixture, RemoveUpcomingOpeningsFixture } from '../../fixtures/workingGroups'
 
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { workingGroups } from '../../types'
+import { workingGroups } from '../../consts'
 
 export default async function upcomingOpenings({ api, query, env }: FlowProps): Promise<void> {
   await Promise.all(

+ 0 - 7
tests/integration-tests/src/types.ts

@@ -89,10 +89,3 @@ export const workingGroups: WorkingGroupModuleName[] = [
   'forumWorkingGroup',
   'membershipWorkingGroup',
 ]
-
-export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
-  storageWorkingGroup: '0x0606060606060606',
-  contentDirectoryWorkingGroup: '0x0707070707070707',
-  forumWorkingGroup: '0x0808080808080808',
-  membershipWorkingGroup: '0x0909090909090909',
-}