mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
This PR adds all user-type owners of projects that you have access to to the personal dashboard payload. It adds the new `projectOwners` property regardless of whether you have access to any projects or not because it required less code and fewer conditionals, but we can do the filtering if we want to. To add the owners, it uses the private project checker to get accessible projects before passing those to the project owner read model, which has a new method to fetch user owners for projects.
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
|
import getLogger from '../../../test/fixtures/no-logger';
|
|
import { type IUser, RoleName, type IGroup } from '../../types';
|
|
import { randomId } from '../../util';
|
|
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
|
import type { ProjectForUi } from './project-read-model-type';
|
|
|
|
jest.mock('../../util', () => ({
|
|
...jest.requireActual('../../util'),
|
|
generateImageUrl: jest.fn((input) => `https://${input.image_url}`),
|
|
}));
|
|
|
|
const mockProjectData = (name: string): ProjectForUi => ({
|
|
name,
|
|
id: name,
|
|
featureCount: 0,
|
|
memberCount: 0,
|
|
mode: 'open' as const,
|
|
health: 100,
|
|
createdAt: new Date(),
|
|
favorite: false,
|
|
lastReportedFlagUsage: null,
|
|
lastUpdatedAt: null,
|
|
});
|
|
|
|
describe('unit tests', () => {
|
|
test('maps owners to projects', () => {
|
|
const projects = [
|
|
{ id: 'project1', name: 'Project one' },
|
|
{ id: 'project2', name: 'Project two' },
|
|
] as any;
|
|
|
|
const owners = {
|
|
project1: [{ ownerType: 'user' as const, name: 'Owner Name' }],
|
|
project2: [{ ownerType: 'user' as const, name: 'Owner Name' }],
|
|
};
|
|
|
|
const projectsWithOwners = ProjectOwnersReadModel.addOwnerData(
|
|
projects,
|
|
owners,
|
|
);
|
|
|
|
expect(projectsWithOwners).toMatchObject([
|
|
{
|
|
id: 'project1',
|
|
name: 'Project one',
|
|
owners: [{ name: 'Owner Name' }],
|
|
},
|
|
{
|
|
id: 'project2',
|
|
name: 'Project two',
|
|
owners: [{ name: 'Owner Name' }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('returns "system" when a project has no owners', async () => {
|
|
const projects = [{ id: 'project1' }, { id: 'project2' }] as any;
|
|
|
|
const owners = {};
|
|
|
|
const projectsWithOwners = ProjectOwnersReadModel.addOwnerData(
|
|
projects,
|
|
owners,
|
|
);
|
|
|
|
expect(projectsWithOwners).toMatchObject([
|
|
{
|
|
id: 'project1',
|
|
owners: [{ ownerType: 'system' }],
|
|
},
|
|
{
|
|
id: 'project2',
|
|
owners: [{ ownerType: 'system' }],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
let db: ITestDb;
|
|
let readModel: ProjectOwnersReadModel;
|
|
|
|
let ownerRoleId: number;
|
|
let owner: IUser;
|
|
let owner2: IUser;
|
|
let member: IUser;
|
|
let group: IGroup;
|
|
let group2: IGroup;
|
|
|
|
beforeAll(async () => {
|
|
db = await dbInit('project_owners_read_model_serial', getLogger);
|
|
readModel = new ProjectOwnersReadModel(db.rawDatabase);
|
|
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
|
|
|
|
const ownerData = {
|
|
name: 'Owner Name',
|
|
username: 'owner',
|
|
email: 'owner@email.com',
|
|
imageUrl: 'image-url-1',
|
|
};
|
|
const ownerData2 = {
|
|
name: 'Second Owner Name',
|
|
username: 'owner2',
|
|
email: 'owner2@email.com',
|
|
imageUrl: 'image-url-3',
|
|
};
|
|
const memberData = {
|
|
name: 'Member Name',
|
|
username: 'member',
|
|
email: 'member@email.com',
|
|
imageUrl: 'image-url-2',
|
|
};
|
|
|
|
// create users
|
|
owner = await db.stores.userStore.insert(ownerData);
|
|
member = await db.stores.userStore.insert(memberData);
|
|
owner2 = await db.stores.userStore.insert(ownerData2);
|
|
|
|
// create groups
|
|
group = await db.stores.groupStore.create({ name: 'Group Name' });
|
|
await db.stores.groupStore.addUserToGroups(owner.id, [group.id]);
|
|
group2 = await db.stores.groupStore.create({ name: 'Second Group Name' });
|
|
await db.stores.groupStore.addUserToGroups(member.id, [group.id]);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (db) {
|
|
await db.destroy();
|
|
}
|
|
});
|
|
|
|
afterEach(async () => {
|
|
db.stores.roleStore;
|
|
});
|
|
|
|
describe('integration tests', () => {
|
|
test('returns an empty object if there are no projects', async () => {
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toStrictEqual({});
|
|
});
|
|
|
|
test('name takes precedence over username', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
expect(owners).toMatchObject({
|
|
[projectId]: expect.arrayContaining([
|
|
expect.objectContaining({ name: 'Owner Name' }),
|
|
]),
|
|
});
|
|
});
|
|
|
|
test('gets project user owners', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toMatchObject({
|
|
[projectId]: [
|
|
{
|
|
ownerType: 'user',
|
|
name: 'Owner Name',
|
|
email: 'owner@email.com',
|
|
imageUrl: 'https://image-url-1',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('does not get regular project members', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
const memberRole = await db.stores.roleStore.getRoleByName(
|
|
RoleName.MEMBER,
|
|
);
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectId,
|
|
);
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
member.id,
|
|
memberRole.id,
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toMatchObject({
|
|
[projectId]: [{ name: 'Owner Name' }],
|
|
});
|
|
});
|
|
|
|
test('gets project group owners', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
await db.stores.accessStore.addGroupToRole(
|
|
group.id,
|
|
ownerRoleId,
|
|
'',
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toMatchObject({
|
|
[projectId]: [
|
|
{
|
|
ownerType: 'group',
|
|
name: 'Group Name',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('users are listed before groups', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
await db.stores.accessStore.addGroupToRole(
|
|
group.id,
|
|
ownerRoleId,
|
|
'',
|
|
projectId,
|
|
);
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toMatchObject({
|
|
[projectId]: [
|
|
{
|
|
email: 'owner@email.com',
|
|
imageUrl: 'https://image-url-1',
|
|
name: 'Owner Name',
|
|
ownerType: 'user',
|
|
},
|
|
{
|
|
name: 'Group Name',
|
|
ownerType: 'group',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('owners (users and groups) are sorted by when they were added; oldest first', async () => {
|
|
const projectId = randomId();
|
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
|
|
|
// Raw query in order to set the created_at date
|
|
await db.rawDatabase('role_user').insert({
|
|
user_id: owner2.id,
|
|
role_id: ownerRoleId,
|
|
project: projectId,
|
|
created_at: new Date('2024-01-01T00:00:00.000Z'),
|
|
});
|
|
|
|
// Raw query in order to set the created_at date
|
|
await db.rawDatabase('group_role').insert({
|
|
group_id: group2.id,
|
|
role_id: ownerRoleId,
|
|
project: projectId,
|
|
created_at: new Date('2024-01-01T00:00:00.000Z'),
|
|
});
|
|
|
|
await db.stores.accessStore.addGroupToRole(
|
|
group.id,
|
|
ownerRoleId,
|
|
'',
|
|
projectId,
|
|
);
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectId,
|
|
);
|
|
|
|
const owners = await readModel.getProjectOwnersDictionary();
|
|
|
|
expect(owners).toMatchObject({
|
|
[projectId]: [
|
|
{
|
|
email: 'owner2@email.com',
|
|
imageUrl: 'https://image-url-3',
|
|
name: 'Second Owner Name',
|
|
ownerType: 'user',
|
|
},
|
|
{
|
|
email: 'owner@email.com',
|
|
imageUrl: 'https://image-url-1',
|
|
name: 'Owner Name',
|
|
ownerType: 'user',
|
|
},
|
|
{
|
|
name: 'Second Group Name',
|
|
ownerType: 'group',
|
|
},
|
|
{
|
|
name: 'Group Name',
|
|
ownerType: 'group',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('does not modify an empty array', async () => {
|
|
const projectsWithOwners = await readModel.addOwners([]);
|
|
|
|
expect(projectsWithOwners).toStrictEqual([]);
|
|
});
|
|
|
|
test('adds system owner when no owners are found', async () => {
|
|
const projectIdA = randomId();
|
|
const projectIdB = randomId();
|
|
await db.stores.projectStore.create({
|
|
id: projectIdA,
|
|
name: projectIdA,
|
|
});
|
|
await db.stores.projectStore.create({
|
|
id: projectIdB,
|
|
name: projectIdB,
|
|
});
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectIdB,
|
|
);
|
|
|
|
const projectsWithOwners = await readModel.addOwners([
|
|
mockProjectData(projectIdA),
|
|
mockProjectData(projectIdB),
|
|
]);
|
|
|
|
expect(projectsWithOwners).toMatchObject([
|
|
{ name: projectIdA, owners: [{ ownerType: 'system' }] },
|
|
{ name: projectIdB, owners: [{ ownerType: 'user' }] },
|
|
]);
|
|
});
|
|
|
|
test('filters out system and group owners when getting all user project owners', async () => {
|
|
const createProject = async () => {
|
|
const id = randomId();
|
|
return db.stores.projectStore.create({
|
|
id,
|
|
name: id,
|
|
});
|
|
};
|
|
|
|
const projectA = await createProject();
|
|
const projectB = await createProject();
|
|
const projectC = await createProject();
|
|
await createProject(); // <- no owner
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectA.id,
|
|
);
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner2.id,
|
|
ownerRoleId,
|
|
projectB.id,
|
|
);
|
|
|
|
await db.stores.accessStore.addGroupToRole(
|
|
group.id,
|
|
ownerRoleId,
|
|
'',
|
|
projectC.id,
|
|
);
|
|
|
|
const userOwners = await readModel.getAllUserProjectOwners();
|
|
userOwners.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
expect(userOwners).toMatchObject([
|
|
{
|
|
name: owner.name,
|
|
ownerType: 'user',
|
|
email: owner.email,
|
|
imageUrl: 'https://image-url-1',
|
|
},
|
|
{
|
|
name: owner2.name,
|
|
ownerType: 'user',
|
|
email: owner2.email,
|
|
imageUrl: 'https://image-url-3',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('only returns projects listed in the projects input if provided', async () => {
|
|
const createProject = async () => {
|
|
const id = randomId();
|
|
return db.stores.projectStore.create({
|
|
id,
|
|
name: id,
|
|
});
|
|
};
|
|
|
|
const projectA = await createProject();
|
|
const projectB = await createProject();
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner.id,
|
|
ownerRoleId,
|
|
projectA.id,
|
|
);
|
|
|
|
await db.stores.accessStore.addUserToRole(
|
|
owner2.id,
|
|
ownerRoleId,
|
|
projectB.id,
|
|
);
|
|
|
|
const noOwners = await readModel.getAllUserProjectOwners(new Set());
|
|
expect(noOwners).toMatchObject([]);
|
|
|
|
const onlyProjectA = await readModel.getAllUserProjectOwners(
|
|
new Set([projectA.id]),
|
|
);
|
|
expect(onlyProjectA).toMatchObject([
|
|
{
|
|
name: owner.name,
|
|
ownerType: 'user',
|
|
email: owner.email,
|
|
imageUrl: 'https://image-url-1',
|
|
},
|
|
]);
|
|
});
|
|
});
|