1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-17 13:46:47 +02:00

feat: Move environments to enterprise (#935)

- Adding, updating and renaming environments are meant to be
  enterprise only features, as such, this PR moves these operations out
  of this server

- We still keep sortOrder updating, toggling on/off and getting one,
  getting all, so we can still work with environments in the OSS version
  as well.

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Fredrik Strand Oseberg 2021-09-13 15:57:38 +02:00 committed by GitHub
parent 1b1bb97715
commit 26c9d53b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 547 additions and 264 deletions

View File

@ -3,27 +3,39 @@ import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
import metricsHelper from '../util/metrics-helper';
import { DB_TIME } from '../metric-events';
import { IEnvironment } from '../types/model';
import { IEnvironment, IEnvironmentCreate } from '../types/model';
import NotFoundError from '../error/notfound-error';
import { IEnvironmentStore } from '../types/stores/environment-store';
import { snakeCaseKeys } from '../util/snakeCase';
interface IEnvironmentsTable {
name: string;
display_name: string;
created_at?: Date;
type: string;
sort_order: number;
enabled: boolean;
protected: boolean;
}
const COLUMNS = [
'type',
'display_name',
'name',
'created_at',
'sort_order',
'enabled',
'protected',
];
function mapRow(row: IEnvironmentsTable): IEnvironment {
return {
name: row.name,
displayName: row.display_name,
};
}
function mapInput(input: IEnvironment): IEnvironmentsTable {
return {
name: input.name,
display_name: input.displayName,
type: row.type,
sortOrder: row.sort_order,
enabled: row.enabled,
protected: row.protected,
};
}
@ -61,7 +73,9 @@ export default class EnvironmentStore implements IEnvironmentStore {
}
async getAll(): Promise<IEnvironment[]> {
const rows = await this.db<IEnvironmentsTable>(TABLE).select('*');
const rows = await this.db<IEnvironmentsTable>(TABLE)
.select('*')
.orderBy('sort_order', 'created_at');
return rows.map(mapRow);
}
@ -86,16 +100,48 @@ export default class EnvironmentStore implements IEnvironmentStore {
return mapRow(row);
}
async upsert(env: IEnvironment): Promise<IEnvironment> {
async updateProperty(
id: string,
field: string,
value: string | number,
): Promise<void> {
await this.db<IEnvironmentsTable>(TABLE)
.insert(mapInput(env))
.onConflict('name')
.merge();
return env;
.update({
[field]: value,
})
.where({ name: id, protected: false });
}
async updateSortOrder(id: string, value: number): Promise<void> {
await this.db<IEnvironmentsTable>(TABLE)
.update({
sort_order: value,
})
.where({ name: id });
}
async update(
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
name: string,
): Promise<IEnvironment> {
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
.update(snakeCaseKeys(env))
.where({ name, protected: false })
.returning<IEnvironmentsTable>(COLUMNS);
return mapRow(updatedEnv[0]);
}
async create(env: IEnvironmentCreate): Promise<IEnvironment> {
const row = await this.db<IEnvironmentsTable>(TABLE)
.insert(snakeCaseKeys(env))
.returning<IEnvironmentsTable>(COLUMNS);
return mapRow(row[0]);
}
async delete(name: string): Promise<void> {
await this.db(TABLE).where({ name }).del();
await this.db(TABLE).where({ name, protected: false }).del();
}
destroy(): void {}

View File

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import Controller from '../controller';
import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option';
import { IEnvironment } from '../../types/model';
import { ISortOrder } from '../../types/model';
import EnvironmentService from '../../services/environment-service';
import { Logger } from '../../logger';
import { ADMIN } from '../../types/permissions';
@ -24,10 +24,10 @@ export class EnvironmentsController extends Controller {
this.logger = config.getLogger('admin-api/environments-controller.ts');
this.service = environmentService;
this.get('/', this.getAll);
this.post('/', this.createEnv, ADMIN);
this.put('/sort-order', this.updateSortOrder, ADMIN);
this.get('/:name', this.getEnv);
this.put('/:name', this.updateEnv, ADMIN);
this.delete('/:name', this.deleteEnv, ADMIN);
this.post('/:name/on', this.toggleEnvironmentOn, ADMIN);
this.post('/:name/off', this.toggleEnvironmentOff, ADMIN);
}
async getAll(req: Request, res: Response): Promise<void> {
@ -35,12 +35,30 @@ export class EnvironmentsController extends Controller {
res.status(200).json({ version: 1, environments });
}
async createEnv(
req: Request<any, any, IEnvironment, any>,
async updateSortOrder(
req: Request<any, any, ISortOrder, any>,
res: Response,
): Promise<void> {
const environment = await this.service.create(req.body);
res.status(201).json(environment);
await this.service.updateSortOrder(req.body);
res.status(200).end();
}
async toggleEnvironmentOn(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, true);
res.status(204).end();
}
async toggleEnvironmentOff(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, false);
res.status(204).end();
}
async getEnv(
@ -48,25 +66,8 @@ export class EnvironmentsController extends Controller {
res: Response,
): Promise<void> {
const { name } = req.params;
const env = await this.service.get(name);
res.status(200).json(env);
}
async updateEnv(
req: Request<EnvironmentParam, any, IEnvironment, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
const env = await this.service.update(name, req.body);
res.status(200).json(env);
}
async deleteEnv(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.delete(name);
res.status(200).end();
}
}

View File

@ -14,7 +14,7 @@ import { createDb } from './db/db-pool';
import sessionDb from './middleware/session-db';
// Types
import { IUnleash } from './types/core';
import { IUnleashConfig, IUnleashOptions } from './types/option';
import { IUnleashConfig, IUnleashOptions, IAuthType } from './types/option';
import { IUnleashServices } from './types/services';
import User, { IUser } from './types/user';
import { Logger, LogLevel } from './logger';
@ -160,6 +160,7 @@ export {
User,
LogLevel,
RoleName,
IAuthType,
};
export default {

View File

@ -1,10 +1,10 @@
import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
import { Logger } from '../logger';
import { IEnvironment } from '../types/model';
import { IEnvironment, ISortOrder } from '../types/model';
import { UNIQUE_CONSTRAINT_VIOLATION } from '../error/db-error';
import NameExistsError from '../error/name-exists-error';
import { environmentSchema } from './state-schema';
import { sortOrderSchema } from './state-schema';
import NotFoundError from '../error/notfound-error';
import { IEnvironmentStore } from '../types/stores/environment-store';
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
@ -46,22 +46,20 @@ export default class EnvironmentService {
return this.environmentStore.get(name);
}
async delete(name: string): Promise<void> {
return this.environmentStore.delete(name);
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
await sortOrderSchema.validateAsync(sortOrder);
await Promise.all(
Object.keys(sortOrder).map((key) => {
const value = sortOrder[key];
return this.environmentStore.updateSortOrder(key, value);
}),
);
}
async create(env: IEnvironment): Promise<IEnvironment> {
await environmentSchema.validateAsync(env);
return this.environmentStore.upsert(env);
}
async update(
name: string,
env: Pick<IEnvironment, 'displayName'>,
): Promise<IEnvironment> {
async toggleEnvironment(name: string, value: boolean): Promise<void> {
const exists = await this.environmentStore.exists(name);
if (exists) {
return this.environmentStore.upsert({ ...env, name });
return this.environmentStore.updateProperty(name, 'enabled', value);
}
throw new NotFoundError(`Could not find environment ${name}`);
}

View File

@ -28,8 +28,17 @@ export const featureEnvironmentsSchema = joi.object().keys({
export const environmentSchema = joi.object().keys({
name: nameType.allow(':global:'),
displayName: joi.string().optional().allow(''),
type: joi.string().required(),
});
export const updateEnvironmentSchema = joi.object().keys({
displayName: joi.string().optional().allow(''),
type: joi.string().optional(),
sortOrder: joi.number().optional(),
});
export const sortOrderSchema = joi.object().pattern(/^/, joi.number());
export const stateSchema = joi.object().keys({
version: joi.number(),
features: joi.array().optional().items(featureSchema),

View File

@ -479,13 +479,15 @@ test('exporting to new format works', async () => {
name: 'extra',
description: 'No surprises here',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'dev',
displayName: 'Development',
type: 'development',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'prod',
displayName: 'Production',
type: 'production',
});
await stores.featureToggleStore.create('fancy', {
name: 'Some-feature',
@ -519,13 +521,15 @@ test('featureStrategies can keep existing', async () => {
name: 'extra',
description: 'No surprises here',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'dev',
displayName: 'Development',
type: 'development',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'prod',
displayName: 'Production',
type: 'production',
});
await stores.featureToggleStore.create('fancy', {
name: 'Some-feature',
@ -565,13 +569,15 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
name: 'extra',
description: 'No surprises here',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'dev',
displayName: 'Development',
type: 'development',
});
await stores.environmentStore.upsert({
await stores.environmentStore.create({
name: 'prod',
displayName: 'Production',
type: 'production',
});
await stores.featureToggleStore.create('fancy', {
name: 'Some-feature',

View File

@ -74,6 +74,10 @@ export interface IEnvironmentDetail extends IEnvironmentOverview {
strategies: IStrategyConfig[];
}
export interface ISortOrder {
[index: string]: number;
}
export interface IFeatureEnvironment {
environment: string;
featureName: string;
@ -98,6 +102,16 @@ export interface IVariant {
export interface IEnvironment {
name: string;
displayName: string;
type: string;
sortOrder: number;
enabled: boolean;
protected: boolean;
}
export interface IEnvironmentCreate {
name: string;
displayName: string;
type: string;
}
export interface IEnvironmentOverview {

View File

@ -1,6 +1,17 @@
import { IEnvironment } from '../model';
import { IEnvironment, IEnvironmentCreate } from '../model';
import { Store } from './store';
export interface IEnvironmentStore extends Store<IEnvironment, string> {
upsert(env: IEnvironment): Promise<IEnvironment>;
exists(name: string): Promise<boolean>;
create(env: IEnvironmentCreate): Promise<IEnvironment>;
update(
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
name: string,
): Promise<IEnvironment>;
updateProperty(
id: string,
field: string,
value: string | number | boolean,
): Promise<void>;
updateSortOrder(id: string, value: number): Promise<void>;
}

View File

@ -0,0 +1,25 @@
import { snakeCase, snakeCaseKeys } from './snakeCase';
test('should return snake case from camelCase', () => {
const resultOne = snakeCase('camelCase');
const resultTwo = snakeCase('SnaejKase');
expect(resultOne).toBe('camel_case');
expect(resultTwo).toBe('snaej_kase');
});
test('should return object with snake case keys', () => {
const input = {
sortOrder: 1,
type: 'production',
displayName: 'dev',
enabled: true,
};
const output = snakeCaseKeys(input);
expect(output.sort_order).toBe(1);
expect(output.type).toBe('production');
expect(output.display_name).toBe('dev');
expect(output.enabled).toBe(true);
});

27
src/lib/util/snakeCase.ts Normal file
View File

@ -0,0 +1,27 @@
export const snakeCase = (input: string): string => {
const result = [];
const splitString = input.split('');
for (let i = 0; i < splitString.length; i++) {
const char = splitString[i];
if (i !== 0 && char.toLocaleUpperCase() === char) {
result.push('_', char.toLocaleLowerCase());
} else {
result.push(char.toLocaleLowerCase());
}
}
return result.join('');
};
export const snakeCaseKeys = (obj: {
[index: string]: any;
}): { [index: string]: any } => {
const objResult: { [index: string]: any } = {};
Object.keys(obj).forEach((key) => {
const snakeCaseKey = snakeCase(key);
objResult[snakeCaseKey] = obj[key];
});
return objResult;
};

View File

@ -0,0 +1,21 @@
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE environments ADD COLUMN sort_order integer DEFAULT 9999, ADD COLUMN type text;
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE environments DROP COLUMN sort_order, DROP COLUMN type;
`,
cb,
);
};
exports._meta = {
version: 1,
};

View File

@ -0,0 +1,21 @@
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE environments ADD COLUMN enabled BOOLEAN DEFAULT true;
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE environments DROP COLUMN enabled;
`,
cb,
);
};
exports._meta = {
version: 1,
};

View File

@ -0,0 +1,22 @@
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE environments ADD COLUMN protected BOOLEAN DEFAULT false;
UPDATE environments SET protected = true WHERE name = ':global:';
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE environments DROP COLUMN protected;
`,
cb,
);
};
exports._meta = {
version: 1,
};

View File

@ -1,9 +1,9 @@
import dbInit from '../../helpers/database-init';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { setupApp } from '../../helpers/test-helper';
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
let app;
let db;
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('environment_api_serial', getLogger);
@ -15,32 +15,6 @@ afterAll(async () => {
await db.destroy();
});
test('Should be able to create an environment', async () => {
const envName = 'environment-info';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
});
test('Environment names must be URL safe', async () => {
const envName = 'Something not url safe **/ */21312';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(400);
});
test('Can list all existing environments', async () => {
await app.request
.get('/api/admin/environments')
@ -51,53 +25,123 @@ test('Can list all existing environments', async () => {
expect(res.body.environments[0]).toStrictEqual({
displayName: 'Across all environments',
name: ':global:',
enabled: true,
sortOrder: 1,
type: 'production',
protected: true,
});
});
});
test('Can delete environment', async () => {
const envName = 'deletable-info';
// Create environment
test('Can update sort order', async () => {
const envName = 'update-sort-order';
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
await app.request
.post('/api/admin/environments')
.put('/api/admin/environments/sort-order')
.send({
name: envName,
displayName: 'Enable feature for environment',
':global:': 2,
[envName]: 1,
})
.set('Content-Type', 'application/json')
.expect(201);
await app.request.get(`/api/admin/environments/${envName}`).expect(200);
await app.request.delete(`/api/admin/environments/${envName}`).expect(200);
await app.request.get(`/api/admin/environments/${envName}`).expect(404);
});
test('Can update environment', async () => {
const envName = 'update-env';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await app.request.get(`/api/admin/environments/${envName}`).expect(200);
await app.request
.put(`/api/admin/environments/${envName}`)
.send({ displayName: 'Update this' })
.expect(200);
await app.request
.get(`/api/admin/environments/${envName}`)
.get('/api/admin/environments')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.displayName).toBe('Update this');
const updatedSort = res.body.environments.find(
(t) => t.name === envName,
);
const global = res.body.environments.find(
(t) => t.name === ':global:',
);
expect(updatedSort.sortOrder).toBe(1);
expect(global.sortOrder).toBe(2);
});
});
test('Updating a non existing environment yields 404', async () => {
const envName = 'non-existing-env';
test('Sort order will fail on wrong data format', async () => {
const envName = 'sort-order-env';
await app.request
.put(`/api/admin/environments/${envName}`)
.send({ displayName: 'Update this' })
.put('/api/admin/environments/sort-order')
.send({
':global:': 'test',
[envName]: 1,
})
.expect(400);
});
test('Can update environment enabled status', async () => {
const envName = 'enable-environment';
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
await app.request
.post(`/api/admin/environments/${envName}/on`)
.set('Content-Type', 'application/json')
.expect(204);
});
test('Can update environment disabled status', async () => {
const envName = 'disable-environment';
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
await app.request
.post(`/api/admin/environments/${envName}/off`)
.set('Content-Type', 'application/json')
.expect(204);
});
test('Can not update non-existing environment enabled status', async () => {
const envName = 'non-existing-env';
await app.request
.post(`/api/admin/environments/${envName}/on`)
.set('Content-Type', 'application/json')
.expect(404);
});
test('Can not update non-existing environment disabled status', async () => {
const envName = 'non-existing-env';
await app.request
.post(`/api/admin/environments/${envName}/off`)
.set('Content-Type', 'application/json')
.expect(404);
});
test('Can get specific environment', async () => {
const envName = 'get-specific';
await db.stores.environmentStore.create({
name: envName,
type: 'production',
displayName: 'Fun!',
});
await app.request
.get(`/api/admin/environments/${envName}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
const { body } = res;
expect(body.name).toBe(envName);
expect(body.type).toBe('production');
});
});
test('Getting a non existing environment yields 404', async () => {
await app.request
.get('/api/admin/environments/this-does-not-exist')
.expect(404);
});

View File

@ -32,11 +32,12 @@ afterAll(async () => {
});
test('Should add environment to project', async () => {
await app.request
.post('/api/admin/environments')
.send({ name: 'test', displayName: 'Test Env' })
.set('Content-Type', 'application/json')
.expect(201);
// Endpoint to create env does not exists anymore
await db.stores.environmentStore.create({
name: 'test',
displayName: 'Test Env',
type: 'test',
});
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: 'test' })
@ -59,13 +60,17 @@ test('Should validate environment', async () => {
.expect(400);
});
test('Should remove environment to project', async () => {
test('Should remove environment from project', async () => {
const name = 'test-delete';
await app.request
.post('/api/admin/environments')
.send({ name, displayName: 'Test Env' })
.set('Content-Type', 'application/json')
.expect(201);
// Endpoint to create env does not exists anymore
await db.stores.environmentStore.create({
name,
displayName: 'Test Env',
type: 'test',
});
// Endpoint to delete project does not exist anymore
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: name })

View File

@ -126,11 +126,11 @@ test('Project overview includes environment connected to feature', async () => {
expect(res.body.name).toBe('com.test.environment');
expect(res.body.createdAt).toBeTruthy();
});
await app.request
.post('/api/admin/environments')
.send({ name: 'project-overview', displayName: 'Project Overview' })
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: 'project-overview',
displayName: 'Project Overview',
type: 'production',
});
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: 'project-overview' })
@ -160,11 +160,11 @@ test('Disconnecting environment from project, removes environment from features
expect(res.body.name).toBe('com.test.disconnect.environment');
expect(res.body.createdAt).toBeTruthy();
});
await app.request
.post('/api/admin/environments')
.send({ name: 'dis-project-overview', displayName: 'Project Overview' })
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: 'dis-project-overview',
displayName: 'Project Overview',
type: 'production',
});
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: 'dis-project-overview' })
@ -188,14 +188,11 @@ test('Can enable/disable environment for feature with strategies', async () => {
const envName = 'enable-feature-environment';
const featureName = 'com.test.enable.environment';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -341,14 +338,11 @@ test('Trying to create toggle under project that does not exist should fail', as
test('Can get environment info for feature toggle', async () => {
const envName = 'environment-info';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -508,14 +502,11 @@ test('Can add strategy to feature toggle to default env', async () => {
const envName = 'default';
const featureName = 'feature.strategy.toggle';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -550,14 +541,11 @@ test('Can add strategy to feature toggle to default env', async () => {
test('Can get strategies for feature and environment', async () => {
const envName = 'get-strategy';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -608,14 +596,11 @@ test('Getting strategies for environment that does not exist yields 404', async
test('Can update a strategy based on id', async () => {
const envName = 'feature.update.strategies';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -664,14 +649,11 @@ test('Can update a strategy based on id', async () => {
test('Trying to update a non existing feature strategy should yield 404', async () => {
const envName = 'feature.non.existing.strategy';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -698,14 +680,11 @@ test('Can patch a strategy based on id', async () => {
const featureName = 'feature.patch.strategies';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'test',
});
// Connect environment to project
await app.request
.post(`${BASE_URI}/environments`)
@ -754,14 +733,11 @@ test('Can patch a strategy based on id', async () => {
test('Trying to get a non existing feature strategy should yield 404', async () => {
const envName = 'feature.non.existing.strategy.get';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'production',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -786,14 +762,11 @@ test('Can not enable environment for feature without strategies', async () => {
const featureName = 'com.test.enable.environment.disabled';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: environment,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: environment,
displayName: 'Enable feature for environment',
type: 'test',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')
@ -830,14 +803,11 @@ test('Can delete strategy from feature toggle', async () => {
const envName = 'del-strategy';
const featureName = 'feature.strategy.toggle.delete.strategy';
// Create environment
await app.request
.post('/api/admin/environments')
.send({
name: envName,
displayName: 'Enable feature for environment',
})
.set('Content-Type', 'application/json')
.expect(201);
await db.stores.environmentStore.create({
name: envName,
displayName: 'Enable feature for environment',
type: 'test',
});
// Connect environment to project
await app.request
.post('/api/admin/projects/default/environments')

View File

@ -172,9 +172,10 @@ test('Can get strategies for specific environment', async () => {
// create new env
await db.stores.environmentStore.upsert({
await db.stores.environmentStore.create({
name: 'testing',
displayName: 'simple test',
type: 'test',
});
await app.services.environmentService.addEnvironmentToProject(

View File

@ -58,7 +58,7 @@ async function connectProject(store: IFeatureEnvironmentStore): Promise<void> {
}
async function createEnvironments(store: EnvironmentStore): Promise<void> {
await Promise.all(dbState.environments.map(async (e) => store.upsert(e)));
await Promise.all(dbState.environments.map(async (e) => store.create(e)));
}
async function setupDatabase(stores) {

View File

@ -30,7 +30,11 @@
"environments": [
{
"name": ":global:",
"displayName": "Across all environments"
"displayName": "Across all environments",
"type": "production",
"sortOrder": 1,
"enabled": true,
"protected": true
}
],
"tag_types": [

View File

@ -19,50 +19,34 @@ afterAll(async () => {
await db.destroy();
});
test('Can create and get environment', async () => {
const created = await service.create({
test('Can get environment', async () => {
const created = await db.stores.environmentStore.create({
name: 'testenv',
displayName: 'Environment for testing',
type: 'production',
});
const retrieved = await service.get('testenv');
expect(retrieved).toEqual(created);
});
test('Can delete environment', async () => {
await service.create({
name: 'testenv',
displayName: 'Environment for testing',
});
await service.delete('testenv');
return expect(async () => service.get('testenv')).rejects.toThrow(
NotFoundError,
);
});
test('Can get all', async () => {
await service.create({
name: 'testenv',
await db.stores.environmentStore.create({
name: 'testenv2',
displayName: 'Environment for testing',
type: 'production',
});
const environments = await service.getAll();
expect(environments).toHaveLength(2); // the one we created plus ':global:'
});
test('Can update display name', async () => {
await service.create({
name: 'testenv',
displayName: 'Environment for testing',
});
await service.update('testenv', { displayName: 'Different name' });
const updated = await service.get('testenv');
expect(updated.displayName).toEqual('Different name');
expect(environments).toHaveLength(3); // the one we created plus ':global:'
});
test('Can connect environment to project', async () => {
await service.create({ name: 'test-connection', displayName: '' });
await db.stores.environmentStore.create({
name: 'test-connection',
displayName: '',
type: 'production',
});
await stores.featureToggleStore.create('default', {
name: 'test-connection',
type: 'release',
@ -87,7 +71,11 @@ test('Can connect environment to project', async () => {
});
test('Can remove environment from project', async () => {
await service.create({ name: 'removal-test', displayName: '' });
await db.stores.environmentStore.create({
name: 'removal-test',
displayName: '',
type: 'production',
});
await stores.featureToggleStore.create('default', {
name: 'removal-test',
});
@ -119,7 +107,11 @@ test('Can remove environment from project', async () => {
});
test('Adding same environment twice should throw a NameExistsError', async () => {
await service.create({ name: 'uniqueness-test', displayName: '' });
await db.stores.environmentStore.create({
name: 'uniqueness-test',
displayName: '',
type: 'production',
});
await service.removeEnvironmentFromProject('test-connection', 'default');
await service.removeEnvironmentFromProject('removal-test', 'default');
@ -140,3 +132,10 @@ test('Removing environment not connected to project should be a noop', async ()
'default',
),
).resolves);
test('Trying to get an environment that does not exist throws NotFoundError', async () => {
const envName = 'this-should-not-exist';
await expect(async () => service.get(envName)).rejects.toThrow(
new NotFoundError(`Could not find environment with name: ${envName}`),
);
});

View File

@ -121,7 +121,11 @@ test('should add environment to project', async () => {
description: 'Blah',
};
await environmentStore.upsert({ name: 'test', displayName: 'Test Env' });
await environmentStore.create({
name: 'test',
displayName: 'Test Env',
type: 'production',
});
await projectStore.create(project);
await projectStore.addEnvironmentToProject(project.id, 'test');

View File

@ -23,7 +23,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
);
}
async upsert(env: IEnvironment): Promise<IEnvironment> {
async create(env: IEnvironment): Promise<IEnvironment> {
this.environments = this.environments.filter(
(e) => e.name !== env.name,
);
@ -31,6 +31,60 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
return Promise.resolve(env);
}
async update(
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
name: string,
): Promise<IEnvironment> {
const found = this.environments.find(
(en: IEnvironment) => en.name === name,
);
const idx = this.environments.findIndex(
(en: IEnvironment) => en.name === name,
);
const updated = { ...found, env };
this.environments[idx] = updated;
return Promise.resolve(updated);
}
async updateSortOrder(id: string, value: number): Promise<void> {
const environment = this.environments.find(
(env: IEnvironment) => env.name === id,
);
environment.sortOrder = value;
return Promise.resolve();
}
async updateProperty(
id: string,
field: string,
value: string | number,
): Promise<void> {
const environment = this.environments.find(
(env: IEnvironment) => env.name === id,
);
environment[field] = value;
return Promise.resolve();
}
async connectProject(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
environment: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: string,
): Promise<void> {
return Promise.reject(new Error('Not implemented'));
}
async connectFeatures(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
environment: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: string,
): Promise<void> {
return Promise.reject(new Error('Not implemented'));
}
async delete(name: string): Promise<void> {
this.environments = this.environments.filter((e) => e.name !== name);
return Promise.resolve();