@@ -4,6 +4,7 @@ import sinon from 'sinon';
44import speakeasy from 'speakeasy' ;
55
66import { CollectiveType } from '../../../../../server/constants/collectives' ;
7+ import MemberRoles from '../../../../../server/constants/roles' ;
78import emailLib from '../../../../../server/lib/email' ;
89import { crypto } from '../../../../../server/lib/encryption' ;
910import { 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 ( / Y o u c a n ' t d e a c t i v a t e h o s t i n g w h i l e s t i l l h o s t i n g / ) ;
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 ( / Y o u n e e d t o b e l o g g e d i n / ) ;
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 ( / f o r b i d d e n / ) ;
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 ( / M u t a t i o n o n l y a v a i l a b l e t o O R G A N I Z A T I O N / ) ;
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 ( / O r g a n i z a t i o n s h o u l d n o t h a v e H o s t i n g a c t i v a t e d / ) ;
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 ( / O r g a n i z a t i o n s h o u l d n o t h a v e M o n e y M a n a g e m e n t a c t i v a t e d / ) ;
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