Files
habitica/test/api/v3/integration/user/DELETE-user.test.js
Phillip Thelen 9d16ab7dba Sign in with Apple (#11793)
* add date check

* achievements modal polishing

* refresh private-messages page when you are already on it

* add countbadge knob to change the example

* fix lint

* typos

* typos

* typos

* add toggle for achievements categories

* typo

* fix test

* fix edit avatar modal cannot be closed

* WIP(settings): subscriber page improvements

* WIP(subscriptions): more design build-out

* fix(css): disabled button styles

* fix(css): better Amazon targeting

* fix hide tooltip + align header correctly

* disable perfect scroll

* load messages on refresh event

* fix header label + conversation actions not breaking layout on hover

* WIP(g1g1): notif

* WIP(g1g1): notif cont'd

* fix(test): snowball change

* fix(event): feature NYE card

* chore(sprites): compile

* fix(bgs): include TT required field

* add gifting banner to the max height calculation

* chore(event): enable winter customizations

* WIP(gifting): partial modal implementation

* feat(gifting): select giftee modal

* fix(gifting): notification order, modal dismiss

* Begin implementing sign in with apple

# Conflicts:
#	package-lock.json
#	website/common/script/constants.js
#	website/server/libs/auth/social.js
#	website/server/models/user/schema.js

* Add apple sign in button to website

* fix lint errors

* fix config json

* fix(modals): correct some repops

* fix(gifting): style updates

* fix(buy): modal style changes

* fix(modals): also clean out "prev"

* Attempt workaround for sign in with apple on android

* temporarily log everything as error

* refactor(modals): hide in dismiss event

* fix temporary test failure

* changes to sign in with apple

* fix: first batch of layout issues for private messages + auto sizing textarea

* fix(modals): new dismiss logic

* fix(modals): new dismiss no go??

* Only use email scope

* print debugging

* .

* ..

* ...

* username second line - open profile on face-avatar/conversation name - fix textarea height

* temporarily disable apple auth and just return data for debugging

* Hopefully this works

* .....

* WIP(subscription): unsubscribed state

* .

* ..

* MAYBE THIS ACTUALLY WORKS???

* Implement apple sign in

* fix some urls

* fix urls

* fix redirect and auth

* attempt to also request name

* fix lint error

* WIP(subscription): partial subscribed

* chore(sprites): compile

* Change approach so that it actually works

* fix config error

* fix lint errors

* Fix

* fix lint error

* lint error

* WIP(subscription): finish subscribed

* refresh on sync

* new "you dont have any messages" style + changed min textarea height

* new conversationItem style / layout

* reset message unread on reload

* chore(npm): update package-locks

* fix styles / textarea height

* feat(subscription): revised sub page RC

* list optOut / chatRevoked informations for each conversation + show why its disabled

* Improve apple redirect view

* Fix apple icon on group task registration page

* WIP(adventure): prereqs

* Block / Unblock - correct disabled states - $gray-200 instead of 300/400

* canReceive not checking chatRevoked

* fix: faceAvatar / userLink open the selected conversation user

* check if the target user is blocking the logged-in user

* fix(subs): style tweaks

* fix(profiles): short circuit contributor
Attempted fix for #11830

* chore(sprites): compile

* fix(content): missing potion data

* fix(content): missing string

* WIP(drops): new modal

* fix(subs): moar style tweaks

* check if blocks is undefined

* max-height instead of height

* fix "no messages" state + canReceive on a new conversation

* WIP(adventure): analytics fixes etc

* Improve apple signin handling

* fixed conversations width (280px on max 768 width page)

* feat(adventure): random egg+potion on 2nd task

* fix(lint): noworkies

* fix(modal): correctly construct classes

* fix(tests): expectations and escape

* Fix typo

* use base url from env variables

* fix lint

* call autosize after message is sent

* fix urls

* always verify token

* throw error when social auth could not retrieve id

* Store emails correctly for apple auth

* Retrieve name when authenticating through apple

* Fix lint errors

* fix all lint errors

* fix(content): missing strings

* Revert "always verify token"

This reverts commit 8ac40c76bf.

# Conflicts:
#	website/server/libs/auth/social.js

* Correctly load name

* remove extra changes

* remove extra logger call

* reset package and package-lock

* add back missing packages

* use name from apple

* add support for multiple apple public keys

* add some unit and integration tests

* add apple auth integration test

* tweak social signup buttons

* pixel pushing

Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: negue <eugen.bolz@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2020-04-08 18:44:30 +02:00

369 lines
10 KiB
JavaScript

import {
find,
each,
map,
} from 'lodash';
import {
checkExistence,
createAndPopulateGroup,
generateGroup,
generateUser,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
const DELETE_CONFIRMATION = 'DELETE';
describe('DELETE /user', () => {
let user;
const password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
context('user with local auth', async () => {
beforeEach(async () => {
user = await generateUser({ balance: 10 });
});
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('returns an error if password is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('deletes the user', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('returns an error if excessive feedback is supplied', async () => {
const feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback += feedbackText;
}
await expect(user.del('/user', {
password,
feedback,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
});
});
it('returns an error if user has active subscription', async () => {
const userWithSubscription = await generateUser({ 'purchased.plan.customerId': 'fake-customer-id' });
await expect(userWithSubscription.del('/user', {
password,
})).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotDeleteActiveAccount'),
});
});
it('deletes the user\'s tasks', async () => {
await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.sync();
// gets the user's tasks ids
const ids = [];
each(user.tasksOrder, idsForOrder => {
ids.push(...idsForOrder);
});
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
await user.del('/user', {
password,
});
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
});
it('reduces memberCount in challenges user is linked to', async () => {
const populatedGroup = await createAndPopulateGroup({
members: 2,
});
const { group } = populatedGroup;
const authorizedUser = populatedGroup.members[1];
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
await challenge.sync();
expect(challenge.memberCount).to.eql(1);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
const feedback = 'Reasons for Deletion';
await user.del('/user', {
password,
feedback,
});
expect(email.sendTxn).to.be.calledOnce;
sandbox.restore();
});
it('does not send email if no feedback is supplied', async () => {
sandbox.spy(email, 'sendTxn');
await user.del('/user', {
password,
});
expect(email.sendTxn).to.not.be.called;
sandbox.restore();
});
it('deletes the user with a legacy sha1 password', async () => {
const textPassword = 'mySecretPassword';
const salt = sha1MakeSalt();
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// delete the user
await user.del('/user', {
password: textPassword,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
context('last member of a party', () => {
let party;
beforeEach(async () => {
party = await generateGroup(user, {
type: 'party',
privacy: 'private',
});
});
it('deletes party when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
});
});
context('last member of a private guild', () => {
let privateGuild;
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
});
});
it('deletes guild when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
});
});
context('groups user is leader of', () => {
let guild; let oldLeader; let
newLeader;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
guild = group;
newLeader = members[0]; // eslint-disable-line prefer-destructuring
oldLeader = groupLeader;
});
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
password,
});
const updatedGuild = await newLeader.get(`/groups/${guild._id}`);
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1; let group2; let userToDelete; let
otherUser;
beforeEach(async () => {
userToDelete = await generateUser({ balance: 10 });
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
const { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0]; // eslint-disable-line prefer-destructuring
await userToDelete.post(`/groups/${group2._id}/join`);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
password,
});
const updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
const updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
const userInGroup = find(updatedGroup2Members, member => member._id === userToDelete._id);
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
});
});
});
context('user with Facebook auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
facebook: {
id: 'facebook-id',
},
},
});
});
it('returns an error if confirmation phrase is wrong', async () => {
await expect(user.del('/user', {
password: 'just-do-it',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase', { magicWord: 'DELETE' }),
});
});
it('returns an error if confirmation phrase is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('deletes a Facebook user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Google auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
google: {
id: 'google-id',
},
},
});
});
it('deletes a Google user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Apple auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
apple: {
id: 'apple-id',
},
},
});
});
it('deletes a Apple user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
});