mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: Add technical support for projects
This commit is contained in:
parent
144e832cdc
commit
b644071a34
@ -36,6 +36,7 @@ module.exports = function(config) {
|
||||
app.use(responseTime(config));
|
||||
app.use(requestLogger(config));
|
||||
app.use(secureHeaders(config));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
if (config.publicFolder) {
|
||||
app.use(favicon(path.join(config.publicFolder, 'favicon.ico')));
|
||||
|
@ -16,6 +16,7 @@ const FEATURE_COLUMNS = [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'project',
|
||||
'enabled',
|
||||
'stale',
|
||||
'strategies',
|
||||
@ -65,6 +66,14 @@ class FeatureToggleStore {
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
async getFeaturesBy(fields) {
|
||||
const rows = await this.db
|
||||
.select(FEATURE_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where(fields);
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
async count() {
|
||||
return this.db
|
||||
.count('*')
|
||||
@ -114,6 +123,7 @@ class FeatureToggleStore {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
project: row.project,
|
||||
enabled: row.enabled > 0,
|
||||
stale: row.stale,
|
||||
strategies: row.strategies,
|
||||
@ -127,6 +137,7 @@ class FeatureToggleStore {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
project: data.project,
|
||||
enabled: data.enabled ? 1 : 0,
|
||||
stale: data.stale,
|
||||
archived: data.archived ? 1 : 0,
|
||||
|
@ -12,6 +12,7 @@ const ClientApplicationsStore = require('./client-applications-store');
|
||||
const ContextFieldStore = require('./context-field-store');
|
||||
const SettingStore = require('./setting-store');
|
||||
const UserStore = require('./user-store');
|
||||
const ProjectStore = require('./project-store');
|
||||
|
||||
module.exports.createStores = (config, eventBus) => {
|
||||
const { getLogger } = config;
|
||||
@ -49,5 +50,6 @@ module.exports.createStores = (config, eventBus) => {
|
||||
),
|
||||
settingStore: new SettingStore(db, getLogger),
|
||||
userStore: new UserStore(db, getLogger),
|
||||
projectStore: new ProjectStore(db, getLogger),
|
||||
};
|
||||
};
|
||||
|
95
lib/db/project-store.js
Normal file
95
lib/db/project-store.js
Normal file
@ -0,0 +1,95 @@
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'created_at'];
|
||||
const TABLE = 'projects';
|
||||
|
||||
class ProjectStore {
|
||||
constructor(db, getLogger) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('project-store.js');
|
||||
}
|
||||
|
||||
fieldToRow(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
};
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
return rows.map(this.mapRow);
|
||||
}
|
||||
|
||||
async get(id) {
|
||||
return this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ id })
|
||||
.then(this.mapRow);
|
||||
}
|
||||
|
||||
async hasProject(id) {
|
||||
return this.db
|
||||
.first('id')
|
||||
.from(TABLE)
|
||||
.where({ id })
|
||||
.then(row => {
|
||||
if (!row) {
|
||||
throw new NotFoundError(`No project with id=${id} found`);
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
archived: row.archived === 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async create(project) {
|
||||
await this.db(TABLE)
|
||||
.insert(this.fieldToRow(project))
|
||||
.catch(err =>
|
||||
this.logger.error('Could not insert project, error: ', err),
|
||||
);
|
||||
}
|
||||
|
||||
async update(data) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ id: data.id })
|
||||
.update(this.fieldToRow(data));
|
||||
} catch (err) {
|
||||
this.logger.error('Could not update project, error: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ id })
|
||||
.del();
|
||||
} catch (err) {
|
||||
this.logger.error('Could not delete project, error: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
mapRow(row) {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No project found');
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectStore;
|
26
lib/error/invalid-operation-error.js
Normal file
26
lib/error/invalid-operation-error.js
Normal file
@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
class InvalidOperationError extends Error {
|
||||
constructor(message) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
isJoi: true,
|
||||
name: this.constructor.name,
|
||||
details: [
|
||||
{
|
||||
message: this.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InvalidOperationError;
|
@ -18,6 +18,9 @@ const {
|
||||
CONTEXT_FIELD_CREATED,
|
||||
CONTEXT_FIELD_UPDATED,
|
||||
CONTEXT_FIELD_DELETED,
|
||||
PROJECT_CREATED,
|
||||
PROJECT_UPDATED,
|
||||
PROJECT_DELETED,
|
||||
} = require('./event-type');
|
||||
|
||||
const strategyTypes = [
|
||||
@ -43,6 +46,8 @@ const contextTypes = [
|
||||
CONTEXT_FIELD_UPDATED,
|
||||
];
|
||||
|
||||
const projectTypes = [PROJECT_CREATED, PROJECT_UPDATED, PROJECT_DELETED];
|
||||
|
||||
function baseTypeFor(event) {
|
||||
if (featureTypes.indexOf(event.type) !== -1) {
|
||||
return 'features';
|
||||
@ -53,6 +58,9 @@ function baseTypeFor(event) {
|
||||
if (contextTypes.indexOf(event.type) !== -1) {
|
||||
return 'context';
|
||||
}
|
||||
if (projectTypes.indexOf(event.type) !== -1) {
|
||||
return 'project';
|
||||
}
|
||||
throw new Error(`unknown event type: ${JSON.stringify(event)}`);
|
||||
}
|
||||
|
||||
|
@ -15,4 +15,7 @@ module.exports = {
|
||||
CONTEXT_FIELD_CREATED: 'context-field-created',
|
||||
CONTEXT_FIELD_UPDATED: 'context-field-updated',
|
||||
CONTEXT_FIELD_DELETED: 'context-field-deleted',
|
||||
PROJECT_CREATED: 'project-created',
|
||||
PROJECT_UPDATED: 'project-updated',
|
||||
PROJECT_DELETED: 'project-deleted',
|
||||
};
|
||||
|
@ -12,6 +12,9 @@ const UPDATE_APPLICATION = 'UPDATE_APPLICATION';
|
||||
const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD';
|
||||
const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
|
||||
const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
|
||||
const CREATE_PROJECT = 'CREATE_PROJECT';
|
||||
const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
||||
const DELETE_PROJECT = 'DELETE_PROJECT';
|
||||
|
||||
module.exports = {
|
||||
ADMIN,
|
||||
@ -26,4 +29,7 @@ module.exports = {
|
||||
CREATE_CONTEXT_FIELD,
|
||||
UPDATE_CONTEXT_FIELD,
|
||||
DELETE_CONTEXT_FIELD,
|
||||
CREATE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
DELETE_PROJECT,
|
||||
};
|
||||
|
@ -65,6 +65,7 @@ const featureShema = joi
|
||||
enabled: joi.boolean().default(false),
|
||||
stale: joi.boolean().default(false),
|
||||
type: joi.string().default('release'),
|
||||
project: joi.string().default('default'),
|
||||
description: joi
|
||||
.string()
|
||||
.allow('')
|
||||
|
@ -17,14 +17,12 @@ test('should require URL firendly name', t => {
|
||||
test('should be valid toggle name', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
stale: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
};
|
||||
|
||||
const { value } = featureShema.validate(toggle);
|
||||
t.deepEqual(value, toggle);
|
||||
t.is(value.name, toggle.name);
|
||||
});
|
||||
|
||||
test('should strip extra variant fields', t => {
|
||||
@ -52,6 +50,7 @@ test('should allow weightType=fix', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
project: 'default',
|
||||
enabled: false,
|
||||
stale: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -95,6 +94,7 @@ test('should be possible to define variant overrides', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
project: 'some',
|
||||
enabled: false,
|
||||
stale: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -152,6 +152,7 @@ test('should keep constraints', t => {
|
||||
const toggle = {
|
||||
name: 'app.constraints',
|
||||
type: 'release',
|
||||
project: 'default',
|
||||
enabled: false,
|
||||
stale: false,
|
||||
strategies: [
|
||||
|
@ -31,6 +31,7 @@ const handleErrors = (res, logger, error) => {
|
||||
switch (error.name) {
|
||||
case 'NotFoundError':
|
||||
return res.status(404).end();
|
||||
case 'InvalidOperationError':
|
||||
case 'NameExistsError':
|
||||
return res
|
||||
.status(409)
|
||||
|
@ -7,18 +7,20 @@ const getApp = require('./app');
|
||||
|
||||
const { startMonitoring } = require('./metrics');
|
||||
const { createStores } = require('./db');
|
||||
const { createServices } = require('./services');
|
||||
const { createOptions } = require('./options');
|
||||
const StateService = require('./services/state-service');
|
||||
const User = require('./user');
|
||||
const permissions = require('./permissions');
|
||||
const AuthenticationRequired = require('./authentication-required');
|
||||
const { addEventHook } = require('./event-hook');
|
||||
const eventType = require('./event-type');
|
||||
|
||||
async function createApp(options) {
|
||||
// Database dependencies (stateful)
|
||||
const logger = options.getLogger('server-impl.js');
|
||||
const eventBus = new EventEmitter();
|
||||
const stores = createStores(options, eventBus);
|
||||
const services = createServices(stores, options, eventBus);
|
||||
const secret = await stores.settingStore.get('unleash.secret');
|
||||
|
||||
const config = {
|
||||
@ -37,7 +39,8 @@ async function createApp(options) {
|
||||
addEventHook(config.eventHook, stores.eventStore);
|
||||
}
|
||||
|
||||
const stateService = new StateService(config);
|
||||
// TODO: refactor this. Should only be accessable via services object
|
||||
const { stateService } = services;
|
||||
config.stateService = stateService;
|
||||
if (config.importFile) {
|
||||
await stateService.importFile({
|
||||
@ -53,6 +56,7 @@ async function createApp(options) {
|
||||
app,
|
||||
config,
|
||||
stores,
|
||||
services,
|
||||
eventBus,
|
||||
stateService,
|
||||
};
|
||||
@ -101,4 +105,5 @@ module.exports = {
|
||||
User,
|
||||
AuthenticationRequired,
|
||||
permissions,
|
||||
eventType,
|
||||
};
|
||||
|
7
lib/services/index.js
Normal file
7
lib/services/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
const ProjectService = require('./project-service');
|
||||
const StateService = require('./state-service');
|
||||
|
||||
module.exports.createServices = (stores, config) => ({
|
||||
projectService: new ProjectService(stores, config),
|
||||
stateService: new StateService(stores, config),
|
||||
});
|
17
lib/services/project-schema.js
Normal file
17
lib/services/project-schema.js
Normal file
@ -0,0 +1,17 @@
|
||||
const joi = require('joi');
|
||||
const { nameType } = require('../routes/admin-api/util');
|
||||
|
||||
const projectSchema = joi
|
||||
.object()
|
||||
.keys({
|
||||
id: nameType,
|
||||
name: joi.string().required(),
|
||||
description: joi
|
||||
.string()
|
||||
.allow(null)
|
||||
.allow('')
|
||||
.optional(),
|
||||
})
|
||||
.options({ allowUnknown: false, stripUnknown: true });
|
||||
|
||||
module.exports = projectSchema;
|
93
lib/services/project-service.js
Normal file
93
lib/services/project-service.js
Normal file
@ -0,0 +1,93 @@
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const InvalidOperationError = require('../error/invalid-operation-error');
|
||||
const eventType = require('../event-type');
|
||||
const { nameType } = require('../routes/admin-api/util');
|
||||
const schema = require('./project-schema');
|
||||
|
||||
class ProjectService {
|
||||
constructor(
|
||||
{ projectStore, eventStore, featureToggleStore },
|
||||
{ getLogger },
|
||||
) {
|
||||
this.projectStore = projectStore;
|
||||
this.eventStore = eventStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.logger = getLogger('services/project-service.js');
|
||||
}
|
||||
|
||||
async getProjects() {
|
||||
return this.projectStore.getAll();
|
||||
}
|
||||
|
||||
async getProject(id) {
|
||||
return this.projectStore.get(id);
|
||||
}
|
||||
|
||||
async createProject(newProject, username) {
|
||||
const data = await schema.validateAsync(newProject);
|
||||
await this.validateUniqueId(data);
|
||||
await this.eventStore.store({
|
||||
type: eventType.PROJECT_CREATED,
|
||||
createdBy: username,
|
||||
data,
|
||||
});
|
||||
await this.projectStore.create(data);
|
||||
}
|
||||
|
||||
async updateProject(updatedProject, username) {
|
||||
await this.projectStore.get(updatedProject.id);
|
||||
const project = await schema.validateAsync(updatedProject);
|
||||
await this.eventStore.store({
|
||||
type: eventType.PROJECT_UPDATED,
|
||||
createdBy: username,
|
||||
data: project,
|
||||
});
|
||||
await this.projectStore.update(project);
|
||||
}
|
||||
|
||||
async deleteProject(id, username) {
|
||||
if (id === 'default') {
|
||||
throw new InvalidOperationError(
|
||||
'You can not delete the default project!',
|
||||
);
|
||||
}
|
||||
|
||||
const toggles = await this.featureToggleStore.getFeaturesBy({
|
||||
project: id,
|
||||
archived: 0,
|
||||
});
|
||||
|
||||
if (toggles.length > 0) {
|
||||
throw new InvalidOperationError(
|
||||
'You can not delete as project with active feature toggles',
|
||||
);
|
||||
}
|
||||
|
||||
await this.eventStore.store({
|
||||
type: eventType.PROJECT_DELETED,
|
||||
createdBy: username,
|
||||
data: { id },
|
||||
});
|
||||
await this.projectStore.delete(id);
|
||||
}
|
||||
|
||||
async validateId(id) {
|
||||
await nameType.validateAsync(id);
|
||||
await this.validateUniqueId(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async validateUniqueId(id) {
|
||||
try {
|
||||
await this.projectStore.hasProject(id);
|
||||
} catch (error) {
|
||||
// No conflict, everything ok!
|
||||
return;
|
||||
}
|
||||
|
||||
// Interntional throw here!
|
||||
throw new NameExistsError('A project with this id already exists.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectService;
|
@ -14,7 +14,7 @@ const {
|
||||
} = require('./state-util');
|
||||
|
||||
class StateService {
|
||||
constructor({ stores, getLogger }) {
|
||||
constructor(stores, { getLogger }) {
|
||||
this.eventStore = stores.eventStore;
|
||||
this.toggleStore = stores.featureToggleStore;
|
||||
this.strategyStore = stores.strategyStore;
|
||||
|
@ -15,7 +15,10 @@ const {
|
||||
|
||||
function getSetup() {
|
||||
const stores = store.createStores();
|
||||
return { stateService: new StateService({ stores, getLogger }), stores };
|
||||
return {
|
||||
stateService: new StateService(stores, { getLogger }),
|
||||
stores,
|
||||
};
|
||||
}
|
||||
|
||||
test('should import a feature', async t => {
|
||||
|
34
migrations/20200928194947-add-projects.js
Normal file
34
migrations/20200928194947-add-projects.js
Normal file
@ -0,0 +1,34 @@
|
||||
/* eslint camelcase: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, cb) {
|
||||
async.series(
|
||||
[
|
||||
db.createTable.bind(db, 'projects', {
|
||||
id: {
|
||||
type: 'string',
|
||||
length: 255,
|
||||
primaryKey: true,
|
||||
notNull: true,
|
||||
},
|
||||
name: { type: 'string', notNull: true },
|
||||
description: { type: 'string' },
|
||||
created_at: { type: 'timestamp', defaultValue: 'now()' },
|
||||
}),
|
||||
db.runSql.bind(
|
||||
db,
|
||||
`
|
||||
INSERT INTO projects(id, name, description) VALUES('default', 'Default', 'Default project');
|
||||
`,
|
||||
),
|
||||
],
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function(db, cb) {
|
||||
return db.dropTable('feature_types', cb);
|
||||
};
|
17
migrations/20200928195238-add-project-id-to-features.js
Normal file
17
migrations/20200928195238-add-project-id-to-features.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, cb) {
|
||||
return db.addColumn(
|
||||
'features',
|
||||
'project',
|
||||
{
|
||||
type: 'string',
|
||||
defaultValue: 'default',
|
||||
},
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function(db, cb) {
|
||||
return db.removeColumn('features', 'project', cb);
|
||||
};
|
@ -75,7 +75,7 @@
|
||||
"express": "^4.17.1",
|
||||
"gravatar-url": "^3.1.0",
|
||||
"helmet": "^4.1.0",
|
||||
"joi": "^17.2.0",
|
||||
"joi": "^17.3.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"knex": "0.21.5",
|
||||
"log4js": "^6.0.0",
|
||||
|
@ -24,6 +24,7 @@ async function resetDatabase(stores) {
|
||||
stores.db('client_instances').del(),
|
||||
stores.db('context_fields').del(),
|
||||
stores.db('users').del(),
|
||||
stores.db('projects').del(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -43,6 +44,10 @@ function createClientInstance(store) {
|
||||
return dbState.clientInstances.map(i => store.insert(i));
|
||||
}
|
||||
|
||||
function createProjects(store) {
|
||||
return dbState.projects.map(i => store.create(i));
|
||||
}
|
||||
|
||||
function createFeatures(store) {
|
||||
return dbState.features.map(f => store._createFeature(f));
|
||||
}
|
||||
@ -54,6 +59,7 @@ async function setupDatabase(stores) {
|
||||
updates.push(...createFeatures(stores.featureToggleStore));
|
||||
updates.push(...createClientInstance(stores.clientInstanceStore));
|
||||
updates.push(...createApplications(stores.clientApplicationsStore));
|
||||
updates.push(...createProjects(stores.projectStore));
|
||||
|
||||
await Promise.all(updates);
|
||||
}
|
||||
|
@ -149,5 +149,11 @@
|
||||
{ "name": "new", "weight": 50 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"projects": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ function createApp(stores, adminAuthentication = 'none', preHook) {
|
||||
adminAuthentication,
|
||||
secret: 'super-secret',
|
||||
sessionAge: 4000,
|
||||
stateService: new StateService({ stores, getLogger }),
|
||||
stateService: new StateService(stores, { getLogger }),
|
||||
getLogger,
|
||||
});
|
||||
}
|
||||
|
148
test/e2e/serices/project-service.e2e.test.js
Normal file
148
test/e2e/serices/project-service.e2e.test.js
Normal file
@ -0,0 +1,148 @@
|
||||
const test = require('ava');
|
||||
const dbInit = require('../helpers/database-init');
|
||||
const getLogger = require('../../fixtures/no-logger');
|
||||
const ProjectService = require('../../../lib/services/project-service');
|
||||
|
||||
let stores;
|
||||
// let projectStore;
|
||||
let projectService;
|
||||
|
||||
test.before(async () => {
|
||||
const db = await dbInit('project_service_serial', getLogger);
|
||||
stores = db.stores;
|
||||
// projectStore = stores.projectStore;
|
||||
projectService = new ProjectService(stores, { getLogger });
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await stores.db.destroy();
|
||||
});
|
||||
|
||||
test.serial('should have default project', async t => {
|
||||
const project = await projectService.getProject('default');
|
||||
t.assert(project);
|
||||
t.is(project.id, 'default');
|
||||
});
|
||||
|
||||
test.serial('should list all projects', async t => {
|
||||
const project = {
|
||||
id: 'test-list',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, 'someUser');
|
||||
const projects = await projectService.getProjects();
|
||||
t.is(projects.length, 2);
|
||||
});
|
||||
|
||||
test.serial('should create new project', async t => {
|
||||
const project = {
|
||||
id: 'test',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, 'someUser');
|
||||
const ret = await projectService.getProject('test');
|
||||
t.deepEqual(project.id, ret.id);
|
||||
t.deepEqual(project.name, ret.name);
|
||||
t.deepEqual(project.description, ret.description);
|
||||
t.truthy(ret.createdAt);
|
||||
});
|
||||
|
||||
test.serial('should delete project', async t => {
|
||||
const project = {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, 'some-user');
|
||||
await projectService.deleteProject(project.id, 'some-user');
|
||||
|
||||
try {
|
||||
await projectService.getProject(project.id);
|
||||
} catch (err) {
|
||||
t.is(err.message, 'No project found');
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should not be able to delete project with toggles', async t => {
|
||||
const project = {
|
||||
id: 'test-delete-with-toggles',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, 'some-user');
|
||||
await stores.featureToggleStore._createFeature({
|
||||
name: 'test-project-delete',
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await projectService.deleteProject(project.id, 'some-user');
|
||||
} catch (err) {
|
||||
t.is(
|
||||
err.message,
|
||||
'You can not delete as project with active feature toggles',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should not delete "default" project', async t => {
|
||||
try {
|
||||
await projectService.deleteProject('default', 'some-user');
|
||||
} catch (err) {
|
||||
t.is(err.message, 'You can not delete the default project!');
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should validate name, legal', async t => {
|
||||
const result = await projectService.validateId('new_name');
|
||||
t.true(result);
|
||||
});
|
||||
|
||||
test.serial('should require URL friendly ID', async t => {
|
||||
try {
|
||||
await projectService.validateId('new name øæå');
|
||||
} catch (err) {
|
||||
t.is(err.message, '"value" must be URL friendly');
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should require unique ID', async t => {
|
||||
try {
|
||||
await projectService.validateId('default');
|
||||
} catch (err) {
|
||||
t.is(err.message, 'A project with this id already exists.');
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should update project', async t => {
|
||||
const project = {
|
||||
id: 'test-update',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const updatedProject = {
|
||||
id: 'test-update',
|
||||
name: 'New name',
|
||||
description: 'Blah longer desc',
|
||||
};
|
||||
|
||||
await projectService.createProject(project, 'some-user');
|
||||
await projectService.updateProject(updatedProject, 'some-user');
|
||||
|
||||
const readProject = await projectService.getProject(project.id);
|
||||
|
||||
t.is(updatedProject.name, readProject.name);
|
||||
t.is(updatedProject.description, readProject.description);
|
||||
});
|
||||
|
||||
test.serial('should give error when getting unkown project', async t => {
|
||||
try {
|
||||
await projectService.getProject('unknown');
|
||||
} catch (err) {
|
||||
t.is(err.message, 'No project found');
|
||||
}
|
||||
});
|
84
test/e2e/stores/project-store.e2e.test.js
Normal file
84
test/e2e/stores/project-store.e2e.test.js
Normal file
@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const dbInit = require('../helpers/database-init');
|
||||
const getLogger = require('../../fixtures/no-logger');
|
||||
|
||||
let stores;
|
||||
let projectStore;
|
||||
|
||||
test.before(async () => {
|
||||
const db = await dbInit('project_store_serial', getLogger);
|
||||
stores = db.stores;
|
||||
projectStore = stores.projectStore;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await stores.db.destroy();
|
||||
});
|
||||
|
||||
test.serial('should have default project', async t => {
|
||||
const project = await projectStore.get('default');
|
||||
t.assert(project);
|
||||
t.is(project.id, 'default');
|
||||
});
|
||||
|
||||
test.serial('should create new project', async t => {
|
||||
const project = {
|
||||
id: 'test',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectStore.create(project);
|
||||
const ret = await projectStore.get('test');
|
||||
t.deepEqual(project.id, ret.id);
|
||||
t.deepEqual(project.name, ret.name);
|
||||
t.deepEqual(project.description, ret.description);
|
||||
t.truthy(ret.createdAt);
|
||||
});
|
||||
|
||||
test.serial('should delete project', async t => {
|
||||
const project = {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectStore.create(project);
|
||||
await projectStore.delete(project.id);
|
||||
|
||||
try {
|
||||
await projectStore.get(project.id);
|
||||
} catch (err) {
|
||||
t.is(err.message, 'No project found');
|
||||
}
|
||||
});
|
||||
|
||||
test.serial('should update project', async t => {
|
||||
const project = {
|
||||
id: 'test-update',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const updatedProject = {
|
||||
id: 'test-update',
|
||||
name: 'New name',
|
||||
description: 'Blah longer desc',
|
||||
};
|
||||
|
||||
await projectStore.create(project);
|
||||
await projectStore.update(updatedProject);
|
||||
|
||||
const readProject = await projectStore.get(project.id);
|
||||
|
||||
t.is(updatedProject.name, readProject.name);
|
||||
t.is(updatedProject.description, readProject.description);
|
||||
});
|
||||
|
||||
test.serial('should give error when getting unkown project', async t => {
|
||||
try {
|
||||
await projectStore.get('unknown');
|
||||
} catch (err) {
|
||||
t.is(err.message, 'No project found');
|
||||
}
|
||||
});
|
48
yarn.lock
48
yarn.lock
@ -183,28 +183,11 @@
|
||||
dependencies:
|
||||
arrify "^1.0.1"
|
||||
|
||||
"@hapi/address@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
|
||||
integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@hapi/formula@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
|
||||
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010"
|
||||
integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==
|
||||
|
||||
"@hapi/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
|
||||
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||
@ -277,6 +260,23 @@
|
||||
dependencies:
|
||||
"@passport-next/passport-strategy" "1.x.x"
|
||||
|
||||
"@sideway/address@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
|
||||
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
@ -3113,16 +3113,16 @@ istanbul-reports@^3.0.2:
|
||||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
joi@^17.2.0:
|
||||
version "17.2.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.0.tgz#81cba6c1145130482d57b6d50129c7ab0e7d8b0a"
|
||||
integrity sha512-9ZC8pMSitNlenuwKARENBGVvvGYHNlwWe5rexo2WxyogaxCB5dNHAgFA1BJQ6nsJrt/jz1p5vSqDT6W6kciDDw==
|
||||
joi@^17.3.0:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
|
||||
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
|
||||
dependencies:
|
||||
"@hapi/address" "^4.1.0"
|
||||
"@hapi/formula" "^2.0.0"
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/pinpoint" "^2.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.0"
|
||||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
js-string-escape@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user