mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
feat: ensure at least one owner on remove user/group access (#5085)
## About the changes This makes sure that projects have at least one owner, either a group or a user. This is to prevent accidentally losing access to a project. We check this when removing a user/group or when changing the role of a user/group **Note**: We can still leave a group empty as the only owner of the project, but that's okay because we can still add more users to the group
This commit is contained in:
parent
6760fc0723
commit
3d9f31f839
@ -14,7 +14,6 @@ import FakeGroupStore from '../../../test/fixtures/fake-group-store';
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||
import ProjectStore from '../../db/project-store';
|
||||
import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
|
||||
import FeatureTypeStore from '../../db/feature-type-store';
|
||||
import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
|
||||
import ProjectStatsStore from '../../db/project-stats-store';
|
||||
import {
|
||||
@ -29,7 +28,6 @@ import { FavoriteFeaturesStore } from '../../db/favorite-features-store';
|
||||
import { FavoriteProjectsStore } from '../../db/favorite-projects-store';
|
||||
import FakeProjectStore from '../../../test/fixtures/fake-project-store';
|
||||
import FakeFeatureToggleStore from '../feature-toggle/fakes/fake-feature-toggle-store';
|
||||
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||
import FakeEnvironmentStore from '../../../test/fixtures/fake-environment-store';
|
||||
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
|
||||
import FakeProjectStatsStore from '../../../test/fixtures/fake-project-stats-store';
|
||||
@ -41,8 +39,6 @@ import {
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
||||
import { LastSeenAtReadModel } from '../../services/client-metrics/last-seen/last-seen-read-model';
|
||||
import { FakeLastSeenReadModel } from '../../services/client-metrics/last-seen/fake-last-seen-read-model';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -63,7 +59,6 @@ export const createProjectService = (
|
||||
getLogger,
|
||||
flagResolver,
|
||||
);
|
||||
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
||||
const accountStore = new AccountStore(db, getLogger);
|
||||
const environmentStore = new EnvironmentStore(db, eventBus, getLogger);
|
||||
const featureEnvironmentStore = new FeatureEnvironmentStore(
|
||||
@ -106,14 +101,12 @@ export const createProjectService = (
|
||||
);
|
||||
|
||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||
const lastSeenReadModel = new LastSeenAtReadModel(db);
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
eventStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
accountStore,
|
||||
@ -126,7 +119,6 @@ export const createProjectService = (
|
||||
favoriteService,
|
||||
eventService,
|
||||
privateProjectChecker,
|
||||
lastSeenReadModel,
|
||||
);
|
||||
};
|
||||
|
||||
@ -138,7 +130,6 @@ export const createFakeProjectService = (
|
||||
const projectStore = new FakeProjectStore();
|
||||
const groupStore = new FakeGroupStore();
|
||||
const featureToggleStore = new FakeFeatureToggleStore();
|
||||
const featureTypeStore = new FakeFeatureTypeStore();
|
||||
const accountStore = new FakeAccountStore();
|
||||
const environmentStore = new FakeEnvironmentStore();
|
||||
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
||||
@ -169,14 +160,12 @@ export const createFakeProjectService = (
|
||||
);
|
||||
|
||||
const privateProjectChecker = createFakePrivateProjectChecker();
|
||||
const fakeLastSeenReadModel = new FakeLastSeenReadModel();
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
eventStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
accountStore,
|
||||
@ -189,6 +178,5 @@ export const createFakeProjectService = (
|
||||
favoriteService,
|
||||
eventService,
|
||||
privateProjectChecker,
|
||||
fakeLastSeenReadModel,
|
||||
);
|
||||
};
|
||||
|
@ -568,7 +568,7 @@ export class AccessService {
|
||||
}
|
||||
|
||||
async removeDefaultProjectRoles(
|
||||
owner: User,
|
||||
owner: IUser,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
this.logger.info(`Removing project roles for ${projectId}`);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { subDays } from 'date-fns';
|
||||
import { ValidationError } from 'joi';
|
||||
import User, { IUser } from '../types/user';
|
||||
import { IUser } from '../types/user';
|
||||
import { AccessService, AccessWithRoles } from './access-service';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
@ -15,7 +15,6 @@ import {
|
||||
IEventStore,
|
||||
IFeatureEnvironmentStore,
|
||||
IFeatureToggleStore,
|
||||
IFeatureTypeStore,
|
||||
IProject,
|
||||
IProjectOverview,
|
||||
IProjectWithCount,
|
||||
@ -65,8 +64,6 @@ import { ProjectDoraMetricsSchema } from 'lib/openapi';
|
||||
import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import EventService from './event-service';
|
||||
import { ILastSeenReadModel } from './client-metrics/last-seen/types/last-seen-read-model-type';
|
||||
import { LastSeenMapper } from './client-metrics/last-seen/last-seen-mapper';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
@ -89,6 +86,10 @@ interface ICalculateStatus {
|
||||
updates: IProjectStats;
|
||||
}
|
||||
|
||||
function includes(list: number[], { id }: { id: number }): boolean {
|
||||
return list.some((l) => l === id);
|
||||
}
|
||||
|
||||
export default class ProjectService {
|
||||
private projectStore: IProjectStore;
|
||||
|
||||
@ -98,8 +99,6 @@ export default class ProjectService {
|
||||
|
||||
private featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
private featureTypeStore: IFeatureTypeStore;
|
||||
|
||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||
|
||||
private environmentStore: IEnvironmentStore;
|
||||
@ -120,8 +119,6 @@ export default class ProjectService {
|
||||
|
||||
private projectStatsStore: IProjectStatsStore;
|
||||
|
||||
private lastSeenReadModel: ILastSeenReadModel;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private isEnterprise: boolean;
|
||||
@ -131,7 +128,6 @@ export default class ProjectService {
|
||||
projectStore,
|
||||
eventStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
accountStore,
|
||||
@ -141,7 +137,6 @@ export default class ProjectService {
|
||||
| 'projectStore'
|
||||
| 'eventStore'
|
||||
| 'featureToggleStore'
|
||||
| 'featureTypeStore'
|
||||
| 'environmentStore'
|
||||
| 'featureEnvironmentStore'
|
||||
| 'accountStore'
|
||||
@ -154,7 +149,6 @@ export default class ProjectService {
|
||||
favoriteService: FavoritesService,
|
||||
eventService: EventService,
|
||||
privateProjectChecker: IPrivateProjectChecker,
|
||||
lastSeenReadModel: ILastSeenReadModel,
|
||||
) {
|
||||
this.projectStore = projectStore;
|
||||
this.environmentStore = environmentStore;
|
||||
@ -162,7 +156,6 @@ export default class ProjectService {
|
||||
this.accessService = accessService;
|
||||
this.eventStore = eventStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureToggleService = featureToggleService;
|
||||
this.favoritesService = favoriteService;
|
||||
this.privateProjectChecker = privateProjectChecker;
|
||||
@ -170,7 +163,6 @@ export default class ProjectService {
|
||||
this.groupService = groupService;
|
||||
this.eventService = eventService;
|
||||
this.projectStatsStore = projectStatsStore;
|
||||
this.lastSeenReadModel = lastSeenReadModel;
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.isEnterprise = config.isEnterprise;
|
||||
@ -267,7 +259,7 @@ export default class ProjectService {
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateProject(updatedProject: IProject, user: User): Promise<void> {
|
||||
async updateProject(updatedProject: IProject, user: IUser): Promise<void> {
|
||||
const preData = await this.projectStore.get(updatedProject.id);
|
||||
|
||||
await this.projectStore.update(updatedProject);
|
||||
@ -283,7 +275,7 @@ export default class ProjectService {
|
||||
|
||||
async updateProjectEnterpriseSettings(
|
||||
updatedProject: IProjectEnterpriseSettingsUpdate,
|
||||
user: User,
|
||||
user: IUser,
|
||||
): Promise<void> {
|
||||
const preData = await this.projectStore.get(updatedProject.id);
|
||||
|
||||
@ -330,7 +322,7 @@ export default class ProjectService {
|
||||
async changeProject(
|
||||
newProjectId: string,
|
||||
featureName: string,
|
||||
user: User,
|
||||
user: IUser,
|
||||
currentProjectId: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
@ -372,7 +364,7 @@ export default class ProjectService {
|
||||
return updatedFeature;
|
||||
}
|
||||
|
||||
async deleteProject(id: string, user: User): Promise<void> {
|
||||
async deleteProject(id: string, user: IUser): Promise<void> {
|
||||
if (id === DEFAULT_PROJECT) {
|
||||
throw new InvalidOperationError(
|
||||
'You can not delete the default project!',
|
||||
@ -508,6 +500,11 @@ export default class ProjectService {
|
||||
userId,
|
||||
);
|
||||
|
||||
const ownerRole = await this.accessService.getRoleByName(
|
||||
RoleName.OWNER,
|
||||
);
|
||||
await this.validateAtLeastOneOwner(projectId, ownerRole);
|
||||
|
||||
await this.accessService.removeUserAccess(projectId, userId);
|
||||
|
||||
await this.eventService.storeEvent(
|
||||
@ -532,6 +529,11 @@ export default class ProjectService {
|
||||
groupId,
|
||||
);
|
||||
|
||||
const ownerRole = await this.accessService.getRoleByName(
|
||||
RoleName.OWNER,
|
||||
);
|
||||
await this.validateAtLeastOneOwner(projectId, ownerRole);
|
||||
|
||||
await this.accessService.removeGroupAccess(projectId, groupId);
|
||||
|
||||
await this.eventService.storeEvent(
|
||||
@ -598,6 +600,8 @@ export default class ProjectService {
|
||||
undefined,
|
||||
);
|
||||
|
||||
await this.validateAtLeastOneOwner(projectId, role);
|
||||
|
||||
await this.accessService.removeGroupFromRole(
|
||||
group.id,
|
||||
role.id,
|
||||
@ -675,28 +679,39 @@ export default class ProjectService {
|
||||
async setRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
newRoles: number[],
|
||||
createdByUserName: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForUser(
|
||||
const currentRoles = await this.accessService.getProjectRolesForUser(
|
||||
projectId,
|
||||
userId,
|
||||
);
|
||||
|
||||
const ownerRole = await this.accessService.getRoleByName(
|
||||
RoleName.OWNER,
|
||||
);
|
||||
|
||||
const hasOwnerRole = includes(currentRoles, ownerRole);
|
||||
const isRemovingOwnerRole = !includes(newRoles, ownerRole);
|
||||
if (hasOwnerRole && isRemovingOwnerRole) {
|
||||
await this.validateAtLeastOneOwner(projectId, ownerRole);
|
||||
}
|
||||
|
||||
await this.accessService.setProjectRolesForUser(
|
||||
projectId,
|
||||
userId,
|
||||
roles,
|
||||
newRoles,
|
||||
);
|
||||
await this.eventService.storeEvent(
|
||||
new ProjectAccessUserRolesUpdated({
|
||||
project: projectId,
|
||||
createdBy: createdByUserName,
|
||||
data: {
|
||||
roles,
|
||||
roles: newRoles,
|
||||
userId,
|
||||
},
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
roles: currentRoles,
|
||||
userId,
|
||||
},
|
||||
}),
|
||||
@ -706,17 +721,28 @@ export default class ProjectService {
|
||||
async setRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
newRoles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForGroup(
|
||||
const currentRoles = await this.accessService.getProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
);
|
||||
|
||||
const ownerRole = await this.accessService.getRoleByName(
|
||||
RoleName.OWNER,
|
||||
);
|
||||
const hasOwnerRole = includes(currentRoles, ownerRole);
|
||||
const isRemovingOwnerRole = !includes(newRoles, ownerRole);
|
||||
if (hasOwnerRole && isRemovingOwnerRole) {
|
||||
await this.validateAtLeastOneOwner(projectId, ownerRole);
|
||||
}
|
||||
await this.validateAtLeastOneOwner(projectId, ownerRole);
|
||||
|
||||
await this.accessService.setProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
roles,
|
||||
newRoles,
|
||||
createdBy,
|
||||
);
|
||||
await this.eventService.storeEvent(
|
||||
@ -724,11 +750,11 @@ export default class ProjectService {
|
||||
project: projectId,
|
||||
createdBy,
|
||||
data: {
|
||||
roles,
|
||||
roles: newRoles,
|
||||
groupId,
|
||||
},
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
roles: currentRoles,
|
||||
groupId,
|
||||
},
|
||||
}),
|
||||
@ -1091,7 +1117,7 @@ export default class ProjectService {
|
||||
return {
|
||||
stats: projectStats,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
description: project.description!,
|
||||
mode: project.mode,
|
||||
featureLimit: project.featureLimit,
|
||||
featureNaming: project.featureNaming,
|
||||
|
@ -17,8 +17,10 @@ import {
|
||||
createFeatureToggleService,
|
||||
createProjectService,
|
||||
} from '../../../lib/features';
|
||||
import { IGroup, IUnleashStores } from 'lib/types';
|
||||
import { User } from 'lib/server-impl';
|
||||
|
||||
let stores;
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
||||
|
||||
let projectService: ProjectService;
|
||||
@ -26,7 +28,8 @@ let accessService: AccessService;
|
||||
let eventService: EventService;
|
||||
let environmentService: EnvironmentService;
|
||||
let featureToggleService: FeatureToggleService;
|
||||
let user;
|
||||
let user: User; // many methods in this test use User instead of IUser
|
||||
let group: IGroup;
|
||||
|
||||
const isProjectUser = async (
|
||||
userId: number,
|
||||
@ -41,13 +44,17 @@ const isProjectUser = async (
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_service_serial', getLogger);
|
||||
stores = db.stores;
|
||||
// @ts-ignore return type IUser type missing generateImageUrl
|
||||
user = await stores.userStore.insert({
|
||||
name: 'Some Name',
|
||||
email: 'test@getunleash.io',
|
||||
});
|
||||
group = await stores.groupStore.create({
|
||||
name: 'aTestGroup',
|
||||
description: '',
|
||||
});
|
||||
const config = createTestConfig({
|
||||
getLogger,
|
||||
// @ts-ignore
|
||||
experimental: {
|
||||
flags: { privateProjects: true },
|
||||
},
|
||||
@ -164,6 +171,7 @@ test('should not be able to delete project with toggles', async () => {
|
||||
await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: 'test-project-delete',
|
||||
// @ts-ignore project does not exist in type FeatureToggleDTO
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
defaultStickiness: 'default',
|
||||
@ -491,31 +499,6 @@ test('should remove user from the project', async () => {
|
||||
expect(memberUsers).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not remove user from the project', async () => {
|
||||
const project = {
|
||||
id: 'remove-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const roles = await stores.roleStore.getRolesForProject(project.id);
|
||||
const ownerRole = roles.find((r) => r.name === RoleName.OWNER);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.removeUser(
|
||||
project.id,
|
||||
ownerRole.id,
|
||||
user.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not change project if feature toggle project does not match current project id', async () => {
|
||||
const project = {
|
||||
id: 'test-change-project',
|
||||
@ -528,6 +511,7 @@ test('should not change project if feature toggle project does not match current
|
||||
const toggle = { name: 'test-toggle' };
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
@ -555,6 +539,7 @@ test('should return 404 if no project is found with the project id', async () =>
|
||||
const toggle = { name: 'test-toggle-2' };
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
@ -594,6 +579,7 @@ test('should fail if user is not authorized', async () => {
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
await projectService.createProject(projectDestination, projectAdmin1);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
@ -626,6 +612,7 @@ test('should change project when checks pass', async () => {
|
||||
|
||||
await projectService.createProject(projectA, user);
|
||||
await projectService.createProject(projectB, user);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(projectA.id, toggle, user);
|
||||
await projectService.changeProject(
|
||||
projectB.id,
|
||||
@ -656,6 +643,7 @@ test('changing project should emit event even if user does not have a username s
|
||||
const toggle = { name: randomId() };
|
||||
await projectService.createProject(projectA, user);
|
||||
await projectService.createProject(projectB, user);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(projectA.id, toggle, user);
|
||||
const eventsBeforeChange = await stores.eventStore.getEvents();
|
||||
await projectService.changeProject(
|
||||
@ -686,6 +674,7 @@ test('should require equal project environments to move features', async () => {
|
||||
|
||||
await projectService.createProject(projectA, user);
|
||||
await projectService.createProject(projectB, user);
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(projectA.id, toggle, user);
|
||||
await stores.environmentStore.create(environment);
|
||||
await environmentService.addEnvironmentToProject(
|
||||
@ -1013,40 +1002,180 @@ test('should able to assign role without existing members', async () => {
|
||||
expect(testUsers).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should not update role for user on project when she is the owner', async () => {
|
||||
const project = {
|
||||
id: 'update-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
describe('ensure project has at least one owner', () => {
|
||||
test('should not remove user from the project', async () => {
|
||||
const project = {
|
||||
id: 'remove-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const projectMember1 = await stores.userStore.insert({
|
||||
name: 'Some Member',
|
||||
email: 'update991@getunleash.io',
|
||||
const roles = await stores.roleStore.getRolesForProject(project.id);
|
||||
const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.removeUser(
|
||||
project.id,
|
||||
ownerRole.id,
|
||||
user.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.removeUserAccess(project.id, user.id, 'test');
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
|
||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
test('should not update role for user on project when she is the owner', async () => {
|
||||
const project = {
|
||||
id: 'update-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
await projectService.addUser(
|
||||
project.id,
|
||||
memberRole.id,
|
||||
projectMember1.id,
|
||||
'test',
|
||||
);
|
||||
const projectMember1 = await stores.userStore.insert({
|
||||
name: 'Some Member',
|
||||
email: 'update991@getunleash.io',
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.changeRole(
|
||||
const memberRole = await stores.roleStore.getRoleByName(
|
||||
RoleName.MEMBER,
|
||||
);
|
||||
|
||||
await projectService.addUser(
|
||||
project.id,
|
||||
memberRole.id,
|
||||
projectMember1.id,
|
||||
'test',
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.changeRole(
|
||||
project.id,
|
||||
memberRole.id,
|
||||
user.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.setRolesForUser(
|
||||
project.id,
|
||||
user.id,
|
||||
[memberRole.id],
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
|
||||
async function projectWithGroupOwner(projectId: string) {
|
||||
const project = {
|
||||
id: projectId,
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const roles = await stores.roleStore.getRolesForProject(project.id);
|
||||
const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
|
||||
|
||||
await projectService.addGroup(
|
||||
project.id,
|
||||
ownerRole.id,
|
||||
group.id,
|
||||
'test',
|
||||
);
|
||||
|
||||
// this should be fine, leaving the group as the only owner
|
||||
// note group has zero members, but it still acts as an owner
|
||||
await projectService.removeUser(
|
||||
project.id,
|
||||
ownerRole.id,
|
||||
user.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
|
||||
return {
|
||||
project,
|
||||
group,
|
||||
ownerRole,
|
||||
};
|
||||
}
|
||||
|
||||
test('should not remove group from the project', async () => {
|
||||
const { project, group, ownerRole } = await projectWithGroupOwner(
|
||||
'remove-group-not-allowed',
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.removeGroup(
|
||||
project.id,
|
||||
ownerRole.id,
|
||||
group.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.removeGroupAccess(
|
||||
project.id,
|
||||
group.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not update role for group on project when she is the owner', async () => {
|
||||
const { project, group } = await projectWithGroupOwner(
|
||||
'update-group-not-allowed',
|
||||
);
|
||||
const memberRole = await stores.roleStore.getRoleByName(
|
||||
RoleName.MEMBER,
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.changeGroupRole(
|
||||
project.id,
|
||||
memberRole.id,
|
||||
group.id,
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.setRolesForGroup(
|
||||
project.id,
|
||||
group.id,
|
||||
[memberRole.id],
|
||||
'test',
|
||||
);
|
||||
}).rejects.toThrowError(
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Should allow bulk update of group permissions', async () => {
|
||||
@ -1056,6 +1185,7 @@ test('Should allow bulk update of group permissions', async () => {
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
const groupStore = stores.groupStore;
|
||||
|
||||
@ -1124,6 +1254,7 @@ test('Should allow bulk update of only groups', async () => {
|
||||
};
|
||||
const groupStore = stores.groupStore;
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const group1 = await groupStore.create({
|
||||
@ -1158,6 +1289,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const group1 = await stores.groupStore.create({
|
||||
@ -1232,11 +1364,13 @@ test('should only count active feature toggles for project', async () => {
|
||||
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: 'only-active-t1',
|
||||
// @ts-ignore project property does not exist in FeatureToggleDTO
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
});
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: 'only-active-t2',
|
||||
// @ts-ignore project property does not exist in FeatureToggleDTO
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
});
|
||||
@ -1261,6 +1395,7 @@ test('should list projects with all features archived', async () => {
|
||||
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: 'archived-toggle',
|
||||
// @ts-ignore project property does not exist in FeatureToggleDTO
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
});
|
||||
@ -1294,6 +1429,7 @@ test('should calculate average time to production', async () => {
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const toggles = [
|
||||
@ -1309,6 +1445,7 @@ test('should calculate average time to production', async () => {
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1360,6 +1497,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
await stores.environmentStore.create({
|
||||
name: 'customEnv',
|
||||
@ -1369,6 +1507,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
|
||||
// actual toggle we take for calculations
|
||||
const toggle = { name: 'main-toggle' };
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
await updateFeature(toggle.name, {
|
||||
created_at: subDays(new Date(), 20),
|
||||
@ -1384,6 +1523,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
|
||||
// ignore toggles enabled in non-prod envs
|
||||
const devToggle = { name: 'dev-toggle' };
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
await featureToggleService.createFeatureToggle(project.id, devToggle, user);
|
||||
await eventService.storeEvent(
|
||||
new FeatureEnvironmentEvent({
|
||||
@ -1397,6 +1537,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
await featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
otherProjectToggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
await eventService.storeEvent(
|
||||
@ -1408,6 +1549,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
await featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
nonReleaseToggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
await eventService.storeEvent(
|
||||
@ -1419,6 +1561,7 @@ test('should calculate average time to production ignoring some items', async ()
|
||||
await featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
previouslyDeleteToggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
await eventService.storeEvent(
|
||||
@ -1441,6 +1584,7 @@ test('should get correct amount of features created in current and past window',
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const toggles = [
|
||||
@ -1455,6 +1599,7 @@ test('should get correct amount of features created in current and past window',
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1478,6 +1623,7 @@ test('should get correct amount of features archived in current and past window'
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const toggles = [
|
||||
@ -1492,6 +1638,7 @@ test('should get correct amount of features archived in current and past window'
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1529,6 +1676,7 @@ test('should get correct amount of project members for current and past window',
|
||||
defaultStickiness: 'default',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const users = [
|
||||
@ -1569,6 +1717,7 @@ test('should return average time to production per toggle', async () => {
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong type should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const toggles = [
|
||||
@ -1584,7 +1733,7 @@ test('should return average time to production per toggle', async () => {
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project.id,
|
||||
toggle,
|
||||
user,
|
||||
user.email!,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@ -1633,7 +1782,9 @@ test('should return average time to production per toggle for a specific project
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project1, user.id);
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project2, user.id);
|
||||
|
||||
const togglesProject1 = [
|
||||
@ -1652,6 +1803,7 @@ test('should return average time to production per toggle for a specific project
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project1.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1662,6 +1814,7 @@ test('should return average time to production per toggle for a specific project
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project2.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1726,6 +1879,7 @@ test('should return average time to production per toggle and include archived t
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project1, user.id);
|
||||
|
||||
const togglesProject1 = [
|
||||
@ -1739,6 +1893,7 @@ test('should return average time to production per toggle and include archived t
|
||||
return featureToggleService.createFeatureToggle(
|
||||
project1.id,
|
||||
toggle,
|
||||
// @ts-ignore user is wrong parameter type, should be string
|
||||
user,
|
||||
);
|
||||
}),
|
||||
@ -1790,6 +1945,7 @@ describe('feature flag naming patterns', () => {
|
||||
featureNaming,
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
await projectService.updateProjectEnterpriseSettings(project, user);
|
||||
@ -1804,6 +1960,7 @@ describe('feature flag naming patterns', () => {
|
||||
...project,
|
||||
featureNaming: { pattern: newPattern },
|
||||
},
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
user.id,
|
||||
);
|
||||
|
||||
@ -1822,10 +1979,12 @@ test('deleting a project with archived toggles should result in any remaining ar
|
||||
};
|
||||
const toggleName = 'archived-and-deleted';
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: toggleName,
|
||||
// @ts-ignore project property does not exist in FeatureToggleDTO
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
defaultStickiness: 'default',
|
||||
@ -1836,6 +1995,7 @@ test('deleting a project with archived toggles should result in any remaining ar
|
||||
|
||||
// bring the project back again, previously this would allow those archived toggles to be resurrected
|
||||
// we now expect them to be deleted correctly
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const toggles = await stores.featureToggleStore.getAll({
|
||||
@ -1852,6 +2012,7 @@ test('deleting a project with no archived toggles should not result in an error'
|
||||
name: 'project-with-nothing',
|
||||
};
|
||||
|
||||
// @ts-ignore user.id is wrong parameter type, should be user
|
||||
await projectService.createProject(project, user.id);
|
||||
await projectService.deleteProject(project.id, user);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user