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

feat: project mode (#3334)

This commit is contained in:
Mateusz Kwasniewski 2023-03-16 15:29:52 +01:00 committed by GitHub
parent a983cf15b9
commit 1064dfa40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 272 additions and 41 deletions

View File

@ -19,6 +19,7 @@ const CreateProject = () => {
const {
projectId,
projectName,
projectMode,
projectDesc,
setProjectId,
setProjectName,
@ -28,6 +29,7 @@ const CreateProject = () => {
validateProjectId,
validateName,
setProjectStickiness,
setProjectMode,
projectStickiness,
errors,
} = useProjectForm();
@ -92,8 +94,10 @@ const CreateProject = () => {
projectId={projectId}
setProjectId={setProjectId}
projectName={projectName}
projectMode={projectMode}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
setProjectMode={setProjectMode}
setProjectName={setProjectName}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}

View File

@ -31,10 +31,12 @@ const EditProject = () => {
projectName,
projectDesc,
projectStickiness,
projectMode,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
setProjectMode,
getProjectPayload,
clearErrors,
validateProjectId,
@ -44,7 +46,8 @@ const EditProject = () => {
id,
project.name,
project.description,
defaultStickiness
defaultStickiness,
project.mode
);
const formatApiCode = () => {
@ -111,9 +114,11 @@ const EditProject = () => {
projectId={projectId}
setProjectId={setProjectId}
projectName={projectName}
projectMode={projectMode}
setProjectName={setProjectName}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
setProjectMode={setProjectMode}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
mode="Edit"

View File

@ -1,23 +1,27 @@
import React from 'react';
import { trim } from 'component/common/util';
import {
StyledForm,
StyledButton,
StyledButtonContainer,
StyledContainer,
StyledDescription,
StyledForm,
StyledInput,
StyledTextField,
StyledButtonContainer,
StyledButton,
} from './ProjectForm.styles';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Select from 'component/common/select';
interface IProjectForm {
projectId: string;
projectName: string;
projectDesc: string;
projectStickiness?: string;
projectMode?: string;
setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>;
setProjectMode?: React.Dispatch<React.SetStateAction<string>>;
setProjectId: React.Dispatch<React.SetStateAction<string>>;
setProjectName: React.Dispatch<React.SetStateAction<string>>;
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
@ -37,17 +41,20 @@ const ProjectForm: React.FC<IProjectForm> = ({
projectName,
projectDesc,
projectStickiness,
projectMode,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
setProjectMode,
errors,
mode,
validateProjectId,
clearErrors,
}) => {
const { uiConfig } = useUiConfig();
const { projectScopedStickiness } = uiConfig.flags;
const { projectScopedStickiness, projectMode: projectModeFlag } =
uiConfig.flags;
return (
<StyledForm onSubmit={handleSubmit}>
@ -113,6 +120,30 @@ const ProjectForm: React.FC<IProjectForm> = ({
</>
}
/>
<ConditionallyRender
condition={Boolean(projectModeFlag)}
show={
<>
<StyledDescription>
What is your project mode?
</StyledDescription>
<Select
id="project-mode"
value={projectMode}
label="Project mode"
name="Project mode"
onChange={e => {
setProjectMode?.(e.target.value);
}}
options={[
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
]}
style={{ minWidth: '150px' }}
></Select>
</>
}
/>
</StyledContainer>
<StyledButtonContainer>

View File

@ -7,7 +7,8 @@ const useProjectForm = (
initialProjectId = '',
initialProjectName = '',
initialProjectDesc = '',
initialProjectStickiness = 'default'
initialProjectStickiness = 'default',
initialProjectMode = 'open'
) => {
const [projectId, setProjectId] = useState(initialProjectId);
const { defaultStickiness } = useDefaultProjectSettings(projectId);
@ -17,6 +18,7 @@ const useProjectForm = (
const [projectStickiness, setProjectStickiness] = useState(
defaultStickiness || initialProjectStickiness
);
const [projectMode, setProjectMode] = useState(initialProjectMode);
const [errors, setErrors] = useState({});
const { validateId } = useProjectApi();
@ -33,12 +35,17 @@ const useProjectForm = (
setProjectDesc(initialProjectDesc);
}, [initialProjectDesc]);
useEffect(() => {
setProjectMode(initialProjectMode);
}, [initialProjectMode]);
const getProjectPayload = () => {
return {
id: projectId,
name: projectName,
description: projectDesc,
projectStickiness,
mode: projectMode,
};
};
@ -74,10 +81,12 @@ const useProjectForm = (
projectName,
projectDesc,
projectStickiness,
projectMode,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
setProjectMode,
getProjectPayload,
validateName,
validateProjectId,

View File

@ -12,6 +12,7 @@ const fallbackProject: IProject = {
version: '1',
description: 'Default',
favorite: false,
mode: 'open',
stats: {
archivedCurrentWindow: 0,
archivedPastWindow: 0,

View File

@ -23,6 +23,7 @@ export interface IProject {
stats: ProjectStatsSchema;
favorite: boolean;
features: IFeatureToggleListItem[];
mode: 'open' | 'protected';
}
export interface IProjectHealthReport extends IProject {

View File

@ -51,6 +51,7 @@ export interface IFlags {
bulkOperations?: boolean;
projectScopedSegments?: boolean;
projectScopedStickiness?: boolean;
projectMode?: boolean;
}
export interface IVersionInfo {

View File

@ -2,7 +2,12 @@ import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
import NotFoundError from '../error/notfound-error';
import { IEnvironment, IProject, IProjectWithCount } from '../types/model';
import {
IEnvironment,
IProject,
IProjectWithCount,
ProjectMode,
} from '../types/model';
import {
IProjectHealthUpdate,
IProjectInsert,
@ -26,6 +31,8 @@ const COLUMNS = [
'updated_at',
];
const TABLE = 'projects';
const SETTINGS_COLUMNS = ['project_mode'];
const SETTINGS_TABLE = 'project_settings';
export interface IEnvironmentProjectLink {
environmentName: string;
@ -63,7 +70,7 @@ class ProjectStore implements IProjectStore {
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
fieldToRow(data): IProjectInsert {
fieldToRow(data): Omit<IProjectInsert, 'mode'> {
return {
id: data.id,
name: data.name,
@ -168,8 +175,13 @@ class ProjectStore implements IProjectStore {
async get(id: string): Promise<IProject> {
return this.db
.first(COLUMNS)
.first([...COLUMNS, ...SETTINGS_COLUMNS])
.from(TABLE)
.leftJoin(
SETTINGS_TABLE,
`${SETTINGS_TABLE}.project`,
`${TABLE}.id`,
)
.where({ id })
.then(this.mapRow);
}
@ -189,11 +201,19 @@ class ProjectStore implements IProjectStore {
.update({ health: healthUpdate.health, updated_at: new Date() });
}
async create(project: IProjectInsert): Promise<IProject> {
async create(
project: IProjectInsert & { mode: ProjectMode },
): Promise<IProject> {
const row = await this.db(TABLE)
.insert(this.fieldToRow(project))
.returning('*');
return this.mapRow(row[0]);
const settingsRow = await this.db(SETTINGS_TABLE)
.insert({
project: project.id,
project_mode: project.mode,
})
.returning('*');
return this.mapRow({ ...row[0], ...settingsRow[0] });
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -202,6 +222,12 @@ class ProjectStore implements IProjectStore {
await this.db(TABLE)
.where({ id: data.id })
.update(this.fieldToRow(data));
await this.db(SETTINGS_TABLE)
.where({ project: data.id })
.update({
project_mode: data.mode,
})
.returning('*');
} catch (err) {
this.logger.error('Could not update project, error: ', err);
}
@ -460,7 +486,7 @@ class ProjectStore implements IProjectStore {
createdAt: row.created_at,
health: row.health ?? 100,
updatedAt: row.updated_at || new Date(),
mode: 'open',
mode: row.project_mode || 'open',
};
}
}

View File

@ -87,6 +87,7 @@ const createProject = async () => {
name: DEFAULT_PROJECT,
description: '',
id: DEFAULT_PROJECT,
mode: 'open' as const,
});
};

View File

@ -109,6 +109,7 @@ const createProject = async () => {
name: DEFAULT_PROJECT,
description: '',
id: DEFAULT_PROJECT,
mode: 'open' as const,
});
await app.request
.post(`/api/admin/projects/${DEFAULT_PROJECT}/environments`)

View File

@ -25,6 +25,14 @@ export const healthOverviewSchema = {
type: 'string',
nullable: true,
},
mode: {
type: 'string',
enum: ['open', 'protected'],
example: 'open',
nullable: true,
description:
'A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or `null`',
},
members: {
type: 'number',
},

View File

@ -36,6 +36,13 @@ export const projectOverviewSchema = {
example: 'DX squad feature release',
description: 'Additional information about the project',
},
mode: {
type: 'string',
enum: ['open', 'protected'],
example: 'open',
description:
'A mode of the project affecting what actions are possible in this project',
},
members: {
type: 'number',
example: 4,

View File

@ -59,9 +59,8 @@ export const projectSchema = {
type: 'string',
enum: ['open', 'protected'],
example: 'open',
nullable: true,
description:
'A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or `null`',
'A mode of the project affecting what actions are possible in this project',
},
},
components: {},

View File

@ -7,5 +7,6 @@ export const projectSchema = joi
id: nameType,
name: joi.string().required(),
description: joi.string().allow(null).allow('').optional(),
mode: joi.string().valid('open', 'protected').default('open'),
})
.options({ allowUnknown: false, stripUnknown: true });

View File

@ -166,7 +166,7 @@ export default class ProjectService {
}
async createProject(
newProject: Pick<IProject, 'id' | 'name'>,
newProject: Pick<IProject, 'id' | 'name' | 'mode'>,
user: IUser,
): Promise<IProject> {
const data = await projectSchema.validateAsync(newProject);
@ -853,6 +853,7 @@ export default class ProjectService {
stats: projectStats,
name: project.name,
description: project.description,
mode: project.mode,
health: project.health || 0,
favorite: favorite,
updatedAt: project.updatedAt,

View File

@ -518,6 +518,7 @@ test('Should not import an existing project', async () => {
id: 'default',
name: 'default',
description: 'Some fancy description for project',
mode: 'open' as const,
},
],
};
@ -546,6 +547,7 @@ test('Should drop projects before import if specified', async () => {
id: 'fancy',
name: 'extra',
description: 'Not expected to be seen after import',
mode: 'open' as const,
});
await stateService.import({ data, dropBeforeImport: true });
const hasProject = await stores.projectStore.hasProject('fancy');
@ -558,6 +560,7 @@ test('Should export projects', async () => {
id: 'fancy',
name: 'extra',
description: 'No surprises here',
mode: 'open' as const,
});
const exported = await stateService.export({
includeFeatureToggles: false,
@ -579,6 +582,7 @@ test('exporting to new format works', async () => {
id: 'fancy',
name: 'extra',
description: 'No surprises here',
mode: 'open' as const,
});
await stores.environmentStore.create({
name: 'dev',
@ -622,7 +626,7 @@ test('exporting variants to v4 format should not include variants in features',
exported.featureEnvironments.forEach((fe) => {
expect(fe.variants).toHaveLength(1);
expect(fe.variants[0].name).toBe(`${fe.environment}-variant`);
expect(fe.variants?.[0].name).toBe(`${fe.environment}-variant`);
});
expect(exported.environments).toHaveLength(3);
});
@ -633,6 +637,7 @@ test('featureStrategies can keep existing', async () => {
id: 'fancy',
name: 'extra',
description: 'No surprises here',
mode: 'open' as const,
});
await stores.environmentStore.create({
name: 'dev',
@ -679,6 +684,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
id: 'fancy',
name: 'extra',
description: 'No surprises here',
mode: 'open' as const,
});
await stores.environmentStore.create({
name: 'dev',

View File

@ -173,6 +173,8 @@ export interface IFeatureOverview {
environments: IEnvironmentOverview[];
}
export type ProjectMode = 'open' | 'protected';
export interface IProjectOverview {
name: string;
description: string;
@ -184,6 +186,7 @@ export interface IProjectOverview {
favorite?: boolean;
updatedAt?: Date;
stats?: IProjectStats;
mode: ProjectMode;
}
export interface IProjectHealthReport extends IProjectOverview {
@ -368,7 +371,6 @@ export interface IProject {
changeRequestsEnabled?: boolean;
mode: ProjectMode;
}
export type ProjectMode = 'open' | 'protected';
export interface ICustomRole {
id: number;

View File

@ -2,7 +2,12 @@ import {
IEnvironmentProjectLink,
IProjectMembersCount,
} from '../../db/project-store';
import { IEnvironment, IProject, IProjectWithCount } from '../model';
import {
IEnvironment,
IProject,
IProjectWithCount,
ProjectMode,
} from '../model';
import { Store } from './store';
export interface IProjectInsert {
@ -11,6 +16,7 @@ export interface IProjectInsert {
description: string;
updatedAt?: Date;
changeRequestsEnabled?: boolean;
mode: ProjectMode;
}
export interface IProjectArchived {

View File

@ -75,11 +75,13 @@ test('Should get archived toggles via project', async () => {
id: 'proj-1',
name: 'proj-1',
description: '',
mode: 'open' as const,
});
await db.stores.projectStore.create({
id: 'proj-2',
name: 'proj-2',
description: '',
mode: 'open' as const,
});
await db.stores.featureToggleStore.create('proj-1', {

View File

@ -36,6 +36,7 @@ test('should return instance statistics with correct number of projects', async
id: 'test',
name: 'Test',
description: 'lorem',
mode: 'open' as const,
});
return app.request

View File

@ -544,7 +544,7 @@ describe('Interacting with features using project IDs that belong to other proje
rootRole: RoleName.ADMIN,
});
await app.services.projectService.createProject(
{ name: otherProject, id: otherProject },
{ name: otherProject, id: otherProject, mode: 'open' },
dummyAdmin,
);
@ -701,8 +701,8 @@ test('Should patch feature toggle', async () => {
});
const updateForOurToggle = events.find((e) => e.data.name === name);
expect(updateForOurToggle).toBeTruthy();
expect(updateForOurToggle.data.description).toBe('New desc');
expect(updateForOurToggle.data.type).toBe('kill-switch');
expect(updateForOurToggle?.data.description).toBe('New desc');
expect(updateForOurToggle?.data.type).toBe('kill-switch');
});
test('Should patch feature toggle and not remove variants', async () => {
@ -1956,6 +1956,7 @@ test('Should not allow changing project to target project without the same enabl
name: 'Project to be moved to',
id: targetProject,
description: '',
mode: 'open',
});
await db.stores.environmentStore.create({
@ -2042,6 +2043,7 @@ test('Should allow changing project to target project with the same enabled envi
name: 'Project to be moved to',
id: targetProject,
description: '',
mode: 'open',
});
await db.stores.environmentStore.create({
@ -2284,6 +2286,7 @@ test('Can create toggle with impression data on different project', async () =>
id: 'impression-data',
name: 'ImpressionData',
description: '',
mode: 'open',
});
const toggle = {
@ -2313,6 +2316,7 @@ test('should reject invalid constraint values for multi-valued constraints', asy
id: uuidv4(),
name: uuidv4(),
description: '',
mode: 'open',
});
const toggle = await db.stores.featureToggleStore.create(project.id, {
@ -2359,6 +2363,7 @@ test('should add default constraint values for single-valued constraints', async
id: uuidv4(),
name: uuidv4(),
description: '',
mode: 'open',
});
const toggle = await db.stores.featureToggleStore.create(project.id, {
@ -2418,6 +2423,7 @@ test('should allow long parameter values', async () => {
id: uuidv4(),
name: uuidv4(),
description: uuidv4(),
mode: 'open',
});
const toggle = await db.stores.featureToggleStore.create(project.id, {

View File

@ -26,7 +26,12 @@ afterAll(async () => {
});
test('Should ONLY return default project', async () => {
projectStore.create({ id: 'test2', name: 'test', description: '' });
projectStore.create({
id: 'test2',
name: 'test',
description: '',
mode: 'open',
});
const { body } = await app.request
.get('/api/admin/projects')

View File

@ -154,6 +154,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
name: projectId,
id: projectId,
description: 'Project for export',
mode: 'open' as const,
});
await app.services.environmentService.addEnvironmentToProject(
environment,
@ -201,6 +202,7 @@ test('Roundtrip with tags works', async () => {
name: projectId,
id: projectId,
description: 'Project for export',
mode: 'open' as const,
});
await app.services.environmentService.addEnvironmentToProject(
environment,
@ -265,6 +267,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
name: projectId,
id: projectId,
description: 'Project for export',
mode: 'open' as const,
});
await app.services.featureToggleServiceV2.createFeatureToggle(
@ -364,6 +367,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
name: projectId,
id: projectId,
description: 'Project for export',
mode: 'open' as const,
});
await app.services.environmentService.addEnvironmentToProject(
environment,

View File

@ -289,6 +289,7 @@ test('returns a feature toggles impression data for a different project', async
id: 'impression-data-client',
name: 'ImpressionData',
description: '',
mode: 'open' as const,
};
await db.stores.projectStore.create(project);

View File

@ -35,6 +35,7 @@ beforeAll(async () => {
id: project2,
name: 'Test Project 2',
description: '',
mode: 'open' as const,
});
await environmentService.addEnvironmentToProject(environment, project);

View File

@ -1859,6 +1859,16 @@ exports[`should serve the OpenAPI spec 1`] = `
"members": {
"type": "number",
},
"mode": {
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
"enum": [
"open",
"protected",
],
"example": "open",
"nullable": true,
"type": "string",
},
"name": {
"type": "string",
},
@ -1912,6 +1922,16 @@ exports[`should serve the OpenAPI spec 1`] = `
"members": {
"type": "number",
},
"mode": {
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
"enum": [
"open",
"protected",
],
"example": "open",
"nullable": true,
"type": "string",
},
"name": {
"type": "string",
},
@ -2771,6 +2791,15 @@ exports[`should serve the OpenAPI spec 1`] = `
"example": 4,
"type": "number",
},
"mode": {
"description": "A mode of the project affecting what actions are possible in this project",
"enum": [
"open",
"protected",
],
"example": "open",
"type": "string",
},
"name": {
"description": "The name of this project",
"example": "dx-squad",
@ -2837,13 +2866,12 @@ exports[`should serve the OpenAPI spec 1`] = `
"type": "number",
},
"mode": {
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
"description": "A mode of the project affecting what actions are possible in this project",
"enum": [
"open",
"protected",
],
"example": "open",
"nullable": true,
"type": "string",
},
"name": {

View File

@ -100,7 +100,10 @@ const createProject = async (id: string, name: string): Promise<void> => {
name: randomId(),
email: `${randomId()}@example.com`,
});
await app.services.projectService.createProject({ id, name }, user);
await app.services.projectService.createProject(
{ id, name, mode: 'open' },
user,
);
};
test('should require a frontend token or an admin token', async () => {
@ -188,14 +191,14 @@ test('should allow requests with a token secret alias', async () => {
.expect((res) => expect(res.body.toggles[0].name).toEqual(featureB));
await app.request
.get('/api/frontend')
.set('Authorization', tokenA.alias)
.set('Authorization', tokenA.alias!)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => expect(res.body.toggles).toHaveLength(1))
.expect((res) => expect(res.body.toggles[0].name).toEqual(featureA));
await app.request
.get('/api/frontend')
.set('Authorization', tokenB.alias)
.set('Authorization', tokenB.alias!)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => expect(res.body.toggles).toHaveLength(1))
@ -952,7 +955,7 @@ test('should terminate data polling when stop is called', async () => {
frontendToken.secret,
);
const logTrap = [];
const logTrap: any[] = [];
const getDebugLogger = (): Logger => {
return {
/* eslint-disable-next-line */
@ -978,7 +981,7 @@ test('should terminate data polling when stop is called', async () => {
},
db.stores,
app.services,
user,
user!,
);
await proxyRepository.start();

View File

@ -36,6 +36,7 @@ beforeAll(async () => {
id: 'test-project',
name: 'Test Project',
description: 'Fancy',
mode: 'open' as const,
};
const user = await stores.userStore.insert({
name: 'Some Name',
@ -219,11 +220,11 @@ test('should return user with multiple projects', async () => {
tokens[1].secret,
);
expect(multiProjectUser.projects).toStrictEqual([
expect(multiProjectUser!.projects).toStrictEqual([
'test-project',
'default',
]);
expect(singleProjectUser.projects).toStrictEqual(['test-project']);
expect(singleProjectUser!.projects).toStrictEqual(['test-project']);
});
test('should not partially create token if projects are invalid', async () => {

View File

@ -100,6 +100,7 @@ test('should list all projects', async () => {
id: 'test-list',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -113,6 +114,7 @@ test('should create new project', async () => {
id: 'test',
name: 'New project',
description: 'Blah',
mode: 'protected' as const,
};
await projectService.createProject(project, user);
@ -121,6 +123,7 @@ test('should create new project', async () => {
expect(project.name).toEqual(ret.name);
expect(project.description).toEqual(ret.description);
expect(ret.createdAt).toBeTruthy();
expect(ret.mode).toEqual('protected');
});
test('should delete project', async () => {
@ -128,6 +131,7 @@ test('should delete project', async () => {
id: 'test-delete',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -145,6 +149,7 @@ test('should not be able to delete project with toggles', async () => {
id: 'test-delete-with-toggles',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
await stores.featureToggleStore.create(project.id, {
@ -180,6 +185,7 @@ test('should not be able to create existing project', async () => {
id: 'test-delete',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
try {
await projectService.createProject(project, user);
@ -210,13 +216,14 @@ test('should update project', async () => {
id: 'test-update',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
const updatedProject = {
id: 'test-update',
name: 'New name',
description: 'Blah longer desc',
mode: 'open' as const,
mode: 'protected' as const,
};
await projectService.createProject(project, user);
@ -226,6 +233,7 @@ test('should update project', async () => {
expect(updatedProject.name).toBe(readProject.name);
expect(updatedProject.description).toBe(readProject.description);
expect(updatedProject.mode).toBe('protected');
});
test('should give error when getting unknown project', async () => {
@ -241,6 +249,7 @@ test('should get list of users with access to project', async () => {
id: 'test-roles-access',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
const { users } = await projectService.getAccessToProject(project.id);
@ -262,6 +271,7 @@ test('should add a member user to the project', async () => {
id: 'add-users',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -317,6 +327,7 @@ test('should add admin users to the project', async () => {
id: 'add-admin-users',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -363,6 +374,7 @@ test('add user should fail if user already have access', async () => {
id: 'add-users-twice',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -397,6 +409,7 @@ test('should remove user from the project', async () => {
id: 'remove-users',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -431,6 +444,7 @@ test('should not remove user from the project', async () => {
id: 'remove-users-not-allowed',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -454,6 +468,7 @@ test('should not change project if feature toggle project does not match current
id: 'test-change-project',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
const toggle = { name: 'test-toggle' };
@ -480,6 +495,7 @@ test('should return 404 if no project is found with the project id', async () =>
id: 'test-change-project-2',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
const toggle = { name: 'test-toggle-2' };
@ -504,12 +520,14 @@ test('should fail if user is not authorized', async () => {
id: 'test-change-project-3',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
const projectDestination = {
id: 'test-change-project-dest',
name: 'New project 2',
description: 'Blah',
mode: 'open' as const,
};
const toggle = { name: 'test-toggle-3' };
@ -537,8 +555,16 @@ test('should fail if user is not authorized', async () => {
});
test('should change project when checks pass', async () => {
const projectA = { id: randomId(), name: randomId() };
const projectB = { id: randomId(), name: randomId() };
const projectA = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const toggle = { name: randomId() };
await projectService.createProject(projectA, user);
@ -558,8 +584,16 @@ test('should change project when checks pass', async () => {
});
test('changing project should emit event even if user does not have a username set', async () => {
const projectA = { id: randomId(), name: randomId() };
const projectB = { id: randomId(), name: randomId() };
const projectA = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const toggle = { name: randomId() };
await projectService.createProject(projectA, user);
await projectService.createProject(projectB, user);
@ -576,8 +610,16 @@ test('changing project should emit event even if user does not have a username s
}, 10000);
test('should require equal project environments to move features', async () => {
const projectA = { id: randomId(), name: randomId() };
const projectB = { id: randomId(), name: randomId() };
const projectA = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
};
const environment = { name: randomId(), type: 'production' };
const toggle = { name: randomId() };
@ -605,6 +647,7 @@ test('A newly created project only gets connected to enabled environments', asyn
id: 'environment-test',
name: 'New environment project',
description: 'Blah',
mode: 'open' as const,
};
const enabledEnv = 'connection_test';
await db.stores.environmentStore.create({
@ -631,6 +674,7 @@ test('should have environments sorted in order', async () => {
id: 'environment-order-test',
name: 'Environment testing project',
description: '',
mode: 'open' as const,
};
const first = 'test';
const second = 'abc';
@ -669,6 +713,7 @@ test('should add a user to the project with a custom role', async () => {
id: 'add-users-custom-role',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -719,6 +764,7 @@ test('should delete role entries when deleting project', async () => {
id: 'test-delete-users-1',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -770,6 +816,7 @@ test('should change a users role in the project', async () => {
id: 'test-change-user-role',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -836,6 +883,7 @@ test('should update role for user on project', async () => {
id: 'update-users',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -873,6 +921,7 @@ test('should able to assign role without existing members', async () => {
id: 'update-users-test',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -915,6 +964,7 @@ test('should not update role for user on project when she is the owner', async (
id: 'update-users-not-allowed',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -948,6 +998,7 @@ test('Should allow bulk update of group permissions', async () => {
const project = {
id: 'bulk-update-project',
name: 'bulk-update-project',
mode: 'open' as const,
};
await projectService.createProject(project, user.id);
const groupStore = stores.groupStore;
@ -1024,6 +1075,7 @@ test('Should allow bulk update of only groups', async () => {
const project = {
id: 'bulk-update-project-only',
name: 'bulk-update-project-only',
mode: 'open' as const,
};
const groupStore = stores.groupStore;
@ -1064,6 +1116,7 @@ test('should only count active feature toggles for project', async () => {
id: 'only-active',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -1091,6 +1144,7 @@ test('should list projects with all features archived', async () => {
id: 'only-archived',
name: 'Listed project',
description: 'Blah',
mode: 'open' as const,
};
await projectService.createProject(project, user);
@ -1126,6 +1180,7 @@ test('should calculate average time to production', async () => {
const project = {
id: 'average-time-to-prod',
name: 'average-time-to-prod',
mode: 'open' as const,
};
await projectService.createProject(project, user.id);
@ -1184,6 +1239,7 @@ test('should get correct amount of features created in current and past window',
const project = {
id: 'features-created',
name: 'features-created',
mode: 'open' as const,
};
await projectService.createProject(project, user.id);
@ -1219,6 +1275,7 @@ test('should get correct amount of features archived in current and past window'
const project = {
id: 'features-archived',
name: 'features-archived',
mode: 'open' as const,
};
await projectService.createProject(project, user.id);
@ -1268,6 +1325,7 @@ test('should get correct amount of project members for current and past window',
const project = {
id: 'features-members',
name: 'features-members',
mode: 'open' as const,
};
await projectService.createProject(project, user.id);

View File

@ -31,6 +31,7 @@ test('should create new project', async () => {
id: 'test',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectStore.create(project);
const ret = await projectStore.get('test');
@ -48,6 +49,7 @@ test('should delete project', async () => {
id: 'test-delete',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
await projectStore.create(project);
await projectStore.delete(project.id);
@ -64,12 +66,14 @@ test('should update project', async () => {
id: 'test-update',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
};
const updatedProject = {
id: 'test-update',
name: 'New name',
description: 'Blah longer desc',
mode: 'open' as const,
};
await projectStore.create(project);
@ -93,11 +97,17 @@ test('should import projects', async () => {
const projectsCount = (await projectStore.getAll()).length;
const projectsToImport: IProjectInsert[] = [
{ description: 'some project desc', name: 'some name', id: 'someId' },
{
description: 'some project desc',
name: 'some name',
id: 'someId',
mode: 'open' as const,
},
{
description: 'another project',
name: 'another name',
id: 'anotherId',
mode: 'open' as const,
},
];
@ -110,8 +120,8 @@ test('should import projects', async () => {
expect(projects.length - projectsCount).toBe(2);
expect(someId).toBeDefined();
expect(someId.name).toBe('some name');
expect(someId.description).toBe('some project desc');
expect(someId?.name).toBe('some name');
expect(someId?.description).toBe('some project desc');
expect(anotherId).toBeDefined();
});
@ -120,6 +130,7 @@ test('should add environment to project', async () => {
id: 'test-env',
name: 'New project with env',
description: 'Blah',
mode: 'open' as const,
};
await environmentStore.create({