mirror of
https://github.com/Gimanh/taskview-community.git
synced 2026-09-24 03:16:18 +00:00
fix: #98
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { TvApi } from '@/tv'
|
||||
import { TvPermissions } from '@/api/permissions'
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { initApi } from './init-api'
|
||||
|
||||
describe('Collaboration roles access control', () => {
|
||||
let user1Api: TvApi
|
||||
let user2Api: TvApi
|
||||
let user2Email: string
|
||||
let deleteAllGoals: () => Promise<void>
|
||||
let manageUsersPermissionId: number
|
||||
|
||||
beforeAll(async () => {
|
||||
const init = await initApi()
|
||||
user1Api = init.$tvApi
|
||||
user2Api = init.$tvApiForSecondUser
|
||||
user2Email = init.user2Email
|
||||
deleteAllGoals = init.deleteAllGoals
|
||||
|
||||
const allPermissions = await user1Api.collaboration.fetchAllPermissions()
|
||||
const found = allPermissions.find(p => p.name === TvPermissions.GOAL_CAN_MANAGE_USERS)
|
||||
if (!found) throw new Error('Permission "goal_can_manage_users" is not in DB')
|
||||
manageUsersPermissionId = found.id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteAllGoals()
|
||||
})
|
||||
|
||||
async function expectHttpStatus<T>(promise: Promise<T>, status: number): Promise<void> {
|
||||
try {
|
||||
await promise
|
||||
throw new Error(`Expected HTTP ${status} but request succeeded`)
|
||||
} catch (e: any) {
|
||||
if (typeof e.message === 'string' && e.message.startsWith('Expected HTTP')) throw e
|
||||
expect(e.response?.status, `Expected ${status}, got ${e.response?.status}`).toBe(status)
|
||||
}
|
||||
}
|
||||
|
||||
async function createGoalWithUser2(grantManageUsers: boolean) {
|
||||
const goal = await user1Api.goals.createGoal({ name: `Roles access ${Date.now()}` })
|
||||
if (!goal) throw new Error('Failed to create goal')
|
||||
|
||||
const collab = await user1Api.collaboration.inviteUserToGoal({ email: user2Email, goalId: goal.id })
|
||||
if (!collab) throw new Error('Failed to invite user2')
|
||||
|
||||
const role = await user1Api.collaboration.createRoleForGoal({
|
||||
goalId: goal.id,
|
||||
roleName: `Manager ${Date.now()}`,
|
||||
})
|
||||
if (!role) throw new Error('Failed to create role')
|
||||
|
||||
if (grantManageUsers) {
|
||||
const toggled = await user1Api.collaboration.toggleRolePermission({
|
||||
roleId: role.id,
|
||||
permissionId: manageUsersPermissionId,
|
||||
})
|
||||
if (!toggled || toggled.add !== true) {
|
||||
throw new Error(`Expected goal_can_manage_users to be added, got ${JSON.stringify(toggled)}`)
|
||||
}
|
||||
}
|
||||
|
||||
await user1Api.collaboration.toggleUserRoles({
|
||||
goalId: goal.id,
|
||||
userId: collab.id,
|
||||
roles: [role.id],
|
||||
})
|
||||
|
||||
return { goal, role }
|
||||
}
|
||||
|
||||
it('collaborator with goal_can_manage_users can read the role-to-permission matrix', async () => {
|
||||
const { goal, role } = await createGoalWithUser2(true)
|
||||
|
||||
const matrix = await user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id)
|
||||
expect(matrix).toBeDefined()
|
||||
// the granted permission is visible in the matrix of the role user2 holds
|
||||
expect(matrix?.some(row => row.roleId === role.id && row.permissionId === manageUsersPermissionId)).toBe(true)
|
||||
})
|
||||
|
||||
it('collaborator without goal_can_manage_users cannot read the matrix', async () => {
|
||||
const { goal } = await createGoalWithUser2(false)
|
||||
|
||||
await expectHttpStatus(user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id), 403)
|
||||
})
|
||||
|
||||
it('changing role permissions stays owner-only', async () => {
|
||||
const { goal, role } = await createGoalWithUser2(true)
|
||||
|
||||
// reading is allowed...
|
||||
const matrix = await user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id)
|
||||
expect(matrix).toBeDefined()
|
||||
|
||||
// ...but editing the matrix is not
|
||||
await expectHttpStatus(
|
||||
user2Api.collaboration.toggleRolePermission({ roleId: role.id, permissionId: manageUsersPermissionId }),
|
||||
403,
|
||||
)
|
||||
|
||||
// owner still can edit
|
||||
const ownerToggle = await user1Api.collaboration.toggleRolePermission({
|
||||
roleId: role.id,
|
||||
permissionId: manageUsersPermissionId,
|
||||
})
|
||||
expect(ownerToggle?.add).toBe(false)
|
||||
|
||||
await expect(user1Api.goals.deleteGoal(goal.id)).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -308,6 +308,127 @@ describe('Collaboration', () => {
|
||||
|
||||
});
|
||||
|
||||
// Regression for #98, the reporter's exact path: an org admin who is a collaborator
|
||||
// in several projects created a new project in that org and lost every role he had.
|
||||
// The trigger is creating a goal in an organization owned by SOMEONE ELSE - only then
|
||||
// does the API add the creator as a collaborator, which used to wipe his other roles.
|
||||
it('an org member creating a project in that org keeps his roles in other projects', async () => {
|
||||
const { $tvApiForSecondUser, user2Email } = await initApi();
|
||||
|
||||
const org = await $api.organizations.create({ name: `Roles org ${Date.now()}` });
|
||||
expect(org?.id).toBeDefined();
|
||||
|
||||
// the reporter's case: the second user is an ADMIN of the organization
|
||||
await $api.organizations.addMember({
|
||||
organizationId: org.id,
|
||||
email: user2Email,
|
||||
role: 'admin',
|
||||
});
|
||||
|
||||
// owner creates two projects in the org and gives the admin roles in both
|
||||
const projectA = await $api.goals.createGoal({ name: 'Org project A', organizationId: org.id });
|
||||
const projectB = await $api.goals.createGoal({ name: 'Org project B', organizationId: org.id });
|
||||
expect(projectA?.id).toBeDefined();
|
||||
expect(projectB?.id).toBeDefined();
|
||||
|
||||
const collabA = await $api.collaboration.inviteUserToGoal({ goalId: projectA!.id, email: user2Email });
|
||||
const collabB = await $api.collaboration.inviteUserToGoal({ goalId: projectB!.id, email: user2Email });
|
||||
expect(collabA?.id).toBeDefined();
|
||||
expect(collabB?.id).toEqual(collabA?.id);
|
||||
|
||||
const editorA = (await $api.collaboration.fetchRolesForGoal(projectA!.id))?.find((r) => r.name === 'editor');
|
||||
const editorB = (await $api.collaboration.fetchRolesForGoal(projectB!.id))?.find((r) => r.name === 'editor');
|
||||
expect(editorA?.id).toBeDefined();
|
||||
expect(editorB?.id).toBeDefined();
|
||||
|
||||
await $api.collaboration.toggleUserRoles({
|
||||
goalId: projectA!.id,
|
||||
userId: collabA!.id,
|
||||
roles: [editorA!.id],
|
||||
});
|
||||
await $api.collaboration.toggleUserRoles({
|
||||
goalId: projectB!.id,
|
||||
userId: collabA!.id,
|
||||
roles: [editorB!.id],
|
||||
});
|
||||
|
||||
// ...the admin now creates his own project INSIDE the owner's organization
|
||||
const ownProject = await $tvApiForSecondUser.goals.createGoal({
|
||||
name: 'Project created by the org admin',
|
||||
organizationId: org.id,
|
||||
});
|
||||
expect(ownProject?.id).toBeDefined();
|
||||
|
||||
// roles in the pre-existing projects must survive
|
||||
const usersA = await $api.collaboration.fetchUsersForGoal(projectA!.id);
|
||||
const usersB = await $api.collaboration.fetchUsersForGoal(projectB!.id);
|
||||
expect(usersA?.find((u) => u.email === user2Email)?.roles).toEqual([editorA!.id]);
|
||||
expect(usersB?.find((u) => u.email === user2Email)?.roles).toEqual([editorB!.id]);
|
||||
|
||||
// and the new project is still usable by its creator
|
||||
const ownGoals = await $tvApiForSecondUser.goals.fetchGoals(org.id);
|
||||
expect(ownGoals?.some((g) => g.id === ownProject!.id)).toBe(true);
|
||||
|
||||
await $api.organizations.delete(org.id).catch(() => { });
|
||||
});
|
||||
|
||||
// Regression for #98: toggling roles in one goal wiped the user's roles in every other goal
|
||||
it('toggling roles in one goal must not touch the same user roles in another goal', async () => {
|
||||
const email = `multi-goal-${Date.now()}@fff.com`;
|
||||
|
||||
const goalA = await $api.goals.createGoal({ name: 'Roles isolation goal A' });
|
||||
const goalB = await $api.goals.createGoal({ name: 'Roles isolation goal B' });
|
||||
expect(goalA).toBeTruthy();
|
||||
expect(goalB).toBeTruthy();
|
||||
|
||||
const userInA = await $api.collaboration.inviteUserToGoal({ goalId: goalA!.id, email });
|
||||
const userInB = await $api.collaboration.inviteUserToGoal({ goalId: goalB!.id, email });
|
||||
expect(userInA?.id).toBeDefined();
|
||||
// the same collaboration user is shared between goals
|
||||
expect(userInB?.id).toEqual(userInA?.id);
|
||||
|
||||
const rolesA = await $api.collaboration.fetchRolesForGoal(goalA!.id);
|
||||
const rolesB = await $api.collaboration.fetchRolesForGoal(goalB!.id);
|
||||
const editorA = rolesA?.find((r) => r.name === 'editor');
|
||||
const editorB = rolesB?.find((r) => r.name === 'editor');
|
||||
expect(editorA?.id).toBeDefined();
|
||||
expect(editorB?.id).toBeDefined();
|
||||
|
||||
// assign a role in goal B first
|
||||
const toggledB = await $api.collaboration.toggleUserRoles({
|
||||
goalId: goalB!.id,
|
||||
userId: userInA?.id!,
|
||||
roles: [editorB?.id!],
|
||||
});
|
||||
expect(toggledB).toEqual([editorB?.id!]);
|
||||
|
||||
// toggling roles in goal A must not clear the role in goal B
|
||||
const toggledA = await $api.collaboration.toggleUserRoles({
|
||||
goalId: goalA!.id,
|
||||
userId: userInA?.id!,
|
||||
roles: [editorA?.id!],
|
||||
});
|
||||
expect(toggledA).toEqual([editorA?.id!]);
|
||||
|
||||
const usersInB = await $api.collaboration.fetchUsersForGoal(goalB!.id);
|
||||
expect(usersInB?.find((u) => u.email === email)?.roles).toEqual([editorB?.id!]);
|
||||
|
||||
// each goal's collaborator list shows only that goal's roles
|
||||
const usersInA = await $api.collaboration.fetchUsersForGoal(goalA!.id);
|
||||
expect(usersInA?.find((u) => u.email === email)?.roles).toEqual([editorA?.id!]);
|
||||
|
||||
// a role id belonging to another goal must not be assignable through this goal
|
||||
const toggledForeign = await $api.collaboration.toggleUserRoles({
|
||||
goalId: goalA!.id,
|
||||
userId: userInA?.id!,
|
||||
roles: [editorB?.id!],
|
||||
}).catch(() => null);
|
||||
expect(toggledForeign ?? []).toEqual([]);
|
||||
|
||||
const usersInB2 = await $api.collaboration.fetchUsersForGoal(goalB!.id);
|
||||
expect(usersInB2?.find((u) => u.email === email)?.roles).toEqual([editorB?.id!]);
|
||||
});
|
||||
|
||||
it('should handle inviting already existing collaborator', async () => {
|
||||
const addResult1 = await $api.collaboration.inviteUserToGoal({
|
||||
goalId: collaborationGoal?.id!,
|
||||
|
||||
Reference in New Issue
Block a user