From b0786647ed0d10b9de186b196ffe03fa85033b22 Mon Sep 17 00:00:00 2001 From: tsukimi2 Date: Mon, 7 Sep 2020 22:48:22 +0800 Subject: [PATCH] Bugfix challenge tags to normal tags after a challenge has ended/deleted (#12341) * Fix bug in challenge tags not converted to normal tags after challenge ended/deleted * Added test cases to test bug fix * Set tag.challenge from String to Boolean in tag model schema * Update existing test with tag challenge set to boolean instead of string * Added migration file for converting tag challenge field from string to bool * Implement suggestions from ilnt * Use mongoose instead of Mock in migration * Change from update to bulkwrite * update users individually Co-authored-by: Matteo Pagliazzi --- .../users/tag-challenge-field-string2bool.js | 73 +++++++++++++++++++ ...lenges_challengeId_winner_winnerId.test.js | 2 +- .../tests/unit/components/tasks/user.spec.js | 65 +++++++++++++++++ website/server/models/tag.js | 2 +- 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 migrations/users/tag-challenge-field-string2bool.js create mode 100644 website/client/tests/unit/components/tasks/user.spec.js diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js new file mode 100644 index 0000000000..27bb1f3446 --- /dev/null +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -0,0 +1,73 @@ +import { model as User } from '../../website/server/models/user'; + +const MIGRATION_NAME = 'tag-challenge-field-string2bool'; + +const progressCount = 1000; +let count = 0; + +export default async function processUsers () { + const query = { + migration: { $ne: MIGRATION_NAME }, + tags: { + $elemMatch: { + challenge: { + $exists: true, + $type: 'string', + }, + }, + }, + }; + + while (true) { // eslint-disable-line no-constant-condition + // eslint-disable-next-line no-await-in-loop + const users = await User.find(query) + .sort({ _id: 1 }) + .limit(250) + .select({ _id: 1, tags: 1 }) + .lean() + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1], + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +} + +async function updateUser (user) { + count += 1; + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + let requiresUpdate = false; + + if (user && user.tags) { + user.tags.forEach(tag => { + if (tag && typeof tag.challenge === 'string') { + requiresUpdate = true; + if (tag.challenge === 'true') { + tag.challenge = true; + } else if (tag.challenge === 'false') { + tag.challenge = false; + } else { + tag.challenge = null; + } + } + }); + } + + if (requiresUpdate) { + const set = { + migration: MIGRATION_NAME, + tags: user.tags, + }; + return User.update({ _id: user._id }, { $set: set }).exec(); + } + + return null; +} diff --git a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js index 3f3b75e7fe..afae509347 100644 --- a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js +++ b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js @@ -159,7 +159,7 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => { expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED'); expect(testTask.challenge.winner).to.eql(winningUser.profile.name); - expect(challengeTag.challenge).to.eql('false'); + expect(challengeTag.challenge).to.eql(false); }); }); }); diff --git a/website/client/tests/unit/components/tasks/user.spec.js b/website/client/tests/unit/components/tasks/user.spec.js new file mode 100644 index 0000000000..2bf0097eb6 --- /dev/null +++ b/website/client/tests/unit/components/tasks/user.spec.js @@ -0,0 +1,65 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import User from '@/components/tasks/user.vue'; +import Store from '@/libs/store'; + +const localVue = createLocalVue(); +localVue.use(Store); + +describe('Tasks User', () => { + describe('Computed Properties', () => { + it('should render a challenge tag under challenge header in tag filter popup when the challenge is active', () => { + const activeChallengeTag = { + id: '1', + name: 'Challenge1', + challenge: true, + }; + const state = { + user: { + data: { + tags: [activeChallengeTag], + }, + }, + }; + const getters = {}; + const store = new Store({ state, getters }); + const wrapper = shallowMount(User, { + store, + localVue, + }); + + const computedTagsByType = wrapper.vm.tagsByType; + + expect(computedTagsByType.challenges.tags.length).to.equal(1); + expect(computedTagsByType.challenges.tags[0].id).to.equal(activeChallengeTag.id); + expect(computedTagsByType.challenges.tags[0].name).to.equal(activeChallengeTag.name); + }); + + it('should render a challenge tag under normal tag header in tag filter popup when the challenge is no longer active', () => { + const inactiveChallengeTag = { + id: '1', + name: 'Challenge1', + challenge: false, + }; + const state = { + user: { + data: { + tags: [inactiveChallengeTag], + }, + }, + }; + const getters = {}; + const store = new Store({ state, getters }); + const wrapper = shallowMount(User, { + store, + localVue, + }); + + const computedTagsByType = wrapper.vm.tagsByType; + + expect(computedTagsByType.challenges.tags.length).to.equal(0); + expect(computedTagsByType.user.tags.length).to.equal(1); + expect(computedTagsByType.user.tags[0].id).to.equal(inactiveChallengeTag.id); + expect(computedTagsByType.user.tags[0].name).to.equal(inactiveChallengeTag.name); + }); + }); +}); diff --git a/website/server/models/tag.js b/website/server/models/tag.js index 94afd95bd8..1ad8dcb2a6 100644 --- a/website/server/models/tag.js +++ b/website/server/models/tag.js @@ -13,7 +13,7 @@ export const schema = new Schema({ required: true, }, name: { $type: String, required: true }, - challenge: { $type: String }, + challenge: { $type: Boolean }, group: { $type: String }, }, { strict: true,