Browse Source

Merge branch 'forum-mappings' into olympia-staging

Leszek Wiesner 3 years ago
parent
commit
e98625f5f8

+ 5 - 5
query-node/mappings/forum.ts

@@ -1,7 +1,7 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
+import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
 import { bytesToString, deserializeMetadata, genericEventFields, getWorker } from './common'
 import {
   CategoryCreatedEvent,
@@ -434,10 +434,10 @@ export async function forum_CategoryStickyThreadUpdate({ event, store }: EventCo
   await store.save<CategoryStickyThreadUpdateEvent>(categoryStickyThreadUpdateEvent)
 }
 
-export async function forum_CategoryMembershipOfModeratorUpdated(
-  store: DatabaseManager,
-  event: SubstrateEvent
-): Promise<void> {
+export async function forum_CategoryMembershipOfModeratorUpdated({
+  store,
+  event,
+}: EventContext & StoreContext): Promise<void> {
   const [moderatorId, categoryId, canModerate] = new Forum.CategoryMembershipOfModeratorUpdatedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
   const moderator = await getWorker(store, 'forumWorkingGroup', moderatorId.toNumber())

+ 8 - 5
runtime-modules/forum/src/lib.rs

@@ -209,7 +209,7 @@ pub struct Poll<Timestamp, Hash> {
 
 /// Represents a thread post
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
 pub struct Post<ForumUserId, ThreadId, Hash, Balance, BlockNumber> {
     /// Id of thread to which this post corresponds.
     pub thread_id: ThreadId,
@@ -1333,13 +1333,16 @@ decl_module! {
             rationale: Vec<u8>,
         ) -> DispatchResult {
 
+            // Check only unique post instances.
+            let unique_posts: BTreeSet<_> = posts.into_iter().collect();
+
             // Ensure data migration is done
             Self::ensure_data_migration_done()?;
 
             let account_id = ensure_signed(origin)?;
 
-            let mut deleting_posts = Vec::new();
-            for (category_id, thread_id, post_id, hide) in &posts {
+            let mut deleting_posts = BTreeSet::new();
+            for (category_id, thread_id, post_id, hide) in &unique_posts {
                 // Ensure actor is allowed to moderate post and post is editable
                 let post = Self::ensure_can_delete_post(
                     &account_id,
@@ -1350,7 +1353,7 @@ decl_module! {
                     *hide,
                 )?;
 
-                deleting_posts.push((category_id, thread_id, post_id, post));
+                deleting_posts.insert((category_id, thread_id, post_id, post));
             }
 
             //
@@ -1366,7 +1369,7 @@ decl_module! {
 
             // Generate event
             Self::deposit_event(
-                RawEvent::PostDeleted(rationale, forum_user_id, posts)
+                RawEvent::PostDeleted(rationale, forum_user_id, unique_posts.into_iter().collect())
             );
 
             Ok(())

+ 3 - 0
tests/integration-tests/src/Fixture.ts

@@ -181,6 +181,9 @@ export class FixtureRunner {
   }
 
   public async runQueryNodeChecks(): Promise<void> {
+    if (process.env.SKIP_QUERY_NODE_CHECKS) {
+      return
+    }
     if (!(this.fixture instanceof BaseQueryNodeFixture)) {
       throw new Error('Tried to run query node checks for non-query-node fixture!')
     }

+ 2 - 1
tests/integration-tests/src/fixtures/forum/DeletePostsFixture.ts

@@ -9,6 +9,7 @@ import { assert } from 'chai'
 import { StandardizedFixture } from '../../Fixture'
 import { MemberId, PostId, ThreadId } from '@joystream/types/common'
 import { CategoryId } from '@joystream/types/forum'
+import _ from 'lodash'
 
 const DEFAULT_RATIONALE = 'State cleanup'
 
@@ -78,7 +79,7 @@ export class DeletePostsFixture extends StandardizedFixture {
     assert.equal(qEvent.actor.id, this.removals[i].asMember.toString())
     assert.sameMembers(
       qEvent.posts.map((p) => p.id),
-      this.removals[i].posts.map((p) => p.postId.toString())
+      _.uniq(this.removals[i].posts.map((p) => p.postId.toString()))
     )
     assert.equal(qEvent.rationale, this.removals[i].rationale || DEFAULT_RATIONALE)
   }

+ 50 - 0
tests/integration-tests/src/flows/forum/multiplePostDeletionsBug.ts

@@ -0,0 +1,50 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { DeletePostsFixture, InitializeForumFixture, PostsRemovalInput } from '../../fixtures/forum'
+import { POST_DEPOSIT } from '../../consts'
+import { formatBalance } from '@polkadot/util'
+
+export default async function threads({ api, query, env }: FlowProps): Promise<void> {
+  const debug = Debugger(`flow:multiple-post-deletions-bug`)
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const initializeForumFixture = new InitializeForumFixture(api, query, {
+    numberOfForumMembers: 1,
+    numberOfCategories: 1,
+    threadsPerCategory: 1,
+    postsPerThread: 1,
+  })
+  await new FixtureRunner(initializeForumFixture).run()
+
+  const [memberId] = initializeForumFixture.getCreatedForumMemberIds()
+  const [postPath] = initializeForumFixture.getPostsPaths()
+
+  const memberBalaceBefore = await api.getBalance(await api.getControllerAccountOfMember(memberId))
+
+  const x = parseInt(env.POST_DELETIONS_COUNT || '3')
+  debug(`Deleting same post ${x} times`)
+
+  const postRemovals: PostsRemovalInput[] = [
+    {
+      posts: Array.from({ length: x }, () => ({
+        ...postPath,
+        hide: false,
+      })),
+      asMember: memberId,
+      rationale: 'Getting some free tokens',
+    },
+  ]
+  const deletePostsFixture = new DeletePostsFixture(api, query, postRemovals)
+  const deletePostsRunner = new FixtureRunner(deletePostsFixture)
+  await deletePostsRunner.run()
+
+  const memberBalaceAfter = await api.getBalance(await api.getControllerAccountOfMember(memberId))
+
+  debug('Post deposit:', formatBalance(POST_DEPOSIT))
+  debug('Balance before:', formatBalance(memberBalaceBefore))
+  debug('Balance after:', formatBalance(memberBalaceAfter))
+
+  debug('Done')
+}

+ 1 - 6
tests/integration-tests/src/flows/forum/posts.ts

@@ -171,11 +171,6 @@ export default async function threads({ api, query }: FlowProps): Promise<void>
     },
     {
       posts: [
-        {
-          ...threadPaths[3],
-          postId: postIds[3],
-          hide: true,
-        },
         {
           ...threadPaths[3],
           postId: postIds[3],
@@ -183,7 +178,7 @@ export default async function threads({ api, query }: FlowProps): Promise<void>
         },
       ],
       asMember: memberIds[3],
-      rationale: 'Lock+remove in one extrinsic test',
+      rationale: 'Hide = false test',
     },
   ]
   const deletePostsFixture = new DeletePostsFixture(api, query, postRemovals)

+ 8 - 0
tests/integration-tests/src/scenarios/forumPostDeletionsBug.ts

@@ -0,0 +1,8 @@
+import leadOpening from '../flows/working-groups/leadOpening'
+import multiplePostDeletionsBug from '../flows/forum/multiplePostDeletionsBug'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  const sudoHireLead = job('hiring working group leads', leadOpening)
+  job('forum post deletions bug', multiplePostDeletionsBug).requires(sudoHireLead)
+})

+ 3 - 2
tests/integration-tests/src/scenarios/full.ts

@@ -25,7 +25,8 @@ import { scenario } from '../Scenario'
 scenario(async ({ job }) => {
   // Membership:
   const membershipSystemJob = job('membership system', membershipSystem)
-  // All other membership jobs should be executed after membershipSystemJob,
+
+  // All other jobs should be executed after membershipSystemJob,
   // otherwise changing membershipPrice etc. may break them
   job('creating members', creatingMemberships).after(membershipSystemJob)
   job('updating member profile', updatingMemberProfile).after(membershipSystemJob)
@@ -35,7 +36,7 @@ scenario(async ({ job }) => {
   job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
 
   // Proposals:
-  const councilJob = job('electing council', electCouncil)
+  const councilJob = job('electing council', electCouncil).after(membershipSystemJob)
   const proposalsJob = job('proposals', [proposals, cancellingProposals, vetoProposal]).requires(councilJob)
 
   // Working groups: