diff --git a/api-js/src/locale/en/messages.js b/api-js/src/locale/en/messages.js index 818053c3de..f112f7fd0b 100644 --- a/api-js/src/locale/en/messages.js +++ b/api-js/src/locale/en/messages.js @@ -221,6 +221,8 @@ 'Successfully removed organization: {0}.': function (a) { return ['Successfully removed organization: ', a('0'), '.'] }, + 'Successfully removed user from organization.': + 'Successfully removed user from organization.', 'Successfully sent invitation to service, and organization email.': 'Successfully sent invitation to service, and organization email.', 'Successfully two factor authenticated.': @@ -323,14 +325,12 @@ 'Unable to query domains. Please try again.', 'Unable to query organizations. Please try again.': 'Unable to query organizations. Please try again.', - 'Unable to query user without a username, please try again.': - 'Unable to query user without a username, please try again.', - 'Unable to query user, please try again.': - 'Unable to query user, please try again.', 'Unable to remove domain. Please try again.': 'Unable to remove domain. Please try again.', 'Unable to remove organization. Please try again.': 'Unable to remove organization. Please try again.', + 'Unable to remove user from organization. Please try again.': + 'Unable to remove user from organization. Please try again.', 'Unable to reset password. Please try again.': 'Unable to reset password. Please try again.', 'Unable to retrieve {0} for domain: {domain}.': function (a) { diff --git a/api-js/src/locale/en/messages.po b/api-js/src/locale/en/messages.po index 8b5ffe3138..9bedba71ef 100644 --- a/api-js/src/locale/en/messages.po +++ b/api-js/src/locale/en/messages.po @@ -30,7 +30,7 @@ msgstr "Authentication error. Please sign in again." msgid "Authentication error. Please sign in." msgstr "Authentication error. Please sign in." -#: src/types/base/index.js:870 +#: src/types/base/index.js:866 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Cannot query affiliations on organization without admin permission or higher." @@ -80,7 +80,7 @@ msgstr "No verified domain with the provided domain could be found." msgid "Organization has already been verified." msgstr "Organization has already been verified." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:43 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:42 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:42 msgid "Passing both `first` and `last` to paginate the `affiliation` is not supported." msgstr "Passing both `first` and `last` to paginate the `affiliation` is not supported." @@ -158,10 +158,10 @@ msgstr "Password was successfully updated." msgid "Passwords do not match." msgstr "Passwords do not match." -#: src/auth/check-user-is-admin-for-user.js:17 -#: src/auth/check-user-is-admin-for-user.js:27 -#: src/auth/check-user-is-admin-for-user.js:59 -#: src/auth/check-user-is-admin-for-user.js:69 +#: src/auth/check-user-is-admin-for-user.js:19 +#: src/auth/check-user-is-admin-for-user.js:29 +#: src/auth/check-user-is-admin-for-user.js:60 +#: src/auth/check-user-is-admin-for-user.js:70 msgid "Permission error, not an admin for this user." msgstr "Permission error, not an admin for this user." @@ -169,7 +169,7 @@ msgstr "Permission error, not an admin for this user." msgid "Profile successfully updated." msgstr "Profile successfully updated." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:66 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:65 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:65 msgid "Requesting `{amount}` records on the `affiliations` exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `affiliations` exceeds the `{argSet}` limit of 100 records." @@ -238,6 +238,10 @@ msgstr "Successfully removed domain: {0} from {1}." msgid "Successfully removed organization: {0}." msgstr "Successfully removed organization: {0}." +#: src/mutations/user-affiliations/remove-user-from-org.js:118 +msgid "Successfully removed user from organization." +msgstr "Successfully removed user from organization." + #: src/mutations/user-affiliations/invite-user-to-org.js:127 msgid "Successfully sent invitation to service, and organization email." msgstr "Successfully sent invitation to service, and organization email." @@ -403,7 +407,7 @@ msgstr "Unable to invite user. Please try again." msgid "Unable to invite yourself to an org. Please try again." msgstr "Unable to invite yourself to an org. Please try again." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:147 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:146 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:146 msgid "Unable to load affiliations. Please try again." msgstr "Unable to load affiliations. Please try again." @@ -499,7 +503,7 @@ msgstr "Unable to load verified organizations. Please try again." msgid "Unable to load web summary. Please try again." msgstr "Unable to load web summary. Please try again." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:137 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:136 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:136 msgid "Unable to query affiliations. Please try again." msgstr "Unable to query affiliations. Please try again." @@ -512,14 +516,6 @@ msgstr "Unable to query domains. Please try again." msgid "Unable to query organizations. Please try again." msgstr "Unable to query organizations. Please try again." -#: src/queries/user/find-user-by-username.js:34 -msgid "Unable to query user without a username, please try again." -msgstr "Unable to query user without a username, please try again." - -#: src/queries/user/find-user-by-username.js:50 -msgid "Unable to query user, please try again." -msgstr "Unable to query user, please try again." - #: src/mutations/domain/remove-domain.js:58 #: src/mutations/domain/remove-domain.js:69 #: src/mutations/domain/remove-domain.js:80 @@ -541,6 +537,14 @@ msgstr "Unable to remove domain. Please try again." msgid "Unable to remove organization. Please try again." msgstr "Unable to remove organization. Please try again." +#: src/mutations/user-affiliations/remove-user-from-org.js:52 +#: src/mutations/user-affiliations/remove-user-from-org.js:63 +#: src/mutations/user-affiliations/remove-user-from-org.js:74 +#: src/mutations/user-affiliations/remove-user-from-org.js:102 +#: src/mutations/user-affiliations/remove-user-from-org.js:113 +msgid "Unable to remove user from organization. Please try again." +msgstr "Unable to remove user from organization. Please try again." + #: src/mutations/user/reset-password.js:60 #: src/mutations/user/reset-password.js:70 #: src/mutations/user/reset-password.js:78 @@ -677,7 +681,7 @@ msgstr "Unable to verify if user is an admin, please try again." msgid "Unable to verify organization. Please try again." msgstr "Unable to verify organization. Please try again." -#: src/queries/user/find-user-by-username.js:60 +#: src/queries/user/find-user-by-username.js:40 msgid "User could not be queried." msgstr "User could not be queried." @@ -697,7 +701,7 @@ msgstr "We've sent you a text message with an authentication code to sign into P msgid "We've sent you an email with an authentication code to sign into Pulse." msgstr "We've sent you an email with an authentication code to sign into Pulse." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:34 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:33 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:33 msgid "You must provide a `first` or `last` value to properly paginate the `affiliation`." msgstr "You must provide a `first` or `last` value to properly paginate the `affiliation`." @@ -767,7 +771,7 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `ver #: src/loaders/guidance-tags/load-ssl-guidance-tags-connections.js:77 #: src/loaders/organizations/load-organization-connections-by-domain-id.js:81 #: src/loaders/organizations/load-organization-connections-by-user-id.js:81 -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:81 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:80 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:80 #: src/loaders/verified-domains/load-verified-domain-connections-by-organization-id.js:79 #: src/loaders/verified-domains/load-verified-domain-connections.js:80 @@ -778,7 +782,7 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `ver msgid "`{argSet}` must be of type `number` not `{typeSet}`." msgstr "`{argSet}` must be of type `number` not `{typeSet}`." -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:55 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:54 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:54 msgid "`{argSet}` on the `affiliations` cannot be less than zero." msgstr "`{argSet}` on the `affiliations` cannot be less than zero." diff --git a/api-js/src/locale/fr/messages.js b/api-js/src/locale/fr/messages.js index 1470631bd9..dce9c7b498 100644 --- a/api-js/src/locale/fr/messages.js +++ b/api-js/src/locale/fr/messages.js @@ -83,6 +83,7 @@ 'todo', 'Successfully removed domain: {0} from {1}.': 'todo', 'Successfully removed organization: {0}.': 'todo', + 'Successfully removed user from organization.': 'todo', 'Successfully sent invitation to service, and organization email.': 'todo', 'Successfully two factor authenticated.': 'todo', 'Successfully verified account.': 'todo', @@ -136,10 +137,9 @@ 'Unable to query affiliations. Please try again.': 'todo', 'Unable to query domains. Please try again.': 'todo', 'Unable to query organizations. Please try again.': 'todo', - 'Unable to query user without a username, please try again.': 'todo', - 'Unable to query user, please try again.': 'todo', 'Unable to remove domain. Please try again.': 'todo', 'Unable to remove organization. Please try again.': 'todo', + 'Unable to remove user from organization. Please try again.': 'todo', 'Unable to reset password. Please try again.': 'todo', 'Unable to retrieve {0} for domain: {domain}.': 'todo', 'Unable to send TFA code, please try again.': 'todo', diff --git a/api-js/src/locale/fr/messages.po b/api-js/src/locale/fr/messages.po index 657f9a388b..d3512b5b56 100644 --- a/api-js/src/locale/fr/messages.po +++ b/api-js/src/locale/fr/messages.po @@ -30,7 +30,7 @@ msgstr "todo" msgid "Authentication error. Please sign in." msgstr "todo" -#: src/types/base/index.js:870 +#: src/types/base/index.js:866 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "todo" @@ -80,7 +80,7 @@ msgstr "todo" msgid "Organization has already been verified." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:43 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:42 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:42 msgid "Passing both `first` and `last` to paginate the `affiliation` is not supported." msgstr "todo" @@ -158,10 +158,10 @@ msgstr "todo" msgid "Passwords do not match." msgstr "todo" -#: src/auth/check-user-is-admin-for-user.js:17 -#: src/auth/check-user-is-admin-for-user.js:27 -#: src/auth/check-user-is-admin-for-user.js:59 -#: src/auth/check-user-is-admin-for-user.js:69 +#: src/auth/check-user-is-admin-for-user.js:19 +#: src/auth/check-user-is-admin-for-user.js:29 +#: src/auth/check-user-is-admin-for-user.js:60 +#: src/auth/check-user-is-admin-for-user.js:70 msgid "Permission error, not an admin for this user." msgstr "todo" @@ -169,7 +169,7 @@ msgstr "todo" msgid "Profile successfully updated." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:66 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:65 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:65 msgid "Requesting `{amount}` records on the `affiliations` exceeds the `{argSet}` limit of 100 records." msgstr "todo" @@ -238,6 +238,10 @@ msgstr "todo" msgid "Successfully removed organization: {0}." msgstr "todo" +#: src/mutations/user-affiliations/remove-user-from-org.js:118 +msgid "Successfully removed user from organization." +msgstr "todo" + #: src/mutations/user-affiliations/invite-user-to-org.js:127 msgid "Successfully sent invitation to service, and organization email." msgstr "todo" @@ -403,7 +407,7 @@ msgstr "todo" msgid "Unable to invite yourself to an org. Please try again." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:147 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:146 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:146 msgid "Unable to load affiliations. Please try again." msgstr "todo" @@ -499,7 +503,7 @@ msgstr "todo" msgid "Unable to load web summary. Please try again." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:137 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:136 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:136 msgid "Unable to query affiliations. Please try again." msgstr "todo" @@ -512,14 +516,6 @@ msgstr "todo" msgid "Unable to query organizations. Please try again." msgstr "todo" -#: src/queries/user/find-user-by-username.js:34 -msgid "Unable to query user without a username, please try again." -msgstr "todo" - -#: src/queries/user/find-user-by-username.js:50 -msgid "Unable to query user, please try again." -msgstr "todo" - #: src/mutations/domain/remove-domain.js:58 #: src/mutations/domain/remove-domain.js:69 #: src/mutations/domain/remove-domain.js:80 @@ -541,6 +537,14 @@ msgstr "todo" msgid "Unable to remove organization. Please try again." msgstr "todo" +#: src/mutations/user-affiliations/remove-user-from-org.js:52 +#: src/mutations/user-affiliations/remove-user-from-org.js:63 +#: src/mutations/user-affiliations/remove-user-from-org.js:74 +#: src/mutations/user-affiliations/remove-user-from-org.js:102 +#: src/mutations/user-affiliations/remove-user-from-org.js:113 +msgid "Unable to remove user from organization. Please try again." +msgstr "todo" + #: src/mutations/user/reset-password.js:60 #: src/mutations/user/reset-password.js:70 #: src/mutations/user/reset-password.js:78 @@ -677,7 +681,7 @@ msgstr "todo" msgid "Unable to verify organization. Please try again." msgstr "todo" -#: src/queries/user/find-user-by-username.js:60 +#: src/queries/user/find-user-by-username.js:40 msgid "User could not be queried." msgstr "todo" @@ -697,7 +701,7 @@ msgstr "todo" msgid "We've sent you an email with an authentication code to sign into Pulse." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:34 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:33 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:33 msgid "You must provide a `first` or `last` value to properly paginate the `affiliation`." msgstr "todo" @@ -767,7 +771,7 @@ msgstr "todo" #: src/loaders/guidance-tags/load-ssl-guidance-tags-connections.js:77 #: src/loaders/organizations/load-organization-connections-by-domain-id.js:81 #: src/loaders/organizations/load-organization-connections-by-user-id.js:81 -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:81 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:80 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:80 #: src/loaders/verified-domains/load-verified-domain-connections-by-organization-id.js:79 #: src/loaders/verified-domains/load-verified-domain-connections.js:80 @@ -778,7 +782,7 @@ msgstr "todo" msgid "`{argSet}` must be of type `number` not `{typeSet}`." msgstr "todo" -#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:55 +#: src/loaders/user-affiliations/load-user-affiliations-by-org-id.js:54 #: src/loaders/user-affiliations/load-user-affiliations-by-user-id.js:54 msgid "`{argSet}` on the `affiliations` cannot be less than zero." msgstr "todo" diff --git a/api-js/src/mutations/user-affiliations/__tests__/remove-user-from-org.test.js b/api-js/src/mutations/user-affiliations/__tests__/remove-user-from-org.test.js new file mode 100644 index 0000000000..2fe254b6a4 --- /dev/null +++ b/api-js/src/mutations/user-affiliations/__tests__/remove-user-from-org.test.js @@ -0,0 +1,2143 @@ +const { DB_PASS: rootPass, DB_URL: url } = process.env + +const { setupI18n } = require('@lingui/core') +const { ArangoTools, dbNameFromFile } = require('arango-tools') +const { graphql, GraphQLSchema, GraphQLError } = require('graphql') +const { toGlobalId } = require('graphql-relay') + +const englishMessages = require('../../../locale/en/messages') +const frenchMessages = require('../../../locale/fr/messages') +const { makeMigrations } = require('../../../../migrations') +const { createQuerySchema } = require('../../../queries') +const { createMutationSchema } = require('../../index') +const { cleanseInput } = require('../../../validators') +const { checkPermission, userRequired } = require('../../../auth') +const { + orgLoaderByKey, + userLoaderByKey, + affiliationLoaderByKey, +} = require('../../../loaders') + +describe('removing a user from an organization', () => { + let query, + drop, + truncate, + collections, + transaction, + migrate, + schema, + i18n, + orgOne, + orgTwo, + admin, + user, + affiliation + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + + beforeAll(async () => { + // Generate DB Items + ;({ migrate } = await ArangoTools({ rootPass, url })) + ;({ query, drop, truncate, collections, transaction } = await migrate( + makeMigrations({ databaseName: dbNameFromFile(__filename), rootPass }), + )) + + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + }) + + beforeEach(async () => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + + orgOne = await collections.organizations.save({ + verified: true, + summaries: { + web: { + pass: 50, + fail: 1000, + total: 1050, + }, + mail: { + pass: 50, + fail: 1000, + total: 1050, + }, + }, + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + orgTwo = await collections.organizations.save({ + verified: true, + summaries: { + web: { + pass: 50, + fail: 1000, + total: 1050, + }, + mail: { + pass: 50, + fail: 1000, + total: 1050, + }, + }, + orgDetails: { + en: { + slug: 'communications-security-establishment', + acronym: 'CSE', + name: 'Communications Security Establishment', + zone: 'FED', + sector: 'DND', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'centre-de-la-securite-des-telecommunications', + acronym: 'CST', + name: 'Centre de la Securite des Telecommunications', + zone: 'FED', + sector: 'DND', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + admin = await collections.users.save({ + userName: 'admin.account@istio.actually.exists', + displayName: 'Test Admin', + preferredLang: 'french', + tfaValidated: false, + emailValidated: false, + }) + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + displayName: 'Test Account', + preferredLang: 'french', + tfaValidated: false, + emailValidated: false, + }) + }) + + afterEach(async () => { + await truncate() + consoleOutput.length = 0 + }) + + afterAll(async () => { + await drop() + }) + + describe('given a users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + language: 'en', + locales: ['en', 'fr'], + missing: 'Traduction manquante', + catalogs: { + en: englishMessages, + fr: frenchMessages, + }, + }) + }) + describe('given a successful removal', () => { + describe('user is a super admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + }) + describe('super admin can remove an admin from any org', () => { + beforeEach(async () => { + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'admin', + }) + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'Successfully removed user from organization.', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + describe('super admin can remove a user from any org', () => { + beforeEach(async () => { + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'Successfully removed user from organization.', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + }) + describe('user is an org admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + describe('user can remove a user from the shared org', () => { + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'Successfully removed user from organization.', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + }) + }) + describe('given an unsuccessful removal', () => { + describe('org is not found', () => { + beforeEach(async () => { + await query` + FOR org IN organizations + REMOVE org IN organizations + RETURN true + ` + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however no org with that id could be found.`, + ]) + }) + }) + describe('super admin attempts to remove another super admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'super_admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'super_admin', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, but they do not have the right permission.`, + ]) + }) + }) + describe('requesting user is an admin for another org', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgTwo._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key}, but they do not have any affiliations to org: ${orgOne._key}.`, + ]) + }) + }) + describe('admin attempts to remove another admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'admin', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, but they do not have the right permission.`, + ]) + }) + }) + describe('requesting user is not an admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'user', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however they do not have the permission to remove users.`, + ]) + }) + }) + describe('requested user is not found', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + await query` + FOR user IN users + FILTER user._key == ${user._key} + REMOVE user IN users + RETURN true + ` + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however no user with that id could be found.`, + ]) + }) + }) + }) + describe('database error occurs', () => { + describe('when checking requested users permission in requested org', () => { + let mockedQuery + beforeEach(async () => { + mockedQuery = jest + .fn() + .mockRejectedValue(new Error('Database error occurred.')) + + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query: mockedQuery, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Database error occurred when user: ${admin._key} attempted to check the current permission of user: ${user._key} to see if they could be removed.`, + ]) + }) + }) + }) + describe('transaction error occurs', () => { + let mockedTransaction + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + describe('when running transaction', () => { + beforeEach(() => { + mockedTransaction = jest.fn().mockReturnValue({ + run() { + throw new Error('Transaction error occurred.') + }, + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction: mockedTransaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Transaction run error occurred when user: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, error: Error: Transaction error occurred.`, + ]) + }) + }) + describe('when committing transaction', () => { + beforeEach(() => { + mockedTransaction = jest.fn().mockReturnValue({ + run() { + return undefined + }, + commit() { + throw new Error('Transaction error occurred.') + }, + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction: mockedTransaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'en', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'Unable to remove user from organization. Please try again.', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Transaction commit error occurred when user: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, error: Error: Transaction error occurred.`, + ]) + }) + }) + }) + }) + describe('users language is set to french', () => { + beforeAll(() => { + i18n = setupI18n({ + language: 'fr', + locales: ['en', 'fr'], + missing: 'Traduction manquante', + catalogs: { + en: englishMessages, + fr: frenchMessages, + }, + }) + }) + describe('given a successful removal', () => { + describe('user is a super admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + }) + describe('super admin can remove an admin from any org', () => { + beforeEach(async () => { + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'admin', + }) + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'todo', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + describe('super admin can remove a user from any org', () => { + beforeEach(async () => { + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'todo', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + }) + describe('user is an org admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + describe('user can remove a user from the shared org', () => { + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + removeUserFromOrg: { + status: 'todo', + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + it('removes the user from the org', async () => { + await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const loader = affiliationLoaderByKey(query, admin._key, i18n) + + const data = await loader.load(affiliation._key) + + expect(data).toEqual(undefined) + expect(consoleOutput).toEqual([ + `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, + ]) + }) + }) + }) + }) + describe('given an unsuccessful removal', () => { + describe('org is not found', () => { + beforeEach(async () => { + await query` + FOR org IN organizations + REMOVE org IN organizations + RETURN true + ` + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however no org with that id could be found.`, + ]) + }) + }) + describe('super admin attempts to remove another super admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'super_admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'super_admin', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, but they do not have the right permission.`, + ]) + }) + }) + describe('requesting user is an admin for another org', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgTwo._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key}, but they do not have any affiliations to org: ${orgOne._key}.`, + ]) + }) + }) + describe('admin attempts to remove another admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'admin', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, but they do not have the right permission.`, + ]) + }) + }) + describe('requesting user is not an admin', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'user', + }) + affiliation = await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however they do not have the permission to remove users.`, + ]) + }) + }) + describe('requested user is not found', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgOne._id, + _to: admin._id, + permission: 'admin', + }) + await query` + FOR user IN users + FILTER user._key == ${user._key} + REMOVE user IN users + RETURN true + ` + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, however no user with that id could be found.`, + ]) + }) + }) + }) + describe('database error occurs', () => { + describe('when checking requested users permission in requested org', () => { + let mockedQuery + beforeEach(async () => { + mockedQuery = jest + .fn() + .mockRejectedValue(new Error('Database error occurred.')) + + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query: mockedQuery, + collections, + transaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Database error occurred when user: ${admin._key} attempted to check the current permission of user: ${user._key} to see if they could be removed.`, + ]) + }) + }) + }) + describe('transaction error occurs', () => { + let mockedTransaction + beforeEach(async () => { + await collections.affiliations.save({ + _from: orgTwo._id, + _to: admin._id, + permission: 'super_admin', + }) + await collections.affiliations.save({ + _from: orgOne._id, + _to: user._id, + permission: 'user', + }) + }) + describe('when running transaction', () => { + beforeEach(() => { + mockedTransaction = jest.fn().mockReturnValue({ + run() { + throw new Error('Transaction error occurred.') + }, + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction: mockedTransaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Transaction run error occurred when user: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, error: Error: Transaction error occurred.`, + ]) + }) + }) + describe('when committing transaction', () => { + beforeEach(() => { + mockedTransaction = jest.fn().mockReturnValue({ + run() { + return undefined + }, + commit() { + throw new Error('Transaction error occurred.') + }, + }) + }) + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeUserFromOrg ( + input: { + userId: "${toGlobalId('users', user._key)}" + orgId: "${toGlobalId('organizations', orgOne._key)}" + } + ) { + status + } + } + `, + null, + { + i18n, + query, + collections, + transaction: mockedTransaction, + userKey: admin._key, + auth: { + checkPermission: checkPermission({ + i18n, + userKey: admin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: admin._key, + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }), + }, + loaders: { + orgLoaderByKey: orgLoaderByKey(query, 'fr', admin._key, i18n), + userLoaderByKey: userLoaderByKey(query, admin._key, i18n), + }, + validators: { cleanseInput }, + }, + ) + + const error = [ + new GraphQLError( + 'todo', + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Transaction commit error occurred when user: ${admin._key} attempted to remove user: ${user._key} from org: ${orgOne._key}, error: Error: Transaction error occurred.`, + ]) + }) + }) + }) + }) +}) diff --git a/api-js/src/mutations/user-affiliations/index.js b/api-js/src/mutations/user-affiliations/index.js index 663f86c446..efec5e6a02 100644 --- a/api-js/src/mutations/user-affiliations/index.js +++ b/api-js/src/mutations/user-affiliations/index.js @@ -1,7 +1,9 @@ const { inviteUserToOrg } = require('./invite-user-to-org') +const { removeUserFromOrg } = require('./remove-user-from-org') const { updateUserRole } = require('./update-user-role') module.exports = { inviteUserToOrg, + removeUserFromOrg, updateUserRole, } diff --git a/api-js/src/mutations/user-affiliations/remove-user-from-org.js b/api-js/src/mutations/user-affiliations/remove-user-from-org.js new file mode 100644 index 0000000000..fbc6239f6c --- /dev/null +++ b/api-js/src/mutations/user-affiliations/remove-user-from-org.js @@ -0,0 +1,176 @@ +const { GraphQLNonNull, GraphQLID, GraphQLString } = require('graphql') +const { mutationWithClientMutationId, fromGlobalId } = require('graphql-relay') +const { t } = require('@lingui/macro') + +const removeUserFromOrg = new mutationWithClientMutationId({ + name: 'RemoveUserFromOrg', + description: '', + inputFields: () => ({ + userId: { + type: GraphQLNonNull(GraphQLID), + description: '', + }, + orgId: { + type: GraphQLNonNull(GraphQLID), + description: '', + }, + }), + outputFields: () => ({ + status: { + type: GraphQLString, + description: '', + resolve: ({ status }) => status, + }, + }), + mutateAndGetPayload: async ( + args, + { + i18n, + query, + collections, + transaction, + userKey, + auth: { checkPermission, userRequired }, + loaders: { orgLoaderByKey, userLoaderByKey }, + validators: { cleanseInput }, + }, + ) => { + // Cleanse Input + const { id: requestedUserKey } = fromGlobalId(cleanseInput(args.userId)) + const { id: requestedOrgKey } = fromGlobalId(cleanseInput(args.orgId)) + + // Get requesting user + await userRequired() + + // Get requested org + const requestedOrg = await orgLoaderByKey.load(requestedOrgKey) + if (typeof requestedOrg === 'undefined') { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUserKey} from org: ${requestedOrgKey}, however no org with that id could be found.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + // Check requesting users permission + const permission = await checkPermission({ orgId: requestedOrg._id }) + if (permission === 'user' || typeof permission === 'undefined') { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUserKey} from org: ${requestedOrg._key}, however they do not have the permission to remove users.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + // Get requested user + const requestedUser = await userLoaderByKey.load(requestedUserKey) + if (typeof requestedUser === 'undefined') { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUserKey} from org: ${requestedOrg._key}, however no user with that id could be found.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + // Get requested users current permission level + let affiliationCursor + try { + affiliationCursor = await query` + FOR v, e IN 1..1 ANY ${requestedUser._id} affiliations + FILTER e._from == ${requestedOrg._id} + RETURN { _key: e._key, permission: e.permission } + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to check the current permission of user: ${requestedUser._key} to see if they could be removed.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + if (affiliationCursor.count < 1) { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUser._key}, but they do not have any affiliations to org: ${requestedOrg._key}.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + const affiliation = await affiliationCursor.next() + + let canRemove + if ( + permission === 'super_admin' && + (affiliation.permission === 'admin' || affiliation.permission === 'user') + ) { + canRemove = true + } else if (permission === 'admin' && affiliation.permission === 'user') { + canRemove = true + } else { + canRemove = false + } + + if (canRemove) { + // Generate list of collections names + const collectionStrings = [] + for (const property in collections) { + collectionStrings.push(property.toString()) + } + + // Setup Transaction + const trx = await transaction(collectionStrings) + + try { + await trx.run(async () => { + await query` + FOR aff IN affiliations + FILTER aff._from == ${requestedOrg._id} + FILTER aff._to == ${requestedUser._id} + REMOVE aff IN affiliations + RETURN true + ` + }) + } catch (err) { + console.error( + `Transaction run error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + try { + await trx.commit() + } catch (err) { + console.error( + `Transaction commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + + console.info(`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`) + + return { + status: i18n._(t`Successfully removed user from organization.`), + } + } else { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, but they do not have the right permission.`, + ) + throw new Error( + i18n._(t`Unable to remove user from organization. Please try again.`), + ) + } + }, +}) + +module.exports = { + removeUserFromOrg, +}