1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: map project owners to projects list (#6928)

- Combining list of projects with owners
- Additional tests and checks
This commit is contained in:
Tymoteusz Czech 2024-04-25 11:26:39 +02:00 committed by GitHub
parent 44521c1c74
commit 34c1da58cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 207 additions and 36 deletions

View File

@ -4,10 +4,53 @@ import { type IUser, RoleName, type IGroup } from '../../types';
import { randomId } from '../../util'; import { randomId } from '../../util';
import { ProjectOwnersReadModel } from './project-owners-read-model'; import { ProjectOwnersReadModel } from './project-owners-read-model';
const mockProjectWithCounts = (name: string) => ({
name,
id: name,
description: '',
featureCount: 0,
memberCount: 0,
mode: 'open' as const,
defaultStickiness: 'default' as const,
staleFeatureCount: 0,
potentiallyStaleFeatureCount: 0,
avgTimeToProduction: 0,
});
describe('unit tests', () => { describe('unit tests', () => {
test('maps owners to projects', () => {}); test('maps owners to projects', () => {
const projects = [{ name: 'project1' }, { name: 'project2' }] 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([
{ name: 'project1', owners: [{ name: 'Owner Name' }] },
{ name: 'project2', owners: [{ name: 'Owner Name' }] },
]);
});
test('returns "system" when a project has no owners', async () => { test('returns "system" when a project has no owners', async () => {
// this is a mapping test; not an integration test const projects = [{ name: 'project1' }, { name: 'project2' }] as any;
const owners = {};
const projectsWithOwners = ProjectOwnersReadModel.addOwnerData(
projects,
owners,
);
expect(projectsWithOwners).toMatchObject([
{ name: 'project1', owners: [{ ownerType: 'system' }] },
{ name: 'project2', owners: [{ ownerType: 'system' }] },
]);
}); });
}); });
@ -16,8 +59,10 @@ let readModel: ProjectOwnersReadModel;
let ownerRoleId: number; let ownerRoleId: number;
let owner: IUser; let owner: IUser;
let owner2: IUser;
let member: IUser; let member: IUser;
let group: IGroup; let group: IGroup;
let group2: IGroup;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('project_owners_read_model_serial', getLogger); db = await dbInit('project_owners_read_model_serial', getLogger);
@ -25,11 +70,17 @@ beforeAll(async () => {
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id; ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
const ownerData = { const ownerData = {
name: 'Owner User', name: 'Owner Name',
username: 'owner', username: 'owner',
email: 'owner@email.com', email: 'owner@email.com',
imageUrl: 'image-url-1', imageUrl: 'image-url-1',
}; };
const ownerData2 = {
name: 'Second Owner Name',
username: 'owner2',
email: 'owner2@email.com',
imageUrl: 'image-url-3',
};
const memberData = { const memberData = {
name: 'Member Name', name: 'Member Name',
username: 'member', username: 'member',
@ -40,10 +91,13 @@ beforeAll(async () => {
// create users // create users
owner = await db.stores.userStore.insert(ownerData); owner = await db.stores.userStore.insert(ownerData);
member = await db.stores.userStore.insert(memberData); member = await db.stores.userStore.insert(memberData);
owner2 = await db.stores.userStore.insert(ownerData2);
// create groups // create groups
group = await db.stores.groupStore.create({ name: 'Group Name' }); group = await db.stores.groupStore.create({ name: 'Group Name' });
await db.stores.groupStore.addUserToGroups(owner.id, [group.id]); 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 () => { afterAll(async () => {
@ -64,6 +118,12 @@ afterEach(async () => {
}); });
describe('integration tests', () => { describe('integration tests', () => {
test('returns an empty object if there are no projects', async () => {
const owners = await readModel.getAllProjectOwners();
expect(owners).toStrictEqual({});
});
test('name takes precedence over username', async () => { test('name takes precedence over username', async () => {
const projectId = randomId(); const projectId = randomId();
await db.stores.projectStore.create({ id: projectId, name: projectId }); await db.stores.projectStore.create({ id: projectId, name: projectId });
@ -77,7 +137,7 @@ describe('integration tests', () => {
const owners = await readModel.getAllProjectOwners(); const owners = await readModel.getAllProjectOwners();
expect(owners).toMatchObject({ expect(owners).toMatchObject({
[projectId]: expect.arrayContaining([ [projectId]: expect.arrayContaining([
expect.objectContaining({ name: 'Owner User' }), expect.objectContaining({ name: 'Owner Name' }),
]), ]),
}); });
}); });
@ -98,7 +158,7 @@ describe('integration tests', () => {
[projectId]: [ [projectId]: [
{ {
ownerType: 'user', ownerType: 'user',
name: 'Owner User', name: 'Owner Name',
email: 'owner@email.com', email: 'owner@email.com',
imageUrl: 'image-url-1', imageUrl: 'image-url-1',
}, },
@ -128,7 +188,7 @@ describe('integration tests', () => {
const owners = await readModel.getAllProjectOwners(); const owners = await readModel.getAllProjectOwners();
expect(owners).toMatchObject({ expect(owners).toMatchObject({
[projectId]: [{ name: 'Owner User' }], [projectId]: [{ name: 'Owner Name' }],
}); });
}); });
@ -155,21 +215,134 @@ describe('integration tests', () => {
}); });
}); });
test('users are listed before groups', async () => {}); test('users are listed before groups', async () => {
const projectId = randomId();
await db.stores.projectStore.create({ id: projectId, name: projectId });
test('owners (users and groups) are sorted by when they were added; oldest first', async () => {}); await db.stores.accessStore.addGroupToRole(
group.id,
ownerRoleId,
'',
projectId,
);
test('returns the system owner for the default project', async () => {}); await db.stores.accessStore.addUserToRole(
owner.id,
ownerRoleId,
projectId,
);
test('returns an empty list if there are no projects', async () => {
const owners = await readModel.getAllProjectOwners(); const owners = await readModel.getAllProjectOwners();
expect(owners).toStrictEqual({}); expect(owners).toMatchObject({
[projectId]: [
{
email: 'owner@email.com',
imageUrl: 'image-url-1',
name: 'Owner Name',
ownerType: 'user',
},
{
name: 'Group Name',
ownerType: 'group',
},
],
});
}); });
test('enriches fully', async () => { test('owners (users and groups) are sorted by when they were added; oldest first', async () => {
const owners = await readModel.enrichWithOwners([]); const projectId = randomId();
await db.stores.projectStore.create({ id: projectId, name: projectId });
expect(owners).toStrictEqual([]); // 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.getAllProjectOwners();
expect(owners).toMatchObject({
[projectId]: [
{
email: 'owner2@email.com',
imageUrl: 'image-url-3',
name: 'Second Owner Name',
ownerType: 'user',
},
{
email: 'owner@email.com',
imageUrl: '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([
mockProjectWithCounts(projectIdA),
mockProjectWithCounts(projectIdB),
]);
expect(projectsWithOwners).toMatchObject([
{ name: projectIdA, owners: [{ ownerType: 'system' }] },
{ name: projectIdB, owners: [{ ownerType: 'user' }] },
]);
}); });
}); });

View File

@ -38,15 +38,14 @@ export class ProjectOwnersReadModel {
this.roleStore = roleStore; this.roleStore = roleStore;
} }
addOwnerData( static addOwnerData(
projects: IProjectWithCount[], projects: IProjectWithCount[],
owners: ProjectOwnersDictionary, owners: ProjectOwnersDictionary,
): IProjectWithCountAndOwners[] { ): IProjectWithCountAndOwners[] {
// const projectsWithOwners = projects.map((p) => ({ return projects.map((project) => ({
// ...p, ...project,
// owners: projectOwners[p.id] || [], owners: owners[project.name] || [{ ownerType: 'system' }],
// })); }));
return [];
} }
private async getAllProjectUsersByRole( private async getAllProjectUsersByRole(
@ -62,6 +61,7 @@ export class ProjectOwnersReadModel {
'ru.project', 'ru.project',
) )
.from(`${T.ROLE_USER} as ru`) .from(`${T.ROLE_USER} as ru`)
.orderBy('ru.created_at', 'asc')
.join(`${T.ROLES} as r`, 'ru.role_id', 'r.id') .join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
.where('r.id', roleId) .where('r.id', roleId)
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id'); .join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
@ -93,6 +93,7 @@ export class ProjectOwnersReadModel {
const groupsResult = await this.db const groupsResult = await this.db
.select('groups.name', 'gr.created_at', 'gr.project') .select('groups.name', 'gr.created_at', 'gr.project')
.from(`${T.GROUP_ROLE} as gr`) .from(`${T.GROUP_ROLE} as gr`)
.orderBy('gr.created_at', 'asc')
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id') .join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
.where('r.id', roleId) .where('r.id', roleId)
.join('groups', 'gr.group_id', 'groups.id'); .join('groups', 'gr.group_id', 'groups.id');
@ -122,30 +123,27 @@ export class ProjectOwnersReadModel {
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id); const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id); const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
const projects = [ const dict: Record<
...new Set([...Object.keys(usersDict), ...Object.keys(groupsDict)]), string,
]; Array<UserProjectOwner | GroupProjectOwner>
> = usersDict;
const dict = Object.fromEntries( Object.keys(groupsDict).forEach((project) => {
projects.map((project) => { if (project in dict) {
return [ dict[project] = dict[project].concat(groupsDict[project]);
project, } else {
[ dict[project] = groupsDict[project];
...(usersDict[project] || []), }
...(groupsDict[project] || []), });
],
];
}),
);
return dict; return dict;
} }
async enrichWithOwners( async addOwners(
projects: IProjectWithCount[], projects: IProjectWithCount[],
): Promise<IProjectWithCountAndOwners[]> { ): Promise<IProjectWithCountAndOwners[]> {
const owners = await this.getAllProjectOwners(); const owners = await this.getAllProjectOwners();
return this.addOwnerData(projects, owners); return ProjectOwnersReadModel.addOwnerData(projects, owners);
} }
} }