diff --git a/test/api/unit/libs/bug-report.test.js b/test/api/unit/libs/bug-report.test.js new file mode 100644 index 0000000000..feac88ef32 --- /dev/null +++ b/test/api/unit/libs/bug-report.test.js @@ -0,0 +1,57 @@ +/* eslint-disable global-require */ +import nconf from 'nconf'; +import { generateUser } from '../../../helpers/api-unit.helper'; +import * as emailLib from '../../../../website/server/libs/email'; +import { bugReportLogic } from '../../../../website/server/libs/bug-report'; + +describe('bug-report', () => { + beforeEach(() => { + sandbox.stub(emailLib, 'sendTxn').returns(Promise.resolve()); + + const nconfGetStub = sandbox.stub(nconf, 'get'); + nconfGetStub.withArgs('ADMIN_EMAIL').returns('true'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('sends a mail using sendTxn', async () => { + const userId = '2b58daeb-bc50-4a83-b5d3-4ac52c7c0608'; + const userMail = 'me@me.com'; + const userMessage = 'The power is over 9000, please fix it'; + const userAgent = 'The UserAgent with a bunch of weird browser engine levels'; + + const user = generateUser({ + _id: userId, + }); + + const result = await bugReportLogic( + user, userMail, userMessage, userAgent, + ); + + expect(emailLib.sendTxn).to.be.called; + expect(result).to.deep.equal({ + sendMailResult: undefined, + emailData: { + BROWSER_UA: userAgent, + REPORT_MSG: userMessage, + USER_CLASS: 'warrior', + USER_CONSECUTIVE_MONTHS: 0, + USER_COSTUME: 'false', + USER_CUSTOMER_ID: undefined, + USER_CUSTOM_DAY: 0, + USER_DAILIES_PAUSED: 'false', + USER_EMAIL: userMail, + USER_HOURGLASSES: 0, + USER_ID: userId, + USER_LEVEL: 1, + USER_OFFSET_MONTHS: 0, + USER_PAYMENT_PLATFORM: undefined, + USER_SUBSCRIPTION: undefined, + USER_TIMEZONE_OFFSET: 0, + USER_USERNAME: undefined, + }, + }); + }); +}); diff --git a/test/api/unit/libs/email.test.js b/test/api/unit/libs/email.test.js index 7d0df88350..b3fe180086 100644 --- a/test/api/unit/libs/email.test.js +++ b/test/api/unit/libs/email.test.js @@ -148,9 +148,18 @@ describe('emails', () => { }); }); - describe('sendTxnEmail', () => { + describe('sendTxn', () => { + let sendTxn = null; + beforeEach(() => { sandbox.stub(got, 'post').returns(defer().promise); + + const nconfGetStub = sandbox.stub(nconf, 'get'); + nconfGetStub.withArgs('IS_PROD').returns(true); + nconfGetStub.withArgs('BASE_URL').returns('BASE_URL'); + + const attachEmail = requireAgain(pathToEmailLib); + sendTxn = attachEmail.sendTxn; }); afterEach(() => { @@ -158,16 +167,14 @@ describe('emails', () => { }); it('can send a txn email to one recipient', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); - const attachEmail = requireAgain(pathToEmailLib); - const sendTxnEmail = attachEmail.sendTxn; const emailType = 'an email type'; const mailingInfo = { name: 'my name', email: 'my@email', }; - sendTxnEmail(mailingInfo, emailType); + sendTxn(mailingInfo, emailType); + expect(got.post).to.be.called; expect(got.post).to.be.calledWith('undefined/job', sinon.match({ json: { data: { @@ -179,27 +186,77 @@ describe('emails', () => { }); it('does not send email if address is missing', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); - const attachEmail = requireAgain(pathToEmailLib); - const sendTxnEmail = attachEmail.sendTxn; const emailType = 'an email type'; const mailingInfo = { name: 'my name', // email: 'my@email', }; - sendTxnEmail(mailingInfo, emailType); + sendTxn(mailingInfo, emailType); expect(got.post).not.to.be.called; }); + it('throws error when mail target is only a string', () => { + const emailType = 'an email type'; + const mailingInfo = 'my email'; + + expect(sendTxn(mailingInfo, emailType)).to.throw; + }); + + it('throws error when mail target has no _id or email', () => { + const emailType = 'an email type'; + const mailingInfo = { + + }; + + expect(sendTxn(mailingInfo, emailType)).to.throw; + }); + + it('throws error when variables not an array', () => { + const emailType = 'an email type'; + const mailingInfo = { + name: 'my name', + email: 'my@email', + }; + const variables = {}; + + expect(sendTxn(mailingInfo, emailType, variables)).to.throw; + }); + it('throws error when variables array not contain name/content', () => { + const emailType = 'an email type'; + const mailingInfo = { + name: 'my name', + email: 'my@email', + }; + const variables = [ + { + + }, + ]; + + expect(sendTxn(mailingInfo, emailType, variables)).to.throw; + }); + it('throws no error when variables array contain name but no content', () => { + const emailType = 'an email type'; + const mailingInfo = { + name: 'my name', + email: 'my@email', + }; + const variables = [ + { + name: 'MY_VAR', + }, + ]; + + expect(sendTxn(mailingInfo, emailType, variables)).to.not.throw; + }); + it('uses getUserInfo in case of user data', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); - const attachEmail = requireAgain(pathToEmailLib); - const sendTxnEmail = attachEmail.sendTxn; const emailType = 'an email type'; const mailingInfo = getUser(); - sendTxnEmail(mailingInfo, emailType); + sendTxn(mailingInfo, emailType); + expect(got.post).to.be.called; expect(got.post).to.be.calledWith('undefined/job', sinon.match({ json: { data: { @@ -211,17 +268,15 @@ describe('emails', () => { }); it('sends email with some default variables', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); - const attachEmail = requireAgain(pathToEmailLib); - const sendTxnEmail = attachEmail.sendTxn; const emailType = 'an email type'; const mailingInfo = { name: 'my name', email: 'my@email', }; - const variables = [1, 2, 3]; + const variables = []; - sendTxnEmail(mailingInfo, emailType, variables); + sendTxn(mailingInfo, emailType, variables); + expect(got.post).to.be.called; expect(got.post).to.be.calledWith('undefined/job', sinon.match({ json: { data: { diff --git a/test/api/v4/POST-bug-report.test.js b/test/api/v4/POST-bug-report.test.js new file mode 100644 index 0000000000..a963519415 --- /dev/null +++ b/test/api/v4/POST-bug-report.test.js @@ -0,0 +1,49 @@ +import { + generateUser, +} from '../../helpers/api-integration/v4'; + +describe('POST /bug-report', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('returns an error when message is not added', async () => { + await expect(user.post('/bug-report', { + message: '', + })) + .to.eventually.be.rejected.and.to.eql({ + code: 400, + error: 'BadRequest', + // seems it is not possible to get the real error message + message: 'Invalid request parameters.', + }); + }); + + it('returns an error when email is not added', async () => { + await expect(user.post('/bug-report', { + message: 'message', + email: '', + })) + .to.eventually.be.rejected.and.to.eql({ + code: 400, + error: 'BadRequest', + // seems it is not possible to get the real error message + message: 'Invalid request parameters.', + }); + }); + + it('returns an error when email is not valid', async () => { + await expect(user.post('/bug-report', { + message: 'message', + email: 'notamail', + })) + .to.eventually.be.rejected.and.to.eql({ + code: 400, + error: 'BadRequest', + // seems it is not possible to get the real error message + message: 'Invalid request parameters.', + }); + }); +}); diff --git a/website/client/config/storybook/config.js b/website/client/config/storybook/config.js index f1bc33bc7a..bc282bd5d7 100644 --- a/website/client/config/storybook/config.js +++ b/website/client/config/storybook/config.js @@ -40,6 +40,18 @@ store.state.user.data = { preferences: { }, + auth: { + local: { + // email: 'example@example.com', + }, + facebook: { + emails: [ + { + value: 'test@test.de', + }, + ], + }, + }, }; Vue.prototype.$store = store; diff --git a/website/client/src/app.vue b/website/client/src/app.vue index d6174f9ac2..4eb742c008 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -33,6 +33,8 @@ + + @@ -177,6 +179,10 @@ import { removeLocalSetting, } from '@/libs/userlocalManager'; +const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal'); +const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal'); + + const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line export default { @@ -196,6 +202,8 @@ export default { paymentsSuccessModal, subCancelModalConfirm, subCanceledModal, + bugReportModal, + bugReportSuccessModal, }, mixins: [notifications, spellsMixin], data () { diff --git a/website/client/src/assets/svg/check_circle.svg b/website/client/src/assets/svg/check_circle.svg new file mode 100644 index 0000000000..4387f48940 --- /dev/null +++ b/website/client/src/assets/svg/check_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index 9c0f9ca107..ad76bb6d47 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -82,9 +82,17 @@ {{ $t('hall') }} - + + {{ $t('reportBug') }} + + + + {{ $t('reportBug') }} diff --git a/website/client/src/components/bug-report-modal.stories.js b/website/client/src/components/bug-report-modal.stories.js new file mode 100644 index 0000000000..13ff90ce86 --- /dev/null +++ b/website/client/src/components/bug-report-modal.stories.js @@ -0,0 +1,43 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { storiesOf } from '@storybook/vue'; +import { withKnobs } from '@storybook/addon-knobs'; + + +import bugReportModal from '@/components/bugReportModal'; +import bugReportSuccessModal from '@/components/bugReportSuccessModal'; + +const stories = storiesOf('Bug Report Modal', module); + +stories.addDecorator(withKnobs); + +stories + .add('bugReportModal', () => ({ + components: { bugReportModal }, + data () { + return { + }; + }, + template: ` + + + + `, + mounted () { + this.$root.$emit('bv::show::modal', 'bug-report-modal'); + }, + })) + .add('bugReportSuccessModal', () => ({ + components: { bugReportSuccessModal }, + data () { + return { + }; + }, + template: ` + + + + `, + mounted () { + this.$root.$emit('bv::show::modal', 'bug-report-success-modal'); + }, + })); diff --git a/website/client/src/components/bugReportModal.vue b/website/client/src/components/bugReportModal.vue new file mode 100644 index 0000000000..91fda2a859 --- /dev/null +++ b/website/client/src/components/bugReportModal.vue @@ -0,0 +1,243 @@ + + + + + {{ $t('reportBug') }} + + + + {{ $t('reportBugHeaderDescribe') }} + + + + + + + + + + + {{ $t('email') }} + + + {{ $t('reportEmailText') }} + + + + + {{ $t('reportEmailError') }} + + + + + {{ $t('reportDescription') }} + + + {{ $t('reportDescriptionText') }} + + + + + + + {{ $t('submitBugReport') }} + + + + + + + + + + + + diff --git a/website/client/src/components/bugReportSuccessModal.vue b/website/client/src/components/bugReportSuccessModal.vue new file mode 100644 index 0000000000..9a25decb2c --- /dev/null +++ b/website/client/src/components/bugReportSuccessModal.vue @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + {{ $t('reportSent') }} + + + + {{ $t('reportSentDescription') }} + + + + + + + + + + diff --git a/website/client/src/components/groups/group.vue b/website/client/src/components/groups/group.vue index fe7e1457f8..0047527085 100644 --- a/website/client/src/components/groups/group.vue +++ b/website/client/src/components/groups/group.vue @@ -311,6 +311,7 @@ import bronzeGuildBadgeIcon from '@/assets/svg/bronze-guild-badge-small.svg'; import QuestDetailModal from './questDetailModal'; import RightSidebar from '@/components/groups/rightSidebar'; import InvitationListModal from './invitationListModal'; +import { PAGES } from '@/libs/consts'; export default { components: { @@ -554,7 +555,7 @@ export default { this.$root.$emit('bv::show::modal', 'group-gems-modal'); }, messageLeader () { - window.open(`/private-messages?uuid=${this.group.leader.id}`); + window.open(`${PAGES.PRIVATE_MESSAGES}?uuid=${this.group.leader.id}`); }, }, }; diff --git a/website/client/src/components/groups/membersModal.vue b/website/client/src/components/groups/membersModal.vue index c81ffdf0e2..512d00a59b 100644 --- a/website/client/src/components/groups/membersModal.vue +++ b/website/client/src/components/groups/membersModal.vue @@ -389,6 +389,7 @@ import messageIcon from '@/assets/members/message.svg'; import starIcon from '@/assets/members/star.svg'; import dots from '@/assets/svg/dots.svg'; import SelectList from '@/components/ui/selectList'; +import { PAGES } from '@/libs/consts'; export default { components: { @@ -558,7 +559,7 @@ export default { }); this.$root.$emit('bv::hide::modal', 'members-modal'); - this.$router.push('/private-messages'); + this.$router.push(PAGES.PRIVATE_MESSAGES); }, async searchMembers (searchTerm = '') { this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({ diff --git a/website/client/src/components/groups/tavern.vue b/website/client/src/components/groups/tavern.vue index 4dc4dca457..8cde0b0c0c 100644 --- a/website/client/src/components/groups/tavern.vue +++ b/website/client/src/components/groups/tavern.vue @@ -361,7 +361,7 @@ {{ $t('reportBug') }} diff --git a/website/client/src/components/header/menu.vue b/website/client/src/components/header/menu.vue index 948be74122..67f40e1c5e 100644 --- a/website/client/src/components/header/menu.vue +++ b/website/client/src/components/header/menu.vue @@ -311,7 +311,7 @@ {{ $t('reportBug') }} diff --git a/website/client/src/components/header/notifications/newPrivateMessage.vue b/website/client/src/components/header/notifications/newPrivateMessage.vue index 79931cc9f8..608ff42d85 100644 --- a/website/client/src/components/header/notifications/newPrivateMessage.vue +++ b/website/client/src/components/header/notifications/newPrivateMessage.vue @@ -17,6 +17,7 @@