Skip to content

Commit f8d5b5b

Browse files
committed
add test coverage for convertOrganizationToCollectiveMutation
1 parent 5aab075 commit f8d5b5b

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed

test/server/graphql/v2/mutation/OrganizationMutations.test.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import sinon from 'sinon';
44
import speakeasy from 'speakeasy';
55

66
import { CollectiveType } from '../../../../../server/constants/collectives';
7+
import MemberRoles from '../../../../../server/constants/roles';
78
import emailLib from '../../../../../server/lib/email';
89
import { crypto } from '../../../../../server/lib/encryption';
910
import { TwoFactorAuthenticationHeader } from '../../../../../server/lib/two-factor-authentication/lib';
@@ -403,4 +404,197 @@ describe('server/graphql/v2/mutation/OrganizationMutations', () => {
403404
expect(result.errors[0].message).to.match(/You can't deactivate hosting while still hosting/);
404405
});
405406
});
407+
408+
describe('convertOrganizationToCollective', () => {
409+
const convertOrganizationToCollectiveMutation = gql`
410+
mutation ConvertOrganizationToCollective($organization: AccountReferenceInput!) {
411+
convertOrganizationToCollective(organization: $organization) {
412+
id
413+
legacyId
414+
type
415+
slug
416+
name
417+
}
418+
}
419+
`;
420+
421+
it('requires authentication', async () => {
422+
const organization = await fakeCollective({ type: CollectiveType.ORGANIZATION });
423+
424+
const result = await utils.graphqlQueryV2(convertOrganizationToCollectiveMutation, {
425+
organization: { legacyId: organization.id },
426+
});
427+
428+
expect(result.errors).to.exist;
429+
expect(result.errors[0].message).to.match(/You need to be logged in/);
430+
});
431+
432+
it('requires admin privileges', async () => {
433+
const adminUser = await fakeUser();
434+
const randomUser = await fakeUser();
435+
const organization = await fakeCollective({ type: CollectiveType.ORGANIZATION, admin: adminUser });
436+
437+
const result = await utils.graphqlQueryV2(
438+
convertOrganizationToCollectiveMutation,
439+
{ organization: { legacyId: organization.id } },
440+
randomUser,
441+
);
442+
443+
expect(result.errors).to.exist;
444+
expect(result.errors[0].message).to.match(/forbidden/);
445+
});
446+
447+
it('successfully converts an organization to a collective', async () => {
448+
const user = await fakeUser();
449+
const organization = await fakeCollective({ type: CollectiveType.ORGANIZATION, admin: user });
450+
451+
expect(organization.type).to.equal(CollectiveType.ORGANIZATION);
452+
453+
const result = await utils.graphqlQueryV2(
454+
convertOrganizationToCollectiveMutation,
455+
{ organization: { legacyId: organization.id } },
456+
user,
457+
);
458+
459+
expect(result.errors).to.not.exist;
460+
expect(result.data.convertOrganizationToCollective.type).to.equal('COLLECTIVE');
461+
462+
// Verify in database
463+
await organization.reload();
464+
expect(organization.type).to.equal(CollectiveType.COLLECTIVE);
465+
466+
// Check activity
467+
const activity = await models.Activity.findOne({
468+
where: {
469+
UserId: user.id,
470+
type: 'organization.convertedToCollective',
471+
CollectiveId: organization.id,
472+
},
473+
});
474+
475+
expect(activity).to.exist;
476+
expect(activity.data.collective).to.exist;
477+
});
478+
479+
it('allows root users to convert any organization', async () => {
480+
const adminUser = await fakeUser();
481+
const organization = await fakeCollective({ type: CollectiveType.ORGANIZATION, admin: adminUser });
482+
const rootUser = await fakeUser({ data: { isRoot: true } });
483+
484+
const platform = await models.Collective.findByPk(1);
485+
await models.Member.create({
486+
MemberCollectiveId: rootUser.CollectiveId,
487+
CollectiveId: platform.id,
488+
role: MemberRoles.ADMIN,
489+
CreatedByUserId: rootUser.id,
490+
});
491+
492+
const result = await utils.graphqlQueryV2(
493+
convertOrganizationToCollectiveMutation,
494+
{ organization: { legacyId: organization.id } },
495+
rootUser,
496+
);
497+
498+
expect(result.errors).to.not.exist;
499+
expect(result.data.convertOrganizationToCollective.type).to.equal('COLLECTIVE');
500+
501+
await organization.reload();
502+
expect(organization.type).to.equal(CollectiveType.COLLECTIVE);
503+
});
504+
505+
it('rejects conversion if account is not an organization', async () => {
506+
const user = await fakeUser();
507+
const collective = await fakeCollective({ type: CollectiveType.COLLECTIVE, admin: user });
508+
509+
const result = await utils.graphqlQueryV2(
510+
convertOrganizationToCollectiveMutation,
511+
{ organization: { legacyId: collective.id } },
512+
user,
513+
);
514+
515+
expect(result.errors).to.exist;
516+
expect(result.errors[0].message).to.match(/Mutation only available to ORGANIZATION/);
517+
});
518+
519+
it('rejects conversion if organization has hosting activated', async () => {
520+
const user = await fakeUser();
521+
const organization = await fakeCollective({
522+
type: CollectiveType.ORGANIZATION,
523+
admin: user,
524+
HostCollectiveId: null
525+
});
526+
527+
// Activate money management and hosting
528+
await organization.activateMoneyManagement(user);
529+
await organization.activateHosting();
530+
531+
const result = await utils.graphqlQueryV2(
532+
convertOrganizationToCollectiveMutation,
533+
{ organization: { legacyId: organization.id } },
534+
user,
535+
);
536+
537+
expect(result.errors).to.exist;
538+
expect(result.errors[0].message).to.match(/Organization should not have Hosting activated/);
539+
});
540+
541+
it('rejects conversion if organization has money management activated', async () => {
542+
const user = await fakeUser();
543+
const organization = await fakeCollective({
544+
type: CollectiveType.ORGANIZATION,
545+
admin: user,
546+
HostCollectiveId: null
547+
});
548+
549+
// Activate money management only
550+
await organization.activateMoneyManagement(user);
551+
552+
const result = await utils.graphqlQueryV2(
553+
convertOrganizationToCollectiveMutation,
554+
{ organization: { legacyId: organization.id } },
555+
user,
556+
);
557+
558+
expect(result.errors).to.exist;
559+
expect(result.errors[0].message).to.match(/Organization should not have Money Management activated/);
560+
});
561+
562+
it('enforces 2FA when enabled on account', async () => {
563+
const secret = speakeasy.generateSecret({ length: 64 });
564+
const encryptedToken = crypto.encrypt(secret.base32).toString();
565+
const user = await fakeUser({ twoFactorAuthToken: encryptedToken });
566+
567+
const organization = await fakeCollective({ type: CollectiveType.ORGANIZATION, admin: user });
568+
569+
// Try without 2FA token
570+
const resultWithout2FA = await utils.graphqlQueryV2(
571+
convertOrganizationToCollectiveMutation,
572+
{ organization: { legacyId: organization.id } },
573+
user,
574+
);
575+
576+
expect(resultWithout2FA.errors).to.exist;
577+
expect(resultWithout2FA.errors[0].extensions.code).to.equal('2FA_REQUIRED');
578+
579+
// Try with valid 2FA token
580+
const twoFactorAuthenticatorCode = speakeasy.totp({
581+
algorithm: 'SHA1',
582+
encoding: 'base32',
583+
secret: secret.base32,
584+
});
585+
586+
const resultWith2FA = await utils.graphqlQueryV2(
587+
convertOrganizationToCollectiveMutation,
588+
{ organization: { legacyId: organization.id } },
589+
user,
590+
null,
591+
{
592+
[TwoFactorAuthenticationHeader]: `totp ${twoFactorAuthenticatorCode}`,
593+
},
594+
);
595+
596+
expect(resultWith2FA.errors).to.not.exist;
597+
expect(resultWith2FA.data.convertOrganizationToCollective.type).to.equal('COLLECTIVE');
598+
});
599+
});
406600
});

0 commit comments

Comments
 (0)