mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									44521c1c74
								
							
						
					
					
						commit
						34c1da58cc
					
				@ -4,10 +4,53 @@ import { type IUser, RoleName, type IGroup } from '../../types';
 | 
			
		||||
import { randomId } from '../../util';
 | 
			
		||||
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', () => {
 | 
			
		||||
    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 () => {
 | 
			
		||||
        // 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 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);
 | 
			
		||||
@ -25,11 +70,17 @@ beforeAll(async () => {
 | 
			
		||||
    ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
 | 
			
		||||
 | 
			
		||||
    const ownerData = {
 | 
			
		||||
        name: 'Owner User',
 | 
			
		||||
        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',
 | 
			
		||||
@ -40,10 +91,13 @@ beforeAll(async () => {
 | 
			
		||||
    // 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 () => {
 | 
			
		||||
@ -64,6 +118,12 @@ afterEach(async () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 () => {
 | 
			
		||||
        const projectId = randomId();
 | 
			
		||||
        await db.stores.projectStore.create({ id: projectId, name: projectId });
 | 
			
		||||
@ -77,7 +137,7 @@ describe('integration tests', () => {
 | 
			
		||||
        const owners = await readModel.getAllProjectOwners();
 | 
			
		||||
        expect(owners).toMatchObject({
 | 
			
		||||
            [projectId]: expect.arrayContaining([
 | 
			
		||||
                expect.objectContaining({ name: 'Owner User' }),
 | 
			
		||||
                expect.objectContaining({ name: 'Owner Name' }),
 | 
			
		||||
            ]),
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@ -98,7 +158,7 @@ describe('integration tests', () => {
 | 
			
		||||
            [projectId]: [
 | 
			
		||||
                {
 | 
			
		||||
                    ownerType: 'user',
 | 
			
		||||
                    name: 'Owner User',
 | 
			
		||||
                    name: 'Owner Name',
 | 
			
		||||
                    email: 'owner@email.com',
 | 
			
		||||
                    imageUrl: 'image-url-1',
 | 
			
		||||
                },
 | 
			
		||||
@ -128,7 +188,7 @@ describe('integration tests', () => {
 | 
			
		||||
        const owners = await readModel.getAllProjectOwners();
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
        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 () => {
 | 
			
		||||
        const owners = await readModel.enrichWithOwners([]);
 | 
			
		||||
    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 });
 | 
			
		||||
 | 
			
		||||
        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' }] },
 | 
			
		||||
        ]);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -38,15 +38,14 @@ export class ProjectOwnersReadModel {
 | 
			
		||||
        this.roleStore = roleStore;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addOwnerData(
 | 
			
		||||
    static addOwnerData(
 | 
			
		||||
        projects: IProjectWithCount[],
 | 
			
		||||
        owners: ProjectOwnersDictionary,
 | 
			
		||||
    ): IProjectWithCountAndOwners[] {
 | 
			
		||||
        // const projectsWithOwners = projects.map((p) => ({
 | 
			
		||||
        //     ...p,
 | 
			
		||||
        //     owners: projectOwners[p.id] || [],
 | 
			
		||||
        // }));
 | 
			
		||||
        return [];
 | 
			
		||||
        return projects.map((project) => ({
 | 
			
		||||
            ...project,
 | 
			
		||||
            owners: owners[project.name] || [{ ownerType: 'system' }],
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async getAllProjectUsersByRole(
 | 
			
		||||
@ -62,6 +61,7 @@ export class ProjectOwnersReadModel {
 | 
			
		||||
                'ru.project',
 | 
			
		||||
            )
 | 
			
		||||
            .from(`${T.ROLE_USER} as ru`)
 | 
			
		||||
            .orderBy('ru.created_at', 'asc')
 | 
			
		||||
            .join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
 | 
			
		||||
            .where('r.id', roleId)
 | 
			
		||||
            .join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
 | 
			
		||||
@ -93,6 +93,7 @@ export class ProjectOwnersReadModel {
 | 
			
		||||
        const groupsResult = await this.db
 | 
			
		||||
            .select('groups.name', 'gr.created_at', 'gr.project')
 | 
			
		||||
            .from(`${T.GROUP_ROLE} as gr`)
 | 
			
		||||
            .orderBy('gr.created_at', 'asc')
 | 
			
		||||
            .join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
 | 
			
		||||
            .where('r.id', roleId)
 | 
			
		||||
            .join('groups', 'gr.group_id', 'groups.id');
 | 
			
		||||
@ -122,30 +123,27 @@ export class ProjectOwnersReadModel {
 | 
			
		||||
        const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
 | 
			
		||||
        const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
 | 
			
		||||
 | 
			
		||||
        const projects = [
 | 
			
		||||
            ...new Set([...Object.keys(usersDict), ...Object.keys(groupsDict)]),
 | 
			
		||||
        ];
 | 
			
		||||
        const dict: Record<
 | 
			
		||||
            string,
 | 
			
		||||
            Array<UserProjectOwner | GroupProjectOwner>
 | 
			
		||||
        > = usersDict;
 | 
			
		||||
 | 
			
		||||
        const dict = Object.fromEntries(
 | 
			
		||||
            projects.map((project) => {
 | 
			
		||||
                return [
 | 
			
		||||
                    project,
 | 
			
		||||
                    [
 | 
			
		||||
                        ...(usersDict[project] || []),
 | 
			
		||||
                        ...(groupsDict[project] || []),
 | 
			
		||||
                    ],
 | 
			
		||||
                ];
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
        Object.keys(groupsDict).forEach((project) => {
 | 
			
		||||
            if (project in dict) {
 | 
			
		||||
                dict[project] = dict[project].concat(groupsDict[project]);
 | 
			
		||||
            } else {
 | 
			
		||||
                dict[project] = groupsDict[project];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return dict;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async enrichWithOwners(
 | 
			
		||||
    async addOwners(
 | 
			
		||||
        projects: IProjectWithCount[],
 | 
			
		||||
    ): Promise<IProjectWithCountAndOwners[]> {
 | 
			
		||||
        const owners = await this.getAllProjectOwners();
 | 
			
		||||
 | 
			
		||||
        return this.addOwnerData(projects, owners);
 | 
			
		||||
        return ProjectOwnersReadModel.addOwnerData(projects, owners);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user