Browse Source

query node - council & referendum mappings XI

ondratra 3 years ago
parent
commit
18ffa876bb

+ 46 - 32
query-node/mappings/council.ts

@@ -37,13 +37,14 @@ import {
   // Council & referendum schema types
   CouncilStageUpdate,
   CouncilStageAnnouncing,
+  CouncilStageIdle,
+  CouncilStageElection,
   CouncilStage,
   ElectionProblem,
   Candidate,
   CouncilMember,
   ElectionRound,
   ElectedCouncil,
-  CouncilStageElection,
   VariantNone,
   CastVote,
   CandidacyNoteMetadata,
@@ -84,7 +85,7 @@ async function getCandidate(
     where.electionRound = electionRound
   }
 
-  const candidate = await store.get(Candidate, { where, order: { createdAt: 'DESC' } })
+  const candidate = await store.get(Candidate, { where, order: { createdAt: 'DESC', candidacyWithdrawn: 'ASC' } })
 
   if (!candidate) {
     throw new Error(`Candidate not found. memberId '${memberId}' electionRound '${electionRound?.id}'`)
@@ -138,13 +139,19 @@ async function getCurrentStageUpdate(store: DatabaseManager): Promise<CouncilSta
 /*
   Returns current elected council record.
 */
-async function getCurrentElectedCouncil(
-  store: DatabaseManager,
-  canFail: boolean = false
-): Promise<ElectedCouncil | undefined> {
+async function getCurrentElectedCouncil(store: DatabaseManager): Promise<ElectedCouncil | undefined> {
   const electedCouncil = await store.get(ElectedCouncil, { order: { electedAtBlock: 'DESC' } })
 
-  if (!electedCouncil && !canFail) {
+  return electedCouncil
+}
+
+/*
+  Returns current elected council record. Throws error when no council is elected
+*/
+async function getRequiredCurrentElectedCouncil(store: DatabaseManager): Promise<ElectedCouncil> {
+  const electedCouncil = await getCurrentElectedCouncil(store)
+
+  if (!electedCouncil) {
     throw new Error('No council is elected.')
   }
 
@@ -202,7 +209,7 @@ async function updateCouncilStage(
   blockNumber: number,
   electionProblem?: ElectionProblem
 ): Promise<void> {
-  const electedCouncil = await getCurrentElectedCouncil(store, true)
+  const electedCouncil = await getCurrentElectedCouncil(store)
   if (!electedCouncil) {
     return
   }
@@ -247,6 +254,7 @@ async function startNextElectionRound(
   await store.save<ElectionRound>(electionRound)
 
   // update council stage
+
   const stage = new CouncilStageAnnouncing()
   stage.candidatesCount = new BN(0)
   await updateCouncilStage(store, stage, blockNumber, electionProblem)
@@ -265,8 +273,7 @@ async function convertCandidatesToCouncilMembers(
   const councilMembers = await candidates.reduce(async (councilMembersPromise, candidate) => {
     const councilMembers = await councilMembersPromise
 
-    // cast to any needed because member is not eagerly loaded into candidate object
-    const member = new Membership({ id: (candidate as any).memberId.toString() })
+    const member = new Membership({ id: candidate.member.id.toString() })
 
     const councilMember = new CouncilMember({
       // id: candidate.id // TODO: are ids needed?
@@ -307,7 +314,7 @@ export async function council_AnnouncingPeriodStarted({ event, store }: EventCon
   // specific event processing
 
   // restart elections
-  const electedCouncil = (await getCurrentElectedCouncil(store))!
+  const electedCouncil = await getRequiredCurrentElectedCouncil(store)
   await startNextElectionRound(store, electedCouncil, event.blockNumber)
 }
 
@@ -328,7 +335,7 @@ export async function council_NotEnoughCandidates({ event, store }: EventContext
   // specific event processing
 
   // restart elections
-  const electedCouncil = (await getCurrentElectedCouncil(store))!
+  const electedCouncil = await getRequiredCurrentElectedCouncil(store)
   await startNextElectionRound(store, electedCouncil, event.blockNumber, ElectionProblem.NOT_ENOUGH_CANDIDATES)
 }
 
@@ -426,7 +433,7 @@ export async function council_NewCouncilElected({ event, store }: EventContext &
   // specific event processing
 
   // mark old council as resinged
-  const oldElectedCouncil = await getCurrentElectedCouncil(store, true)
+  const oldElectedCouncil = await getCurrentElectedCouncil(store)
   if (oldElectedCouncil) {
     oldElectedCouncil.isResigned = true
     oldElectedCouncil.endedAtBlock = event.blockNumber
@@ -441,8 +448,8 @@ export async function council_NewCouncilElected({ event, store }: EventContext &
   // TODO: uncomment when following query will be working (after some QN patches make it to Olympia)
   ////const electedCandidates = await store.getMany(Candidate, { where: { electionRoundId: electionRound.id, member: { id_in: electedMemberIds } } })
   const electedCandidates = (
-    await store.getMany(Candidate, { where: { electionRoundId: electionRound.id } })
-  ).filter((item: Candidate) => electedMemberIds.find((tmpId) => tmpId == (item as any).memberId.toString()))
+    await store.getMany(Candidate, { where: { electionRoundId: electionRound.id }, relations: ['member'] })
+  ).filter((item: Candidate) => electedMemberIds.find((tmpId) => tmpId == item.member.id.toString()))
 
   // create new council record
   const electedCouncil = new ElectedCouncil({
@@ -467,8 +474,9 @@ export async function council_NewCouncilElected({ event, store }: EventContext &
     })
   )
 
-  // end the last election round and start new one
-  await startNextElectionRound(store, electedCouncil, event.blockNumber)
+  // add council stage update
+  const stage = new CouncilStageIdle()
+  await updateCouncilStage(store, stage, event.blockNumber)
 
   // unset `isCouncilMember` sign for old council's members
   const oldElectedMembers = await store.getMany(Membership, { where: { isCouncilMember: true } })
@@ -518,7 +526,7 @@ export async function council_NewCouncilNotElected({ event, store }: EventContex
   // specific event processing
 
   // restart elections
-  const electedCouncil = (await getCurrentElectedCouncil(store))!
+  const electedCouncil = await getRequiredCurrentElectedCouncil(store)
   await startNextElectionRound(store, electedCouncil, event.blockNumber, ElectionProblem.NEW_COUNCIL_NOT_ELECTED)
 }
 
@@ -581,26 +589,37 @@ export async function council_CandidacyNoteSet({ event, store }: EventContext &
   const electionRound = await getCurrentElectionRound(store)
   const candidate = await getCandidate(store, memberId.toString(), electionRound)
 
+  const areBulletPointsSet = (metadataBulletPoints: string[] | null | undefined) => !!metadataBulletPoints
+  const areBulletPointsBeingUnset = (metadataBulletPoints: string[]) => {
+    // assumes areBulletPointsSet() were checked before
+
+    return metadataBulletPoints.length && metadataBulletPoints[0] == ''
+  }
+
   // unpack note's metadata and save it to db
   const metadata = deserializeMetadata(CouncilCandidacyNoteMetadata, note)
   const noteMetadata = candidate.noteMetadata
   // `XXX || (null as any)` construct clears metadata if requested (see https://github.com/Joystream/hydra/issues/435)
-  noteMetadata.header = isSet(metadata?.header) ? metadata?.header || (null as any) : metadata?.header
-  noteMetadata.bulletPoints = metadata?.bulletPoints || []
+  noteMetadata.header = isSet(metadata?.header) ? metadata?.header || (null as any) : noteMetadata?.header
+  noteMetadata.bulletPoints = areBulletPointsSet(metadata?.bulletPoints)
+    ? areBulletPointsBeingUnset(metadata?.bulletPoints as string[]) // check deletion request
+      ? [] // empty bullet points
+      : (metadata?.bulletPoints as string[]) // set new value
+    : noteMetadata?.bulletPoints // keep previous value
   noteMetadata.bannerImageUri = isSet(metadata?.bannerImageUri)
     ? metadata?.bannerImageUri || (null as any)
-    : metadata?.bannerImageUri
+    : noteMetadata?.bannerImageUri
   noteMetadata.description = isSet(metadata?.description)
     ? metadata?.description || (null as any)
-    : metadata?.description
+    : noteMetadata?.description
   await store.save<CandidacyNoteMetadata>(noteMetadata)
 
   // save metadata set by this event
   const noteMetadataSnapshot = new CandidacyNoteMetadata({
-    header: metadata?.header || undefined,
-    bulletPoints: metadata?.bulletPoints || [],
-    bannerImageUri: metadata?.bannerImageUri || undefined,
-    description: metadata?.description || undefined,
+    header: metadata?.header ?? undefined,
+    bulletPoints: areBulletPointsSet(metadata?.bulletPoints) ? (metadata?.bulletPoints as string[]) : undefined,
+    bannerImageUri: metadata?.bannerImageUri ?? undefined,
+    description: metadata?.description ?? undefined,
   })
 
   await store.save<CandidacyNoteMetadata>(noteMetadataSnapshot)
@@ -817,17 +836,13 @@ export async function referendum_ReferendumFinished({ event, store }: EventConte
 
   const [optionResultsRaw] = new Referendum.ReferendumFinishedEvent(event).params
 
-  const members = await store.getMany(Membership, {
-    where: { id: optionResultsRaw.map((item) => item.option_id.toString()) },
-  })
-
   const referendumFinishedEvent = new ReferendumFinishedEvent({
     ...genericEventFields(event),
     optionResults: optionResultsRaw.map(
       (item, index) =>
         new ReferendumStageRevealingOptionResult({
           votePower: item.vote_power,
-          option: members.find((tmpMember) => tmpMember.id.toString() == optionResultsRaw[index].option_id.toString()),
+          option: optionResultsRaw[index].option_id.toString(),
         })
     ),
   })
@@ -877,7 +892,6 @@ export async function referendum_VoteRevealed({ event, store }: EventContext & S
   // common event processing - init
 
   const [account, memberId, salt] = new Referendum.VoteRevealedEvent(event).params
-  const member = await getMembership(store, memberId.toString())
 
   // specific event processing
 

+ 4 - 6
query-node/schemas/council.graphql

@@ -110,8 +110,8 @@ type CandidacyNoteMetadata @entity {
   "Candidacy header text."
   header: String
 
-  "Candidate program in form of bullet points."
-  bulletPoints: [String!]!
+  "Candidate program in form of bullet points. Takes array with one empty string [''] as deletion request."
+  bulletPoints: [String!]
 
   "Image uri of candidate's banner."
   bannerImageUri: String
@@ -229,14 +229,12 @@ type ElectedCouncil @entity {
   "Network running at the time of resignation."
   endedAtNetwork: Network
 
-  # it might seem that derived field is wrongly set to `nextElectedCouncil`, but that's how it should be
+  # it might seems that derived field is wrongly set to `nextElectedCouncil`, but that's how it should be
   "Elections held before the council was rightfully elected."
-  #councilElections: [ElectionRound!]! @derivedFrom(field: "electedCouncil")
   councilElections: [ElectionRound!]! @derivedFrom(field: "nextElectedCouncil")
 
-  # it might seem that derived field is wrongly set to `electedCouncil`, but that's how it should be
+  # it might seems that derived field is wrongly set to `electedCouncil`, but that's how it should be
   "Elections held before the next council was or will be rightfully elected."
-  #nextCouncilElections: [ElectionRound!]! @derivedFrom(field: "nextElectedCouncil")
   nextCouncilElections: [ElectionRound!]! @derivedFrom(field: "electedCouncil")
 
   "Sign if council is already resigned."

+ 0 - 214
tests/integration-tests/src/fixtures/council/FailToElectCouncilFixture.ts

@@ -1,214 +0,0 @@
-import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
-import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
-import { blake2AsHex } from '@polkadot/util-crypto'
-import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
-import { ForumThreadWithInitialPostFragment, ThreadCreatedEventFieldsFragment } from '../../graphql/generated/queries'
-import { assertCouncilMembersRuntimeQnMatch } from './common'
-import { Balance } from '@polkadot/types/interfaces'
-import { MemberId } from '@joystream/types/common'
-import { assert } from 'chai'
-
-interface IScenarioResources {
-  candidatesMemberIds: MemberId[]
-  candidatesStakingAccounts: string[]
-  candidatesMemberAccounts: string[]
-  councilCandidateStake: Balance
-  councilMemberIds: MemberId[]
-}
-
-export class FailToElectCouncilFixture extends BaseQueryNodeFixture {
-  /*
-    Execute all scenarios when a new council election is expected to fail.
-  */
-  public async execute(): Promise<void> {
-    const { api, query } = this
-
-    const resources1 = await this.prepareScenarioResources()
-    await this.executeNotEnoughCandidates(resources1)
-
-    const resources2 = await this.prepareScenarioResources()
-    await this.executeNotEnoughCandidatesWithVotes(resources2)
-  }
-
-  /*
-    Prepares resources used by fail-to-elect council scenarios.
-  */
-  private async prepareScenarioResources(): Promise<IScenarioResources> {
-    const { api, query } = this
-
-    const { councilSize, minNumberOfExtraCandidates } = api.consts.council
-    const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
-
-    // prepare memberships
-    const candidatesMemberAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
-    const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, candidatesMemberAccounts)
-    await new FixtureRunner(buyMembershipsFixture).run()
-    const candidatesMemberIds = buyMembershipsFixture.getCreatedMembers()
-
-    // prepare staking accounts
-    const councilCandidateStake = api.consts.council.minCandidateStake
-
-    const candidatesStakingAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
-    const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
-      api,
-      query,
-      candidatesStakingAccounts.map((account, i) => ({
-        asMember: candidatesMemberIds[i],
-        account,
-        stakeAmount: councilCandidateStake,
-      }))
-    )
-    await new FixtureRunner(addStakingAccountsFixture).run()
-
-    // retrieve currently elected council's members
-    const councilMembers = await this.api.query.council.councilMembers()
-    const councilMemberIds = councilMembers.map((item) => item.membership_id)
-
-    return {
-      candidatesMemberIds,
-      candidatesStakingAccounts,
-      candidatesMemberAccounts,
-      councilCandidateStake,
-      councilMemberIds,
-    }
-  }
-
-  /*
-    Execute scenario when not enough candidates announce their candidacy and candidacy announcement stage
-    has to be repeated.
-  */
-  private async executeNotEnoughCandidates({
-    candidatesMemberIds,
-    candidatesStakingAccounts,
-    candidatesMemberAccounts,
-    councilCandidateStake,
-    councilMemberIds,
-  }: IScenarioResources) {
-    const { api, query } = this
-
-    const lessCandidatesNumber = 1
-    const candidatingMemberIds = candidatesMemberIds.slice(0, candidatesMemberIds.length - lessCandidatesNumber)
-
-    // announcing stage
-    await this.api.untilCouncilStage('Announcing')
-
-    // ensure no voting is in progress
-    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
-    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
-
-    // announce candidacies
-    const applyForCouncilTxs = candidatingMemberIds.map((memberId, i) =>
-      api.tx.council.announceCandidacy(
-        memberId,
-        candidatesStakingAccounts[i],
-        candidatesMemberAccounts[i],
-        councilCandidateStake
-      )
-    )
-    await api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
-    await api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
-
-    // wait for next announcement stage that should be right after the previous one
-    await this.api.untilCouncilStage('Announcing', announcementPeriodNrInit.toNumber() + 1)
-    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
-
-    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
-    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
-
-    // ensure council members haven't changed
-    const councilMembersEnding = await this.api.query.council.councilMembers()
-    assert.sameMembers(
-      councilMemberIds.map((item) => item.toString()),
-      councilMembersEnding.map((item) => item.membership_id.toString())
-    )
-
-    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
-  }
-
-  /*
-    Execute scenario when enough candidates announce their candidacy but not enough of them receive any votes
-    and council can't be fully filled.
-  */
-  private async executeNotEnoughCandidatesWithVotes({
-    candidatesMemberIds,
-    candidatesStakingAccounts,
-    candidatesMemberAccounts,
-    councilCandidateStake,
-    councilMemberIds,
-  }: IScenarioResources) {
-    const { api, query } = this
-
-    const lessVotersNumber = 1
-    const numberOfCandidates = candidatesMemberIds.length
-    const numberOfVoters = numberOfCandidates - 1
-
-    // create voters
-    const voteStake = api.consts.referendum.minimumStake
-    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
-    await api.treasuryTransferBalanceToAccounts(votersStakingAccounts, voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE))
-
-    // announcing stage
-    await this.api.untilCouncilStage('Announcing')
-
-    // ensure no voting is in progress
-    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
-    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
-
-    // announce candidacies
-    const applyForCouncilTxs = candidatesMemberIds.map((memberId, i) =>
-      api.tx.council.announceCandidacy(
-        memberId,
-        candidatesStakingAccounts[i],
-        candidatesMemberAccounts[i],
-        councilCandidateStake
-      )
-    )
-    await api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
-    await api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
-
-    // voting stage
-    await this.api.untilCouncilStage('Voting')
-
-    // vote
-    const cycleId = (await this.api.query.referendum.stage()).asType('Voting').current_cycle_id
-    const votingTxs = votersStakingAccounts.map((account, i) => {
-      const accountId = api.createType('AccountId', account)
-      const optionId = candidatesMemberIds[i % numberOfCandidates]
-      const salt = api.createType('Bytes', `salt${i}`)
-
-      const payload = Buffer.concat([accountId.toU8a(), optionId.toU8a(), salt.toU8a(), cycleId.toU8a()])
-      const commitment = blake2AsHex(payload)
-      return api.tx.referendum.vote(commitment, voteStake)
-    })
-    await api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
-    await api.sendExtrinsicsAndGetResults(votingTxs, votersStakingAccounts)
-
-    // Announcing stage
-    await this.api.untilCouncilStage('Announcing')
-    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
-
-    // ensure new announcement stage started
-    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
-    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
-
-    // ensure council members haven't changed
-    const councilMembersEnding = await this.api.query.council.councilMembers()
-    assert.sameMembers(
-      councilMemberIds.map((item) => item.toString()),
-      councilMembersEnding.map((item) => item.membership_id.toString())
-    )
-
-    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
-  }
-
-  async aaa({ councilMemberIds }: IScenarioResources) {
-    // ensure council members haven't changed
-    const councilMembersEnding = await this.api.query.council.councilMembers()
-    assert.sameMembers(
-      councilMemberIds.map((item) => item.toString()),
-      councilMembersEnding.map((item) => item.membership_id.toString())
-    )
-
-    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
-  }
-}

+ 57 - 0
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts

@@ -0,0 +1,57 @@
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { assertCouncilMembersRuntimeQnMatch, prepareFailtToElectResources } from './common'
+import { assert } from 'chai'
+
+export class NotEnoughCandidatesFixture extends BaseQueryNodeFixture {
+  /*
+      Execute scenario when not enough candidates announce their candidacy and candidacy announcement stage
+      has to be repeated.
+  */
+  public async execute(): Promise<void> {
+    const {
+      candidatesMemberIds,
+      candidatesStakingAccounts,
+      candidatesMemberAccounts,
+      councilCandidateStake,
+      councilMemberIds,
+    } = await prepareFailtToElectResources(this.api, this.query)
+
+    const lessCandidatesNumber = 1
+    const candidatingMemberIds = candidatesMemberIds.slice(0, candidatesMemberIds.length - lessCandidatesNumber)
+
+    // announcing stage
+    await this.api.untilCouncilStage('Announcing')
+
+    // ensure no voting is in progress
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
+
+    // announce candidacies
+    const applyForCouncilTxs = candidatingMemberIds.map((memberId, i) =>
+      this.api.tx.council.announceCandidacy(
+        memberId,
+        candidatesStakingAccounts[i],
+        candidatesMemberAccounts[i],
+        councilCandidateStake
+      )
+    )
+    await this.api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
+    await this.api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
+
+    // wait for next announcement stage that should be right after the previous one
+    await this.api.untilCouncilStage('Announcing', announcementPeriodNrInit.toNumber() + 1)
+    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
+
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
+
+    // ensure council members haven't changed
+    const councilMembersEnding = await this.api.query.council.councilMembers()
+    assert.sameMembers(
+      councilMemberIds.map((item) => item.toString()),
+      councilMembersEnding.map((item) => item.membership_id.toString())
+    )
+
+    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
+  }
+}

+ 82 - 0
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts

@@ -0,0 +1,82 @@
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { assertCouncilMembersRuntimeQnMatch, prepareFailtToElectResources } from './common'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import { assert } from 'chai'
+import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
+
+export class NotEnoughCandidatesWithVotesFixture extends BaseQueryNodeFixture {
+  public async execute(): Promise<void> {
+    const {
+      candidatesMemberIds,
+      candidatesStakingAccounts,
+      candidatesMemberAccounts,
+      councilCandidateStake,
+      councilMemberIds,
+    } = await prepareFailtToElectResources(this.api, this.query)
+
+    const lessVotersNumber = 1
+    const numberOfCandidates = candidatesMemberIds.length
+    const numberOfVoters = numberOfCandidates - 1
+
+    // create voters
+    const voteStake = this.api.consts.referendum.minimumStake
+    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
+    await this.api.treasuryTransferBalanceToAccounts(
+      votersStakingAccounts,
+      voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
+    )
+
+    // announcing stage
+    await this.api.untilCouncilStage('Announcing')
+
+    // ensure no voting is in progress
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
+
+    // announce candidacies
+    const applyForCouncilTxs = candidatesMemberIds.map((memberId, i) =>
+      this.api.tx.council.announceCandidacy(
+        memberId,
+        candidatesStakingAccounts[i],
+        candidatesMemberAccounts[i],
+        councilCandidateStake
+      )
+    )
+    await this.api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
+    await this.api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
+
+    // voting stage
+    await this.api.untilCouncilStage('Voting')
+
+    // vote
+    const cycleId = (await this.api.query.referendum.stage()).asType('Voting').current_cycle_id
+    const votingTxs = votersStakingAccounts.map((account, i) => {
+      const accountId = this.api.createType('AccountId', account)
+      const optionId = candidatesMemberIds[i % numberOfCandidates]
+      const salt = this.api.createType('Bytes', `salt${i}`)
+
+      const payload = Buffer.concat([accountId.toU8a(), optionId.toU8a(), salt.toU8a(), cycleId.toU8a()])
+      const commitment = blake2AsHex(payload)
+      return this.api.tx.referendum.vote(commitment, voteStake)
+    })
+    await this.api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
+    await this.api.sendExtrinsicsAndGetResults(votingTxs, votersStakingAccounts)
+
+    // Announcing stage
+    await this.api.untilCouncilStage('Announcing')
+    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
+
+    // ensure new announcement stage started
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
+
+    // ensure council members haven't changed
+    const councilMembersEnding = await this.api.query.council.councilMembers()
+    assert.sameMembers(
+      councilMemberIds.map((item) => item.toString()),
+      councilMembersEnding.map((item) => item.membership_id.toString())
+    )
+
+    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
+  }
+}

+ 51 - 3
tests/integration-tests/src/fixtures/council/common.ts

@@ -1,15 +1,25 @@
 import { assert } from 'chai'
 import { Api } from '../../Api'
 import { QueryNodeApi } from '../../QueryNodeApi'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
+import { FixtureRunner } from '../../Fixture'
+import { MemberId } from '@joystream/types/common'
+import { Balance } from '@polkadot/types/interfaces'
+
+interface IFailToElectResources {
+  candidatesMemberIds: MemberId[]
+  candidatesStakingAccounts: string[]
+  candidatesMemberAccounts: string[]
+  councilCandidateStake: Balance
+  councilMemberIds: MemberId[]
+}
 
 export async function assertCouncilMembersRuntimeQnMatch(api: Api, query: QueryNodeApi) {
   const runtimeCouncilMembers = await api.query.council.councilMembers()
-  runtimeCouncilMembers.map((item) => console.log(item.toString()))
 
-  const tmp = await query.tryQueryWithTimeout(
+  await query.tryQueryWithTimeout(
     () => query.getCurrentCouncilMembers(),
     (qnElectedCouncil) => {
-      console.log(qnElectedCouncil, qnElectedCouncil?.councilMembers)
       assert.sameMembers(
         (qnElectedCouncil?.councilMembers || []).map((item: any) => item.member.id.toString()),
         runtimeCouncilMembers.map((item: any) => item.membership_id.toString())
@@ -17,3 +27,41 @@ export async function assertCouncilMembersRuntimeQnMatch(api: Api, query: QueryN
     }
   )
 }
+
+export async function prepareFailtToElectResources(api: Api, query: QueryNodeApi): Promise<IFailToElectResources> {
+  const { councilSize, minNumberOfExtraCandidates } = api.consts.council
+  const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
+
+  // prepare memberships
+  const candidatesMemberAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, candidatesMemberAccounts)
+  await new FixtureRunner(buyMembershipsFixture).run()
+  const candidatesMemberIds = buyMembershipsFixture.getCreatedMembers()
+
+  // prepare staking accounts
+  const councilCandidateStake = api.consts.council.minCandidateStake
+
+  const candidatesStakingAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
+    api,
+    query,
+    candidatesStakingAccounts.map((account, i) => ({
+      asMember: candidatesMemberIds[i],
+      account,
+      stakeAmount: councilCandidateStake,
+    }))
+  )
+  await new FixtureRunner(addStakingAccountsFixture).run()
+
+  // retrieve currently elected council's members
+  const councilMembers = await api.query.council.councilMembers()
+  const councilMemberIds = councilMembers.map((item) => item.membership_id)
+
+  return {
+    candidatesMemberIds,
+    candidatesStakingAccounts,
+    candidatesMemberAccounts,
+    councilCandidateStake,
+    councilMemberIds,
+  }
+}

+ 2 - 0
tests/integration-tests/src/fixtures/council/index.ts

@@ -1 +1,3 @@
 export { ElectCouncilFixture } from './ElectCouncilFixture'
+export { NotEnoughCandidatesFixture } from './NotEnoughCandidatesFixture'
+export { NotEnoughCandidatesWithVotesFixture } from './NotEnoughCandidatesWithVotesFixture'

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

@@ -35,7 +35,6 @@ export class UpdateProfileHappyCaseFixture extends BaseQueryNodeFixture {
     this.memberContext = memberContext
     this.oldValues = oldValues
     this.newValues = newValues
-    console.log({ oldValues, newValues })
   }
 
   private assertProfileUpdateSuccesful(qMember: MembershipFieldsFragment | null) {

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

@@ -180,7 +180,6 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
         const isAccepted = acceptedApplicationsIds.some((id) => id.toNumber() === qApplication.runtimeId)
         if (isAccepted) {
           Utils.assert(qApplication.status.__typename === 'ApplicationStatusAccepted', 'Invalid application status')
-          console.log('qApplication.status', qApplication.status)
           // FIXME: Missing due to Hydra bug now
           // Utils.assert(qApplication.status.openingFilledEvent, 'Query node: Missing openingFilledEvent relation')
           // assert.equal(qApplication.status.openingFilledEvent.id, qEvent.id)

+ 6 - 3
tests/integration-tests/src/flows/council/failToElect.ts

@@ -1,7 +1,7 @@
 import { FlowProps } from '../../Flow'
 import { extendDebug } from '../../Debugger'
 import { FixtureRunner } from '../../Fixture'
-import { FailToElectCouncilFixture } from '../../fixtures/council/FailToElectCouncilFixture'
+import { NotEnoughCandidatesFixture, NotEnoughCandidatesWithVotesFixture } from '../../fixtures/council'
 
 // Currently only used by Olympia flow
 
@@ -10,8 +10,11 @@ export default async function failToElectCouncil({ api, query }: FlowProps): Pro
   debug('Started')
   api.enableDebugTxLogs()
 
-  const failToElectCouncilFixture = new FailToElectCouncilFixture(api, query)
-  await new FixtureRunner(failToElectCouncilFixture).run()
+  const notEnoughCandidatesFixture = new NotEnoughCandidatesFixture(api, query)
+  await new FixtureRunner(notEnoughCandidatesFixture).run()
+
+  const notEnoughCandidatesWithVotesFixture = new NotEnoughCandidatesWithVotesFixture(api, query)
+  await new FixtureRunner(notEnoughCandidatesWithVotesFixture).run()
 
   debug('Done')
 }