diff --git a/test/api/v3/integration/groups/GET-groups.test.js b/test/api/v3/integration/groups/GET-groups.test.js index 5359cc4c85..3414ec795a 100644 --- a/test/api/v3/integration/groups/GET-groups.test.js +++ b/test/api/v3/integration/groups/GET-groups.test.js @@ -11,7 +11,9 @@ import apiError from '../../../../../website/server/libs/apiError'; describe('GET /groups', () => { let user; + let userInGuild; const NUMBER_OF_PUBLIC_GUILDS = 3; // 2 + the tavern + const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2; const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1; const NUMBER_OF_USERS_PRIVATE_GUILDS = 1; const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5; @@ -33,14 +35,20 @@ describe('GET /groups', () => { name: 'public guild - is member', type: 'guild', privacy: 'public', + summary: 'ohayou kombonwa', + description: 'oyasumi', }); await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] }); await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`); + userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] }); + publicGuildNotMember = await generateGroup(leader, { name: 'public guild - is not member', type: 'guild', privacy: 'public', + summary: 'Natsume Soseki', + description: 'Kinnosuke no Hondana', categories, }); @@ -150,6 +158,35 @@ describe('GET /groups', () => { expect(guilds.length).to.equal(0); }); + + it('filters public guilds by leader role', async () => { + const guilds = await user.get('/groups?type=publicGuilds&leader=true'); + expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER); + }); + + it('filters public guilds by member role', async () => { + const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true'); + expect(guilds.length).to.equal(1); + expect(guilds[0].name).to.have.string('is member'); + }); + + it('filters public guilds by single-word search term', async () => { + const guilds = await user.get('/groups?type=publicGuilds&search=kom'); + expect(guilds.length).to.equal(1); + expect(guilds[0].summary).to.have.string('ohayou kombonwa'); + }); + + it('filters public guilds by single-word search term left and right-padded by spaces', async () => { + const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++'); + expect(guilds.length).to.equal(1); + expect(guilds[0].summary).to.have.string('ohayou kombonwa'); + }); + + it('filters public guilds by two-words search term separated by multiple spaces', async () => { + const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon'); + expect(guilds.length).to.equal(1); + expect(guilds[0].description).to.have.string('Kinnosuke'); + }); }); describe('public guilds pagination', () => { diff --git a/website/client/src/mixins/groupsUtilities.js b/website/client/src/mixins/groupsUtilities.js index b31d219e41..fbad44f8b4 100644 --- a/website/client/src/mixins/groupsUtilities.js +++ b/website/client/src/mixins/groupsUtilities.js @@ -1,4 +1,22 @@ import intersection from 'lodash/intersection'; +import _ from 'lodash'; + +const containsAnyCi = (target, patterns) => patterns.some(el => target.match(new RegExp(el, 'i'))); +const isPassedSearch = ({ name, summary, description }, search) => { + if (!search) return false; + + const searchWords = _.escapeRegExp(search.trim()).split(/\s+/); + + if (containsAnyCi(name, searchWords)) return true; + + if (!summary) return false; + if (containsAnyCi(summary, searchWords)) return true; + + if (!description) return false; + if (containsAnyCi(description, searchWords)) return true; + + return false; +}; export default { filters: { @@ -56,9 +74,7 @@ export default { if (group._id === this.$store.state.constants.TAVERN_ID || group._id === 'habitrpg') return false; - if (search) { - passedSearch = group.name.toLowerCase().indexOf(search.toLowerCase()) >= 0; - } + if (search) passedSearch = isPassedSearch(group, search); if (filters.categories && filters.categories.length > 0) { const intersectingCats = intersection(filters.categories, group.categorySlugs); @@ -75,11 +91,11 @@ export default { } if (filters.guildSize && filters.guildSize.indexOf('gold_tier') !== -1) { - correctSize = group.memberCount > 1000; + correctSize = group.memberCount >= 1000; } if (filters.guildSize && filters.guildSize.indexOf('silver_tier') !== -1) { - correctSize = group.memberCount > 100 && group.memberCount < 1000; + correctSize = group.memberCount >= 100 && group.memberCount < 1000; } if (filters.guildSize && filters.guildSize.indexOf('bronze_tier') !== -1) { diff --git a/website/client/tests/unit/components/groups/myGuilds.spec.js b/website/client/tests/unit/components/groups/myGuilds.spec.js new file mode 100644 index 0000000000..395fb5d0c7 --- /dev/null +++ b/website/client/tests/unit/components/groups/myGuilds.spec.js @@ -0,0 +1,107 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Store from '@/libs/store'; +import myGuilds from '@/components/groups/myGuilds'; +import PublicGuildItem from '@/components/groups/publicGuildItem'; + +const localVue = createLocalVue(); +localVue.use(Store); + +describe('myGuilds component', () => { + let computed; + const guilds = [{ + _id: '1', + type: 'guild', + name: 'Crimson Vow', + summary: 'testing', + description: 'testing', + }, { + _id: '2', + type: 'guild', + name: 'Log Horizon', + summary: 'testing', + description: 'testing', + }, { + _id: '3', + type: 'guild', + name: 'CAD Cads', + summary: '3D', + description: '3D', + }, { + _id: '4', + type: 'guild', + name: 'Santa Claus', + summary: '3d', + description: 'hohoho', + }]; + const store = new Store({ + state: { + user: { + data: { + _id: '999', + guilds: ['1', '2', '3', '4'], + }, + }, + editingGroup: {}, + constants: { + TAVERN_ID: '9999', + }, + }, + getters: {}, + actions: { + 'guilds:getMyGuilds': () => guilds, + }, + }); + + function makeWrapper (opts = {}) { + return shallowMount(myGuilds, { + data () { + return { + filter: {}, + search: '', + }; + }, + store, + localVue, + ...opts, + }); + } + + before(() => { + computed = { + guilds: () => guilds, + }; + }); + + it('renders all guilds with no filter and no search', () => { + const wrapper = makeWrapper({ computed }); + expect(wrapper.findAll(PublicGuildItem).length).to.equal(4); + }); + + it('renders guilds with name matching against a single-word search term', () => { + const search = 'vow'; + const wrapper = makeWrapper({ computed }); + wrapper.setData({ search }); + expect(wrapper.findAll(PublicGuildItem).length).to.equal(1); + }); + + it('renders guilds with summary matching against a single-word search term', () => { + const search = '3d'; + const wrapper = makeWrapper({ computed }); + wrapper.setData({ search }); + expect(wrapper.findAll(PublicGuildItem).length).to.equal(2); + }); + + it('renders guilds with description matching against a single-word search term', () => { + const search = 'hoho'; + const wrapper = makeWrapper({ computed }); + wrapper.setData({ search }); + expect(wrapper.findAll(PublicGuildItem).length).to.equal(1); + }); + + it('renders guilds with summary matching against two search terms with space in between', () => { + const search = '3d ohayou'; + const wrapper = makeWrapper({ computed }); + wrapper.setData({ search }); + expect(wrapper.findAll(PublicGuildItem).length).to.equal(2); + }); +}); diff --git a/website/client/tests/unit/mixins/groupsUtilities.spec.js b/website/client/tests/unit/mixins/groupsUtilities.spec.js index bf69718d52..0a7ae6ecbb 100644 --- a/website/client/tests/unit/mixins/groupsUtilities.spec.js +++ b/website/client/tests/unit/mixins/groupsUtilities.spec.js @@ -61,4 +61,172 @@ describe('Groups Utilities Mixin', () => { })).to.equal(false); }); }); + + describe('filterGuild', () => { + let testGroup; + let testGroup2; + + before(() => { + testGroup = { + type: 'guild', + _id: user.guilds[0], + name: 'Crimson Vow', + summary: 'testing', + description: 'dummy 1', + leader: user.guilds[0], // test user is not guild leader + categories: [{ + _id: '123', + slug: 'hobbies_occupations', + name: 'hobbies_occupations', + }], + categorySlugs: ['hobbies_occupations'], + memberCount: 1000, + }; + testGroup2 = { + type: 'guild', + _id: '790', + name: 'CAD Cads', + summary: '3D', + description: 'My dummy', + leader: user._id, // test user is guild leader + categories: [{ + _id: '123', + slug: 'hobbies_occupations', + name: 'hobbies_occupations', + }], + categorySlugs: ['hobbies_occupations'], + memberCount: 100, + }; + }); + + it('returns true with no filter and no search', () => { + const filter = {}; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns false with no filter and one search word not matching against any of the guild name, summary, and description', () => { + const filter = {}; + const search = '3d'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false); + }); + + it('returns true with no filter and one search word matched successfully against guild name', () => { + const filter = {}; + const search = 'vow'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns true with no filter and one search word matched successfully against guild summary', () => { + const filter = {}; + const search = 'test'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns true with no filter and one search word matched successfully against guild description', () => { + const filter = {}; + const search = 'dum'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns true with no filter and two search words with two spaces in between matched successfully against guild name', () => { + const filter = {}; + const search = 'cad test'; + expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true); + }); + + it('returns true with no filter and two search words with two spaces in between matched successfully against guild summary', () => { + const filter = {}; + const search = 'cad 3d'; + expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true); + }); + + it('returns true with no filter and two search words with two spaces in between matched successfully against guild description', () => { + const filter = {}; + const search = 'my dummy'; + expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true); + }); + + it('returns false with no search word and one filter category that does not match against any guild categories', () => { + const filter = { + categories: ['academics'], + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false); + }); + + it('returns true with no search word and one filter category that matches successfully against any guild categories', () => { + const filter = { + categories: ['hobbies_occupations'], + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns false with no search word and one filter role that does not match against guild role', () => { + const filter = { + roles: ['guild_leader'], + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false); + }); + + it('returns true with no search word and one filter role that matches successfully against guild role', () => { + const filter = { + roles: ['member'], + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns true with no search word and filter size silver tier that matches against a guild size of 1000, the max guild size belonging to silver tier', () => { + const filter = { + guildSize: 'gold_tier', + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + + it('returns true with no search word and filter size bronze tier that matches against a guild size of 100, the max guild size belonging to bronze tier', () => { + const filter = { + guildSize: 'silver_tier', + }; + const search = ''; + expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true); + }); + + it('returns false with no search word and filter category that matches successfully against one guild category and filter role that does not match against guild role', () => { + const filter = { + categories: ['hobbies_occupations'], + roles: ['guild_leader'], + }; + const search = ''; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false); + }); + + it('returns true with no search word and filter category that matches successfully against one guild category and filter role that matches successfully against guild role', () => { + const filter = { + categories: ['hobbies_occupations'], + roles: ['guild_leader'], + }; + const search = ''; + expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true); + }); + + it('returns false with one search word that does not match against guild name and one filter category that matches successfully against guild categories', () => { + const filter = { + categories: ['hobbies_occupations'], + }; + const search = 'konnichiwa'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false); + }); + + it('returns true with one search word that matches against guild name and one filter role that matches successfully against guild role', () => { + const filter = { + categories: ['hobbies_occupations'], + }; + const search = 'vow'; + expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true); + }); + }); }); diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 1fb322e0f3..e71e2b63f7 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -349,9 +349,10 @@ api.getGroups = { if (req.query.search) { filters.$or = []; - const searchWords = _.escapeRegExp(req.query.search).split(' ').join('|'); + const searchWords = _.escapeRegExp(req.query.search.trim()).split(/\s+/).join('|'); const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') }; filters.$or.push({ name: searchQuery }); + filters.$or.push({ summary: searchQuery }); filters.$or.push({ description: searchQuery }); }