console/integration-tests/tests/api/organization/members.spec.ts
2026-04-02 12:43:37 +02:00

271 lines
8.7 KiB
TypeScript

import { pollFor } from 'testkit/flow';
import { graphql } from 'testkit/gql';
import { ResourceAssignmentModeType } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { history } from '../../../testkit/emails';
import { initSeed } from '../../../testkit/seed';
test.concurrent('owner of an organization should have all scopes', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { organization } = await createOrg();
expect(organization.owner.role.permissions).toMatchInlineSnapshot(`
[
organization:describe,
support:manageTickets,
accessToken:modify,
organization:modifySlug,
auditLog:export,
organization:delete,
member:describe,
member:modify,
billing:describe,
billing:update,
oidc:modify,
gitHubIntegration:modify,
slackIntegration:modify,
project:create,
schemaLinting:modifyOrganizationRules,
personalAccessToken:modify,
project:describe,
project:delete,
project:modifySettings,
projectAccessToken:modify,
sharedSavedFilter:modify,
schemaLinting:modifyProjectRules,
target:create,
alert:modify,
target:delete,
target:modifySettings,
targetAccessToken:modify,
cdnAccessToken:modify,
laboratory:describe,
laboratory:modify,
laboratory:modifyPreflightScript,
schemaProposal:describe,
schemaProposal:modify,
schema:compose,
usage:report,
traces:report,
schemaCheck:approve,
schemaCheck:create,
schemaVersion:publish,
schemaVersion:deleteService,
appDeployment:create,
appDeployment:publish,
appDeployment:retire,
]
`);
});
test.concurrent('invited member should have basic scopes (Viewer role)', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { member } = await inviteAndJoinMember();
expect(member.role.permissions).toMatchInlineSnapshot(`
[
organization:describe,
support:manageTickets,
project:describe,
laboratory:describe,
]
`);
});
test.concurrent(
'granting no permissions is equal to setting read-only access for the organization',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { createMemberRole } = await inviteAndJoinMember();
const readOnlyRole = await createMemberRole([]);
expect(readOnlyRole.permissions).toMatchInlineSnapshot(`
[
organization:describe,
]
`);
},
);
test.concurrent('cannot delete a role with members', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { createMemberRole, deleteMemberRole, assignMemberRole, member } =
await inviteAndJoinMember();
const { member: viewerRoleMember } = await inviteAndJoinMember();
const membersManagerRole = await createMemberRole([]);
const readOnlyRole = await createMemberRole([]);
await assignMemberRole({
roleId: membersManagerRole.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: readOnlyRole.id,
userId: viewerRoleMember.user.id,
});
// delete the role as the owner
await expect(deleteMemberRole(readOnlyRole.id)).rejects.toThrowError(
'Cannot delete a role with members',
);
});
test.concurrent('email invitation', async ({ expect }) => {
const seed = initSeed();
const { createOrg } = await seed.createOwner();
const { inviteMember, organization } = await createOrg();
const inviteEmail = seed.generateEmail();
const invitationResult = await inviteMember(inviteEmail);
const inviteCode = invitationResult.ok?.createdOrganizationInvitation.code;
expect(inviteCode).toBeDefined();
await pollFor(async () => {
const sentEmails = await history(inviteEmail);
return sentEmails.length > 0;
});
const sentEmails = await history(inviteEmail);
expect(sentEmails[0].subject).toEqual('You have been invited to join ' + organization.slug);
});
test.concurrent('can not invite with role not existing in organization', async ({ expect }) => {
const seed = initSeed();
const owner1 = await seed.createOwner();
const org1 = await owner1.createOrg();
const owner2 = await seed.createOwner();
const org2 = await owner2.createOrg();
// no idea why the createMemberRole functionality is "hidden" in the tests SDK and requires to invite a user first to get to :D
const org2Members = await org2.inviteAndJoinMember();
const org2Role = await org2Members.createMemberRole(['organization:describe']);
const result = await org1.inviteMember(undefined, undefined, org2Role.id);
expect(result).toEqual({
error: {
message: 'The provided member role does not exist.',
},
ok: null,
});
});
test.concurrent('invite user with assigned resouces', async ({ expect }) => {
const seed = initSeed();
const owner = await seed.createOwner();
const org = await owner.createOrg();
const { project: project1 } = await org.createProject();
// we just create this to make sure it does not show up :)
const { project: _project2 } = await org.createProject();
const { project: project3 } = await org.createProject();
const m = await org.inviteAndJoinMember();
const role = await m.createMemberRole(['organization:describe', 'project:describe']);
const member = await org.inviteAndJoinMember({
memberRoleId: role.id,
resources: {
mode: ResourceAssignmentModeType.Granular,
projects: [
{
projectId: project1.id,
targets: { mode: ResourceAssignmentModeType.Granular, targets: [] },
},
{
projectId: project3.id,
targets: { mode: ResourceAssignmentModeType.Granular, targets: [] },
},
],
},
});
const result = await org.projects(member.memberToken);
expect(result).toHaveLength(2);
expect(result[0].id).toEqual(project3.id);
expect(result[1].id).toEqual(project1.id);
});
test.concurrent(
'cannot join organization twice using the same invitation code',
async ({ expect }) => {
const seed = initSeed();
const { createOrg } = await seed.createOwner();
const { inviteMember, joinMemberUsingCode } = await createOrg();
// Invite
const extra = seed.generateEmail();
const invitationResult = await inviteMember(extra);
const inviteCode = invitationResult.ok!.createdOrganizationInvitation.code;
expect(inviteCode).toBeDefined();
// Join
const { access_token: member_access_token } = await seed.authenticate(extra);
const joinResult = await (
await joinMemberUsingCode(inviteCode, member_access_token)
).expectNoGraphQLErrors();
expect(joinResult.joinOrganization.__typename).toBe('OrganizationPayload');
if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') {
throw new Error('Join failed');
}
const other = seed.generateEmail();
const { access_token: other_access_token } = await seed.authenticate(other);
const otherJoinResult = await (
await joinMemberUsingCode(inviteCode, other_access_token)
).expectNoGraphQLErrors();
expect(otherJoinResult.joinOrganization.__typename).toBe('OrganizationInvitationError');
},
);
const OrganizationInvitationsQuery = graphql(`
query OrganizationInvitationsQuery($organizationSlug: String!) {
organization: organizationBySlug(organizationSlug: $organizationSlug) {
id
invitations {
edges {
node {
id
}
}
}
}
}
`);
test.concurrent(
'Organization.invitations resolves to null without error if user does not have member:modify permission',
async ({ expect }) => {
const seed = initSeed();
const { createOrg } = await seed.createOwner();
const { organization, inviteAndJoinMember } = await createOrg();
const { createMemberRole, assignMemberRole, updateMemberRole, memberToken, member } =
await inviteAndJoinMember();
const role = await createMemberRole([]);
await assignMemberRole({ roleId: role.id, userId: member.id });
let result = await execute({
document: OrganizationInvitationsQuery,
variables: {
organizationSlug: organization.slug,
},
authToken: memberToken,
}).then(r => r.expectNoGraphQLErrors());
expect(result.organization!.invitations).toEqual(null);
await updateMemberRole(role, ['member:modify']);
result = await execute({
document: OrganizationInvitationsQuery,
variables: {
organizationSlug: organization.slug,
},
authToken: memberToken,
}).then(r => r.expectNoGraphQLErrors());
expect(result.organization!.invitations).not.toEqual(null);
},
);