console/integration-tests/tests/api/organization/members.spec.ts

383 lines
13 KiB
TypeScript

import {
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from 'testkit/gql/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();
Object.values(OrganizationAccessScope).forEach(scope => {
expect(organization.owner.organizationAccessScopes).toContain(scope);
});
Object.values(ProjectAccessScope).forEach(scope => {
expect(organization.owner.projectAccessScopes).toContain(scope);
});
Object.values(TargetAccessScope).forEach(scope => {
expect(organization.owner.targetAccessScopes).toContain(scope);
});
});
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();
// Should have only organization:read access
expect(member.organizationAccessScopes).toContainEqual(OrganizationAccessScope.Read);
// Nothing more
expect(member.organizationAccessScopes).toHaveLength(1);
// Should have only project:read and project:operations-store:read access
expect(member.projectAccessScopes).toContainEqual(ProjectAccessScope.Read);
expect(member.projectAccessScopes).toContainEqual(ProjectAccessScope.OperationsStoreRead);
// Nothing more
expect(member.projectAccessScopes).toHaveLength(2);
// Should have only target:read and target:registry:read access
expect(member.targetAccessScopes).toContainEqual(TargetAccessScope.Read);
expect(member.targetAccessScopes).toContainEqual(TargetAccessScope.RegistryRead);
// Nothing more
expect(member.targetAccessScopes).toHaveLength(2);
});
test.concurrent(
'cannot create a role with an access scope that user has no access to',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember, organization } = await createOrg();
const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember();
// Create a role with organization:members access, so we could perform the test.
// To create a role, user must have access to organization:members first.
const membersManagerRole = await createMemberRole({
organization: [OrganizationAccessScope.Members],
project: [],
target: [],
});
await assignMemberRole({
roleId: membersManagerRole.id,
userId: member.user.id,
});
await expect(
createMemberRole(
{
organization: [OrganizationAccessScope.Settings], // <-- this scope is not part of the membersManagerRole, so it should fail
project: [],
target: [],
},
{
useMemberToken: true,
},
),
).rejects.toThrowError('Missing access');
},
);
test.concurrent(
'cannot grant an access scope to another user if user has no access to that scope',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember, organization } = await createOrg();
const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember();
const { member: viewerRoleMember } = await inviteAndJoinMember();
// Create a role with organization:members access, so we could perform the test.
// To create a role, user must have access to organization:members first.
const membersManagerRole = await createMemberRole({
organization: [OrganizationAccessScope.Members],
project: [],
target: [],
});
await assignMemberRole({
roleId: membersManagerRole.id,
userId: member.user.id,
});
const adminRoleId = organization.memberRoles.find(r => r.name === 'Admin')?.id;
if (!adminRoleId) {
throw new Error('Admin role not found');
}
await expect(
assignMemberRole(
{
roleId: adminRoleId,
userId: viewerRoleMember.user.id,
},
{
useMemberToken: true,
},
),
).rejects.toThrowError('Missing access');
},
);
test.concurrent(
'granting no scopes is equal to setting read-only for org, project and target',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { createMemberRole } = await inviteAndJoinMember();
const readOnlyRole = await createMemberRole({
organization: [],
project: [],
target: [],
});
expect(readOnlyRole?.organizationAccessScopes).toHaveLength(1);
expect(readOnlyRole?.organizationAccessScopes).toContainEqual(OrganizationAccessScope.Read);
expect(readOnlyRole?.projectAccessScopes).toHaveLength(1);
expect(readOnlyRole?.projectAccessScopes).toContainEqual(ProjectAccessScope.Read);
expect(readOnlyRole?.targetAccessScopes).toHaveLength(1);
expect(readOnlyRole?.targetAccessScopes).toContainEqual(TargetAccessScope.Read);
},
);
test.concurrent('cannot downgrade a member when assigning a new role', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember();
const { member: viewerRoleMember } = await inviteAndJoinMember();
const managerRole = await createMemberRole({
organization: [OrganizationAccessScope.Members],
project: [ProjectAccessScope.Settings],
target: [
TargetAccessScope.RegistryRead,
TargetAccessScope.RegistryWrite,
TargetAccessScope.Settings,
],
});
const originalRole = await createMemberRole({
organization: [],
project: [],
target: [
TargetAccessScope.RegistryRead,
TargetAccessScope.RegistryWrite,
TargetAccessScope.Settings,
],
});
const roleWithLessAccess = await createMemberRole({
organization: [],
project: [],
target: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite],
});
await assignMemberRole({
roleId: managerRole.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: originalRole.id,
userId: viewerRoleMember.user.id,
});
// non-admin member cannot downgrade another member
await expect(
assignMemberRole(
{
roleId: roleWithLessAccess.id,
userId: viewerRoleMember.user.id,
},
{
useMemberToken: true,
},
),
).rejects.toThrowError('Cannot downgrade member');
// admin can downgrade another member
await assignMemberRole({
roleId: roleWithLessAccess.id,
userId: viewerRoleMember.user.id,
});
});
test.concurrent('cannot downgrade a member when modifying a role', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { inviteAndJoinMember } = await createOrg();
const { createMemberRole, assignMemberRole, member, updateMemberRole } =
await inviteAndJoinMember();
const { member: viewerRoleMember } = await inviteAndJoinMember();
const managerRole = await createMemberRole({
organization: [OrganizationAccessScope.Members],
project: [ProjectAccessScope.Settings],
target: [
TargetAccessScope.RegistryRead,
TargetAccessScope.RegistryWrite,
TargetAccessScope.Settings,
],
});
const roleToBeUpdated = await createMemberRole({
organization: [],
project: [],
target: [
TargetAccessScope.RegistryRead,
TargetAccessScope.RegistryWrite,
TargetAccessScope.Settings,
],
});
await assignMemberRole({
roleId: managerRole.id,
userId: member.user.id,
});
await assignMemberRole({
roleId: roleToBeUpdated.id,
userId: viewerRoleMember.user.id,
});
// non-admin member cannot downgrade another member
await expect(
updateMemberRole(
roleToBeUpdated,
{
organization: [],
project: [],
target: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], // <-- downgrade (missing: TargetAccessScope.Settings)
},
{
useMemberToken: true,
},
),
).rejects.toThrowError('Cannot downgrade member');
// admin can downgrade another member
await updateMemberRole(roleToBeUpdated, {
organization: [],
project: [],
target: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], // <-- downgrade (missing: TargetAccessScope.Settings)
});
});
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({
organization: [OrganizationAccessScope.Members],
project: [],
target: [],
});
const readOnlyRole = await createMemberRole({
organization: [],
project: [],
target: [],
});
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',
);
// delete the role as a member with enough access to delete the role
await expect(
deleteMemberRole(readOnlyRole.id, {
useMemberToken: true,
}),
).rejects.toThrowError('Cannot delete a role with members');
});
test.concurrent('cannot invite a member with more access than the inviter', async ({ expect }) => {
const seed = initSeed();
const { createOrg } = await seed.createOwner();
const { inviteMember, inviteAndJoinMember, organization } = await createOrg();
const {
member: invitingMember,
memberToken: invitingMemberToken,
createMemberRole,
assignMemberRole,
} = await inviteAndJoinMember();
const adminRoleId = organization.memberRoles.find(r => r.name === 'Admin')?.id;
if (!adminRoleId) {
throw new Error('Admin role not found');
}
const membersManagerRole = await createMemberRole({
organization: [OrganizationAccessScope.Members],
project: [],
target: [],
});
const readOnlyRole = await createMemberRole({
organization: [],
project: [],
target: [],
});
// give the inviting member a role with enough access to invite other members
await assignMemberRole({
roleId: membersManagerRole.id,
userId: invitingMember.user.id,
});
const inviteEmail = seed.generateEmail();
const failedInvitationResult = await inviteMember(inviteEmail, invitingMemberToken, adminRoleId);
expect(failedInvitationResult.error?.message).toEqual(
expect.stringContaining('Not enough access to invite a member'),
);
const invitationResult = await inviteMember(inviteEmail, invitingMemberToken, readOnlyRole.id);
const inviteCode = invitationResult.ok?.code;
expect(inviteCode).toBeDefined();
});
test.concurrent('email invitation', async ({ expect }) => {
const seed = initSeed();
const { createOrg } = await seed.createOwner();
const { inviteMember } = await createOrg();
const inviteEmail = seed.generateEmail();
const invitationResult = await inviteMember(inviteEmail);
const inviteCode = invitationResult.ok?.code;
expect(inviteCode).toBeDefined();
const sentEmails = await history();
expect(sentEmails).toContainEqual(expect.objectContaining({ to: inviteEmail }));
});
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 invitationResult = await inviteMember();
const inviteCode = invitationResult.ok!.code;
expect(inviteCode).toBeDefined();
// Join
const extra = seed.generateEmail();
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');
},
);