From 960a6d161d45959f2831aa99dae383cccdcef945 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Wed, 4 Oct 2023 18:31:36 +0200 Subject: [PATCH 01/16] feat: last seen initial steps for new architecture --- src/lib/db/feature-toggle-store.ts | 2 +- src/lib/db/index.ts | 2 + .../features/project/createProjectService.ts | 6 + .../client-metrics/last-seen-service.test.ts | 4 +- .../last-seen/createLastSeenService.ts | 25 + .../last-seen/fake-last-seen-read-model.ts | 10 + .../last-seen/fake-last-seen-store.ts | 9 + .../last-seen/last-seen-read-model.ts | 22 + .../last-seen/last-seen-service.test.ts | 0 .../{ => last-seen}/last-seen-service.ts | 17 +- .../last-seen/last-seen-store.ts | 75 ++ .../types/last-seen-read-model-type.ts | 5 + .../last-seen/types/last-seen-store-type.ts | 5 + .../client-metrics/metrics-service-v2.test.ts | 4 +- .../client-metrics/metrics-service-v2.ts | 2 +- src/lib/services/index.ts | 30 +- src/lib/services/project-service.ts | 11 + src/lib/types/services.ts | 2 +- src/lib/types/stores.ts | 3 + src/lib/types/stores/feature-toggle-store.ts | 2 +- ...231003113443-last-seen-at-metrics-table.js | 21 + .../e2e/services/access-service.e2e.test.ts | 778 ++++++------ .../services/api-token-service.e2e.test.ts | 134 +- .../services/last-seen-service.e2e.test.ts | 8 +- .../project-health-service.e2e.test.ts | 110 +- .../e2e/services/project-service.e2e.test.ts | 1094 ++++++++--------- .../fixtures/fake-feature-toggle-store.ts | 2 +- src/test/fixtures/store.ts | 1 + 28 files changed, 1291 insertions(+), 1093 deletions(-) create mode 100644 src/lib/services/client-metrics/last-seen/createLastSeenService.ts create mode 100644 src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts create mode 100644 src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-read-model.ts create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-service.test.ts rename src/lib/services/client-metrics/{ => last-seen}/last-seen-service.ts (77%) create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-store.ts create mode 100644 src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts create mode 100644 src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts create mode 100644 src/migrations/20231003113443-last-seen-at-metrics-table.js diff --git a/src/lib/db/feature-toggle-store.ts b/src/lib/db/feature-toggle-store.ts index fcb988214d88..3d96050f32eb 100644 --- a/src/lib/db/feature-toggle-store.ts +++ b/src/lib/db/feature-toggle-store.ts @@ -7,7 +7,7 @@ import { Logger, LogProvider } from '../logger'; import { FeatureToggle, FeatureToggleDTO, IVariant } from '../types/model'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { Db } from './db'; -import { LastSeenInput } from '../services/client-metrics/last-seen-service'; +import { LastSeenInput } from '../services/client-metrics/last-seen/last-seen-service'; import { NameExistsError } from '../error'; export type EnvironmentFeatureNames = { [key: string]: string[] }; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 8e0ee4e1fbfe..6d4c41f0d952 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -38,6 +38,7 @@ import { Db } from './db'; import { ImportTogglesStore } from '../features/export-import-toggles/import-toggles-store'; import PrivateProjectStore from '../features/private-project/privateProjectStore'; import { DependentFeaturesStore } from '../features/dependent-features/dependent-features-store'; +import LastSeenStore from '../services/client-metrics/last-seen/last-seen-store'; export const createStores = ( config: IUnleashConfig, @@ -132,6 +133,7 @@ export const createStores = ( importTogglesStore: new ImportTogglesStore(db), privateProjectStore: new PrivateProjectStore(db, getLogger), dependentFeaturesStore: new DependentFeaturesStore(db), + lastSeenStore: new LastSeenStore(db, eventBus, getLogger), }; }; diff --git a/src/lib/features/project/createProjectService.ts b/src/lib/features/project/createProjectService.ts index 58d2830ae724..e6e230ee6e76 100644 --- a/src/lib/features/project/createProjectService.ts +++ b/src/lib/features/project/createProjectService.ts @@ -41,6 +41,8 @@ import { createPrivateProjectChecker, } from '../private-project/createPrivateProjectChecker'; import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store'; +import { LastSeenAtReadModel } from '../../services/client-metrics/last-seen/last-seen-read-model'; +import { FakeLastSeenReadModel } from '../../services/client-metrics/last-seen/fake-last-seen-read-model'; export const createProjectService = ( db: Db, @@ -99,6 +101,7 @@ export const createProjectService = ( ); const privateProjectChecker = createPrivateProjectChecker(db, config); + const lastSeenReadModel = new LastSeenAtReadModel(db); return new ProjectService( { @@ -118,6 +121,7 @@ export const createProjectService = ( favoriteService, eventService, privateProjectChecker, + lastSeenReadModel, ); }; @@ -160,6 +164,7 @@ export const createFakeProjectService = ( ); const privateProjectChecker = createFakePrivateProjectChecker(); + const fakeLastSeenReadModel = new FakeLastSeenReadModel(); return new ProjectService( { @@ -179,5 +184,6 @@ export const createFakeProjectService = ( favoriteService, eventService, privateProjectChecker, + fakeLastSeenReadModel, ); }; diff --git a/src/lib/services/client-metrics/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen-service.test.ts index 90f181772a7b..b4da24078c27 100644 --- a/src/lib/services/client-metrics/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen-service.test.ts @@ -2,7 +2,7 @@ import createStores from '../../../test/fixtures/store'; import EventEmitter from 'events'; import getLogger from '../../../test/fixtures/no-logger'; import { IUnleashConfig } from '../../types'; -import { LastSeenService } from './last-seen-service'; +import { LastSeenService } from './last-seen/last-seen-service'; function initLastSeenService(flagEnabled = true) { const stores = createStores(); @@ -20,7 +20,7 @@ function initLastSeenService(flagEnabled = true) { }, } as unknown as IUnleashConfig; - const lastSeenService = new LastSeenService(stores, config); + const lastSeenService = new LastSeenService(stores.lastSeenStore, config); return { lastSeenService, featureToggleStore: stores.featureToggleStore }; } diff --git a/src/lib/services/client-metrics/last-seen/createLastSeenService.ts b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts new file mode 100644 index 000000000000..288a5692d4b3 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts @@ -0,0 +1,25 @@ +import { Db, IUnleashConfig } from '../../../server-impl'; +import { FakeLastSeenStore } from './fake-last-seen-store'; +import { LastSeenService } from './last-seen-service'; +import LastSeenStore from './last-seen-store'; + +export const createLastSeenService = ( + db: Db, + config: IUnleashConfig, +): LastSeenService => { + const lastSeenStore = new LastSeenStore( + db, + config.eventBus, + config.getLogger, + ); + + return new LastSeenService(lastSeenStore, config); +}; + +export const createFakeLastSeenService = ( + config: IUnleashConfig, +): LastSeenService => { + const lastSeenStore = new FakeLastSeenStore(); + + return new LastSeenService(lastSeenStore, config); +}; diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts new file mode 100644 index 000000000000..063f3b59dfab --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts @@ -0,0 +1,10 @@ +import { ILastSeenReadModel } from './types/last-seen-read-model-type'; + +export class FakeLastSeenReadModel implements ILastSeenReadModel { + getForFeature( + features: string[], + ): Promise<{ lastSeen: Date; environment: string }[]> { + features.map((feature) => feature); + return Promise.resolve([]); + } +} diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts new file mode 100644 index 000000000000..94b6be14452f --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-store.ts @@ -0,0 +1,9 @@ +import { LastSeenInput } from './last-seen-service'; +import { ILastSeenStore } from './types/last-seen-store-type'; + +export class FakeLastSeenStore implements ILastSeenStore { + setLastSeen(data: LastSeenInput[]): Promise { + data.map((lastSeen) => lastSeen); + return Promise.resolve(); + } +} diff --git a/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts new file mode 100644 index 000000000000..2514f236bd57 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts @@ -0,0 +1,22 @@ +import { Db } from '../../../db/db'; +import { ILastSeenReadModel } from './types/last-seen-read-model-type'; + +const TABLE = 'last_seen_at_metrics'; + +export class LastSeenAtReadModel implements ILastSeenReadModel { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async getForFeature( + features: string[], + ): Promise<{ lastSeen: Date; environment: string }[]> { + const rows = await this.db(TABLE).whereIn('feature_name', features); + + console.log(rows); + + return [{ lastSeen: new Date(), environment: 'string' }]; + } +} diff --git a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/lib/services/client-metrics/last-seen-service.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.ts similarity index 77% rename from src/lib/services/client-metrics/last-seen-service.ts rename to src/lib/services/client-metrics/last-seen/last-seen-service.ts index c4d9564f4993..a05d1c0ac6b7 100644 --- a/src/lib/services/client-metrics/last-seen-service.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-service.ts @@ -1,9 +1,8 @@ import { secondsToMilliseconds } from 'date-fns'; -import { Logger } from '../../logger'; -import { IUnleashConfig } from '../../server-impl'; -import { IUnleashStores } from '../../types'; -import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2'; -import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store'; +import { Logger } from '../../../logger'; +import { IUnleashConfig } from '../../../server-impl'; +import { IClientMetricsEnv } from '../../../types/stores/client-metrics-store-v2'; +import { ILastSeenStore } from './types/last-seen-store-type'; export type LastSeenInput = { featureName: string; @@ -17,14 +16,14 @@ export class LastSeenService { private logger: Logger; - private featureToggleStore: IFeatureToggleStore; + private lastSeenStore: ILastSeenStore; constructor( - { featureToggleStore }: Pick, + lastSeenStore: ILastSeenStore, config: IUnleashConfig, lastSeenInterval = secondsToMilliseconds(30), ) { - this.featureToggleStore = featureToggleStore; + this.lastSeenStore = lastSeenStore; this.logger = config.getLogger( '/services/client-metrics/last-seen-service.ts', ); @@ -42,7 +41,7 @@ export class LastSeenService { this.logger.debug( `Updating last seen for ${lastSeenToggles.length} toggles`, ); - await this.featureToggleStore.setLastSeen(lastSeenToggles); + await this.lastSeenStore.setLastSeen(lastSeenToggles); } return count; } diff --git a/src/lib/services/client-metrics/last-seen/last-seen-store.ts b/src/lib/services/client-metrics/last-seen/last-seen-store.ts new file mode 100644 index 000000000000..65672f7553d5 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/last-seen-store.ts @@ -0,0 +1,75 @@ +import EventEmitter from 'events'; +import { LogProvider, Logger } from '../../../logger'; +import { DB_TIME } from '../../../metric-events'; +import { Db } from '../../../server-impl'; +import metricsHelper from '../../..//util/metrics-helper'; +import { LastSeenInput } from './last-seen-service'; +import { ILastSeenStore } from './types/last-seen-store-type'; + +// const LAST_SEEN_AT_COLUMNS = ['feature_name', 'environment', 'last_seen_at']; +const TABLE = 'last_seen_at_metrics'; + +export interface FeaturesTable { + feature_name: string; + last_seen_at: Date; + environment: string; +} + +export default class LastSeenStore implements ILastSeenStore { + private db: Db; + + private logger: Logger; + + private timer: Function; + + constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('last-seen-store.ts'); + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'last-seen-environment-store', + action, + }); + } + + async exists(name: string): Promise { + const result = await this.db.raw( + 'SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present', + [name], + ); + const { present } = result.rows[0]; + return present; + } + + async setLastSeen(data: LastSeenInput[]): Promise { + const now = new Date(); + + try { + const inserts = data.map((item) => { + return { + feature_name: item.featureName, + environment: item.environment, + last_seen_at: now, + }; + }); + + const batchSize = 1000; + + await this.db.transaction(async (trx) => { + for (let i = 0; i < inserts.length; i += batchSize) { + const batch = inserts.slice(i, i + batchSize); + // Knex optimizes multi row insert when given an array: + // https://knexjs.org/guide/query-builder.html#insert + await trx(TABLE) + .insert(batch) + .onConflict(['feature_name', 'environment']) + .merge(); + } + }); + } catch (err) { + this.logger.error('Could not update lastSeen, error: ', err); + } + } +} + +module.exports = LastSeenStore; diff --git a/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts new file mode 100644 index 000000000000..60d540798204 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts @@ -0,0 +1,5 @@ +export interface ILastSeenReadModel { + getForFeature( + features: string[], + ): Promise<{ lastSeen: Date; environment: string }[]>; +} diff --git a/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts b/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts new file mode 100644 index 000000000000..8670792f3044 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/types/last-seen-store-type.ts @@ -0,0 +1,5 @@ +import { LastSeenInput } from '../last-seen-service'; + +export interface ILastSeenStore { + setLastSeen(data: LastSeenInput[]): Promise; +} diff --git a/src/lib/services/client-metrics/metrics-service-v2.test.ts b/src/lib/services/client-metrics/metrics-service-v2.test.ts index db9336cfc0e5..5fe9671ac8b3 100644 --- a/src/lib/services/client-metrics/metrics-service-v2.test.ts +++ b/src/lib/services/client-metrics/metrics-service-v2.test.ts @@ -4,7 +4,7 @@ import getLogger from '../../../test/fixtures/no-logger'; import createStores from '../../../test/fixtures/store'; import EventEmitter from 'events'; -import { LastSeenService } from './last-seen-service'; +import { LastSeenService } from './last-seen/last-seen-service'; import { IUnleashConfig } from 'lib/types'; function initClientMetrics(flagEnabled = true) { @@ -23,7 +23,7 @@ function initClientMetrics(flagEnabled = true) { }, } as unknown as IUnleashConfig; - const lastSeenService = new LastSeenService(stores, config); + const lastSeenService = new LastSeenService(stores.lastSeenStore, config); lastSeenService.updateLastSeen = jest.fn(); const service = new ClientMetricsServiceV2(stores, config, lastSeenService); diff --git a/src/lib/services/client-metrics/metrics-service-v2.ts b/src/lib/services/client-metrics/metrics-service-v2.ts index e766110e44ea..db950e342f48 100644 --- a/src/lib/services/client-metrics/metrics-service-v2.ts +++ b/src/lib/services/client-metrics/metrics-service-v2.ts @@ -17,7 +17,7 @@ import ApiUser from '../../types/api-user'; import { ALL } from '../../types/models/api-token'; import User from '../../types/user'; import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics'; -import { LastSeenService } from './last-seen-service'; +import { LastSeenService } from './last-seen/last-seen-service'; import { generateHourBuckets } from '../../util/time-utils'; import { ClientMetricsSchema } from 'lib/openapi'; import { nameSchema } from '../../schema/feature-schema'; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 0df9ca9d70db..9744a99bdad0 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -35,7 +35,7 @@ import { ProxyService } from './proxy-service'; import EdgeService from './edge-service'; import PatService from './pat-service'; import { PublicSignupTokenService } from './public-signup-token-service'; -import { LastSeenService } from './client-metrics/last-seen-service'; +import { LastSeenService } from './client-metrics/last-seen/last-seen-service'; import { InstanceStatsService } from '../features/instance-stats/instance-stats-service'; import { FavoritesService } from './favorites-service'; import MaintenanceService from './maintenance-service'; @@ -59,7 +59,11 @@ import { createFakeChangeRequestAccessService, } from '../features/change-request-access-service/createChangeRequestAccessReadModel'; import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service'; -import { createFeatureToggleService } from '../features'; +import { + createFakeProjectService, + createFeatureToggleService, + createProjectService, +} from '../features'; import EventAnnouncerService from './event-announcer-service'; import { createGroupService } from '../features/group/createGroupService'; import { @@ -77,6 +81,10 @@ import { } from '../features/dependent-features/createDependentFeaturesService'; import { DependentFeaturesReadModel } from '../features/dependent-features/dependent-features-read-model'; import { FakeDependentFeaturesReadModel } from '../features/dependent-features/fake-dependent-features-read-model'; +import { + createFakeLastSeenService, + createLastSeenService, +} from './client-metrics/last-seen/createLastSeenService'; // TODO: will be moved to scheduler feature directory export const scheduleServices = async ( @@ -171,7 +179,9 @@ export const createServices = ( const groupService = new GroupService(stores, config, eventService); const accessService = new AccessService(stores, config, groupService); const apiTokenService = new ApiTokenService(stores, config, eventService); - const lastSeenService = new LastSeenService(stores, config); + const lastSeenService = db + ? createLastSeenService(db, config) + : createFakeLastSeenService(config); const clientMetricsServiceV2 = new ClientMetricsServiceV2( stores, config, @@ -260,16 +270,10 @@ export const createServices = ( eventService, ); const favoritesService = new FavoritesService(stores, config, eventService); - const projectService = new ProjectService( - stores, - config, - accessService, - featureToggleServiceV2, - groupService, - favoritesService, - eventService, - privateProjectChecker, - ); + const projectService = db + ? createProjectService(db, config) + : createFakeProjectService(config); + const projectHealthService = new ProjectHealthService( stores, config, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 302a8e5a3a4a..45036e76078c 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -65,6 +65,7 @@ import { ProjectDoraMetricsSchema } from 'lib/openapi'; import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation'; import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; import EventService from './event-service'; +import { ILastSeenReadModel } from './client-metrics/last-seen/types/last-seen-read-model-type'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; @@ -118,6 +119,8 @@ export default class ProjectService { private projectStatsStore: IProjectStatsStore; + private lastSeenReadModel: ILastSeenReadModel; + private flagResolver: IFlagResolver; private isEnterprise: boolean; @@ -150,6 +153,7 @@ export default class ProjectService { favoriteService: FavoritesService, eventService: EventService, privateProjectChecker: IPrivateProjectChecker, + lastSeenReadModel: ILastSeenReadModel, ) { this.projectStore = projectStore; this.environmentStore = environmentStore; @@ -165,6 +169,7 @@ export default class ProjectService { this.groupService = groupService; this.eventService = eventService; this.projectStatsStore = projectStatsStore; + this.lastSeenReadModel = lastSeenReadModel; this.logger = config.getLogger('services/project-service.js'); this.flagResolver = config.flagResolver; this.isEnterprise = config.isEnterprise; @@ -1071,6 +1076,12 @@ export default class ProjectService { this.projectStatsStore.getProjectStats(projectId), ]); + //const featureNames = features.map((feature) => feature.name); + // const lastSeenAtPerEnvironment = + // await this.lastSeenReadModel.getForFeature(featureNames); + // Get + // console.log(features); + return { stats: projectStats, name: project.name, diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index ff66b785fee4..486665e02667 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -32,7 +32,7 @@ import { ProxyService } from '../services/proxy-service'; import EdgeService from '../services/edge-service'; import PatService from '../services/pat-service'; import { PublicSignupTokenService } from '../services/public-signup-token-service'; -import { LastSeenService } from '../services/client-metrics/last-seen-service'; +import { LastSeenService } from '../services/client-metrics/last-seen/last-seen-service'; import { InstanceStatsService } from '../features/instance-stats/instance-stats-service'; import { FavoritesService } from '../services/favorites-service'; import MaintenanceService from '../services/maintenance-service'; diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index da1f7258fb33..ad78f996e905 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -35,6 +35,7 @@ import { IProjectStatsStore } from './stores/project-stats-store-type'; import { IImportTogglesStore } from '../features/export-import-toggles/import-toggles-store-type'; import { IPrivateProjectStore } from '../features/private-project/privateProjectStoreType'; import { IDependentFeaturesStore } from '../features/dependent-features/dependent-features-store-type'; +import { ILastSeenStore } from '../services/client-metrics/last-seen/types/last-seen-store-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -74,6 +75,7 @@ export interface IUnleashStores { importTogglesStore: IImportTogglesStore; privateProjectStore: IPrivateProjectStore; dependentFeaturesStore: IDependentFeaturesStore; + lastSeenStore: ILastSeenStore; } export { @@ -113,4 +115,5 @@ export { IImportTogglesStore, IPrivateProjectStore, IDependentFeaturesStore, + ILastSeenStore, }; diff --git a/src/lib/types/stores/feature-toggle-store.ts b/src/lib/types/stores/feature-toggle-store.ts index 9108519fb1eb..67bc103898b0 100644 --- a/src/lib/types/stores/feature-toggle-store.ts +++ b/src/lib/types/stores/feature-toggle-store.ts @@ -1,6 +1,6 @@ import { FeatureToggle, FeatureToggleDTO, IVariant } from '../model'; import { Store } from './store'; -import { LastSeenInput } from '../../services/client-metrics/last-seen-service'; +import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service'; export interface IFeatureToggleQuery { archived: boolean; diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js new file mode 100644 index 000000000000..e332d3a425c4 --- /dev/null +++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js @@ -0,0 +1,21 @@ +'use strict'; + +exports.up = function (db, callback) { + db.runSql( + ` + CREATE TABLE last_seen_at_metrics ( + feature_name VARCHAR(255), + environment VARCHAR(100), + last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (feature_name, environment) + ); + + CREATE INDEX idx_feature_name + ON last_seen_at_metrics (feature_name);`, + callback(), + ); +}; + +exports.down = function (db, callback) { + callback(); +}; diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index b28b279863bc..c55d6088987a 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -1,26 +1,26 @@ -import dbInit, { ITestDb } from '../helpers/database-init'; -import getLogger from '../../fixtures/no-logger'; +import dbInit, { ITestDb } from "../helpers/database-init"; +import getLogger from "../../fixtures/no-logger"; import { AccessService, PermissionRef, -} from '../../../lib/services/access-service'; +} from "../../../lib/services/access-service"; -import * as permissions from '../../../lib/types/permissions'; -import { RoleName } from '../../../lib/types/model'; +import * as permissions from "../../../lib/types/permissions"; +import { RoleName } from "../../../lib/types/model"; import { ICreateGroupUserModel, IUnleashStores, IUserAccessOverview, -} from '../../../lib/types'; -import { createTestConfig } from '../../config/test-config'; -import { DEFAULT_PROJECT } from '../../../lib/types/project'; -import { ALL_PROJECTS } from '../../../lib/util/constants'; +} from "../../../lib/types"; +import { createTestConfig } from "../../config/test-config"; +import { DEFAULT_PROJECT } from "../../../lib/types/project"; +import { ALL_PROJECTS } from "../../../lib/util/constants"; import { createAccessService, createFeatureToggleService, createProjectService, -} from '../../../lib/features'; +} from "../../../lib/features"; let db: ITestDb; let stores: IUnleashStores; @@ -44,7 +44,7 @@ const createUser = async (role?: number) => { await accessService.addUserToRole( user.id, role, - role === readRole.id ? ALL_PROJECTS : DEFAULT_PROJECT, + role === readRole.id ? ALL_PROJECTS : DEFAULT_PROJECT ); return user; }; @@ -62,7 +62,7 @@ const createGroup = async ({ name: `Group ${groupIndex}`, rootRole: role, }); - if (users) await groupStore.addUsersToGroup(group.id, users, 'Admin'); + if (users) await groupStore.addUsersToGroup(group.id, users, "Admin"); return group; }; @@ -76,9 +76,9 @@ const createRole = async (rolePermissions: PermissionRef[]) => { }; const hasCommonProjectAccess = async (user, projectName, condition) => { - const defaultEnv = 'default'; - const developmentEnv = 'development'; - const productionEnv = 'production'; + const defaultEnv = "default"; + const developmentEnv = "development"; + const productionEnv = "production"; const { CREATE_FEATURE, @@ -91,116 +91,116 @@ const hasCommonProjectAccess = async (user, projectName, condition) => { UPDATE_FEATURE_VARIANTS, } = permissions; expect( - await accessService.hasPermission(user, CREATE_FEATURE, projectName), + await accessService.hasPermission(user, CREATE_FEATURE, projectName) ).toBe(condition); expect( - await accessService.hasPermission(user, UPDATE_FEATURE, projectName), + await accessService.hasPermission(user, UPDATE_FEATURE, projectName) ).toBe(condition); expect( - await accessService.hasPermission(user, DELETE_FEATURE, projectName), + await accessService.hasPermission(user, DELETE_FEATURE, projectName) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_VARIANTS, - projectName, - ), + projectName + ) ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - defaultEnv, - ), + defaultEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - defaultEnv, - ), + defaultEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - defaultEnv, - ), + defaultEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - defaultEnv, - ), + defaultEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - developmentEnv, - ), + developmentEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - developmentEnv, - ), + developmentEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - developmentEnv, - ), + developmentEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - developmentEnv, - ), + developmentEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - productionEnv, - ), + productionEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - productionEnv, - ), + productionEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - productionEnv, - ), + productionEnv + ) ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - productionEnv, - ), + productionEnv + ) ).toBe(condition); }; @@ -208,23 +208,23 @@ const hasFullProjectAccess = async (user, projectName: string, condition) => { const { DELETE_PROJECT, UPDATE_PROJECT, MOVE_FEATURE_TOGGLE } = permissions; expect( - await accessService.hasPermission(user, DELETE_PROJECT, projectName), + await accessService.hasPermission(user, DELETE_PROJECT, projectName) ).toBe(condition); expect( - await accessService.hasPermission(user, UPDATE_PROJECT, projectName), + await accessService.hasPermission(user, UPDATE_PROJECT, projectName) ).toBe(condition); expect( await accessService.hasPermission( user, MOVE_FEATURE_TOGGLE, - projectName, - ), + projectName + ) ); await hasCommonProjectAccess(user, projectName, condition); }; beforeAll(async () => { - db = await dbInit('access_service_serial', getLogger); + db = await dbInit("access_service_serial", getLogger); stores = db.stores; // projectStore = stores.projectStore; const config = createTestConfig({ @@ -246,20 +246,20 @@ beforeAll(async () => { const testAdmin = await createUser(adminRole.id); await projectService.createProject( { - id: 'some-project', - name: 'Some project', - description: 'Used in the test', + id: "some-project", + name: "Some project", + description: "Used in the test", }, - testAdmin, + testAdmin ); await projectService.createProject( { - id: 'unusedprojectname', - name: 'Another project not used', - description: 'Also used in the test', + id: "unusedprojectname", + name: "Another project not used", + description: "Also used in the test", }, - testAdmin, + testAdmin ); }); @@ -267,7 +267,7 @@ afterAll(async () => { await db.destroy(); }); -test('should have access to admin addons', async () => { +test("should have access to admin addons", async () => { const { CREATE_ADDON, UPDATE_ADDON, DELETE_ADDON } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_ADDON)).toBe(true); @@ -275,7 +275,7 @@ test('should have access to admin addons', async () => { expect(await accessService.hasPermission(user, DELETE_ADDON)).toBe(true); }); -test('should have access to admin strategies', async () => { +test("should have access to admin strategies", async () => { const { CREATE_STRATEGY, UPDATE_STRATEGY, DELETE_STRATEGY } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_STRATEGY)).toBe(true); @@ -283,77 +283,77 @@ test('should have access to admin strategies', async () => { expect(await accessService.hasPermission(user, DELETE_STRATEGY)).toBe(true); }); -test('should have access to admin contexts', async () => { +test("should have access to admin contexts", async () => { const { CREATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_CONTEXT_FIELD)).toBe( - true, + true ); expect(await accessService.hasPermission(user, UPDATE_CONTEXT_FIELD)).toBe( - true, + true ); expect(await accessService.hasPermission(user, DELETE_CONTEXT_FIELD)).toBe( - true, + true ); }); -test('should have access to create projects', async () => { +test("should have access to create projects", async () => { const { CREATE_PROJECT } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_PROJECT)).toBe(true); }); -test('should have access to update applications', async () => { +test("should have access to update applications", async () => { const { UPDATE_APPLICATION } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, UPDATE_APPLICATION)).toBe( - true, + true ); }); -test('should not have admin permission', async () => { +test("should not have admin permission", async () => { const { ADMIN } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, ADMIN)).toBe(false); }); -test('should have project admin to default project as editor', async () => { - const projectName = 'default'; +test("should have project admin to default project as editor", async () => { + const projectName = "default"; const user = editorUser; await hasFullProjectAccess(user, projectName, true); }); -test('should not have project admin to other projects as editor', async () => { - const projectName = 'unusedprojectname'; +test("should not have project admin to other projects as editor", async () => { + const projectName = "unusedprojectname"; const user = editorUser; await hasFullProjectAccess(user, projectName, false); }); -test('cannot add CREATE_FEATURE without defining project', async () => { +test("cannot add CREATE_FEATURE without defining project", async () => { await expect(async () => { await accessService.addPermissionToRole( editorRole.id, - permissions.CREATE_FEATURE, + permissions.CREATE_FEATURE ); }).rejects.toThrow( - new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), + new Error("ProjectId cannot be empty for permission=CREATE_FEATURE") ); }); -test('cannot remove CREATE_FEATURE without defining project', async () => { +test("cannot remove CREATE_FEATURE without defining project", async () => { await expect(async () => { await accessService.removePermissionFromRole( editorRole.id, - permissions.CREATE_FEATURE, + permissions.CREATE_FEATURE ); }).rejects.toThrow( - new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), + new Error("ProjectId cannot be empty for permission=CREATE_FEATURE") ); }); -test('should remove CREATE_FEATURE on default environment', async () => { +test("should remove CREATE_FEATURE on default environment", async () => { const { CREATE_FEATURE } = permissions; const user = editorUser; const editRole = await accessService.getRoleByName(RoleName.EDITOR); @@ -361,7 +361,7 @@ test('should remove CREATE_FEATURE on default environment', async () => { await accessService.addPermissionToRole( editRole.id, permissions.CREATE_FEATURE, - '*', + "*" ); // TODO: to validate the remove works, we should make sure that we had permission before removing it @@ -373,15 +373,15 @@ test('should remove CREATE_FEATURE on default environment', async () => { await accessService.removePermissionFromRole( editRole.id, permissions.CREATE_FEATURE, - '*', + "*" ); expect( - await accessService.hasPermission(user, CREATE_FEATURE, 'some-project'), + await accessService.hasPermission(user, CREATE_FEATURE, "some-project") ).toBe(false); }); -test('admin should be admin', async () => { +test("admin should be admin", async () => { const { DELETE_PROJECT, UPDATE_PROJECT, @@ -392,40 +392,40 @@ test('admin should be admin', async () => { } = permissions; const user = await createUser(adminRole.id); expect( - await accessService.hasPermission(user, DELETE_PROJECT, 'default'), + await accessService.hasPermission(user, DELETE_PROJECT, "default") ).toBe(true); expect( - await accessService.hasPermission(user, UPDATE_PROJECT, 'default'), + await accessService.hasPermission(user, UPDATE_PROJECT, "default") ).toBe(true); expect( - await accessService.hasPermission(user, CREATE_FEATURE, 'default'), + await accessService.hasPermission(user, CREATE_FEATURE, "default") ).toBe(true); expect( - await accessService.hasPermission(user, UPDATE_FEATURE, 'default'), + await accessService.hasPermission(user, UPDATE_FEATURE, "default") ).toBe(true); expect( - await accessService.hasPermission(user, DELETE_FEATURE, 'default'), + await accessService.hasPermission(user, DELETE_FEATURE, "default") ).toBe(true); expect(await accessService.hasPermission(user, ADMIN)).toBe(true); }); -test('should create default roles to project', async () => { - const project = 'some-project'; +test("should create default roles to project", async () => { + const project = "some-project"; const user = editorUser; await accessService.createDefaultProjectRoles(user, project); await hasFullProjectAccess(user, project, true); }); -test('should require name when create default roles to project', async () => { +test("should require name when create default roles to project", async () => { await expect(async () => { // @ts-ignore await accessService.createDefaultProjectRoles(editorUser); - }).rejects.toThrow(new Error('ProjectId cannot be empty')); + }).rejects.toThrow(new Error("ProjectId cannot be empty")); }); -test('should grant user access to project', async () => { +test("should grant user access to project", async () => { const { DELETE_PROJECT, UPDATE_PROJECT } = permissions; - const project = 'another-project'; + const project = "another-project"; const user = editorUser; const sUser = await createUser(readRole.id); await accessService.createDefaultProjectRoles(user, project); @@ -438,15 +438,15 @@ test('should grant user access to project', async () => { // Should not be able to admin the project itself. expect( - await accessService.hasPermission(sUser, UPDATE_PROJECT, project), + await accessService.hasPermission(sUser, UPDATE_PROJECT, project) ).toBe(false); expect( - await accessService.hasPermission(sUser, DELETE_PROJECT, project), + await accessService.hasPermission(sUser, DELETE_PROJECT, project) ).toBe(false); }); -test('should not get access if not specifying project', async () => { - const project = 'another-project-2'; +test("should not get access if not specifying project", async () => { + const project = "another-project-2"; const user = editorUser; const sUser = await createUser(readRole.id); await accessService.createDefaultProjectRoles(user, project); @@ -459,51 +459,51 @@ test('should not get access if not specifying project', async () => { await hasCommonProjectAccess(sUser, undefined, false); }); -test('should remove user from role', async () => { +test("should remove user from role", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random123@getunleash.io', + name: "Some User", + email: "random123@getunleash.io", }); - await accessService.addUserToRole(user.id, editorRole.id, 'default'); + await accessService.addUserToRole(user.id, editorRole.id, "default"); // check user has one role const userRoles = await accessService.getRolesForUser(user.id); expect(userRoles.length).toBe(1); expect(userRoles[0].name).toBe(RoleName.EDITOR); - await accessService.removeUserFromRole(user.id, editorRole.id, 'default'); + await accessService.removeUserFromRole(user.id, editorRole.id, "default"); const userRolesAfterRemove = await accessService.getRolesForUser(user.id); expect(userRolesAfterRemove.length).toBe(0); }); -test('should return role with users', async () => { +test("should return role with users", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random2223@getunleash.io', + name: "Some User", + email: "random2223@getunleash.io", }); - await accessService.addUserToRole(user.id, editorRole.id, 'default'); + await accessService.addUserToRole(user.id, editorRole.id, "default"); const roleWithUsers = await accessService.getRoleData(editorRole.id); expect(roleWithUsers.role.name).toBe(RoleName.EDITOR); expect(roleWithUsers.users.length >= 2).toBe(true); expect(roleWithUsers.users.find((u) => u.id === user.id)).toBeTruthy(); expect( - roleWithUsers.users.find((u) => u.email === user.email), + roleWithUsers.users.find((u) => u.email === user.email) ).toBeTruthy(); }); -test('should return role with permissions and users', async () => { +test("should return role with permissions and users", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random2244@getunleash.io', + name: "Some User", + email: "random2244@getunleash.io", }); - await accessService.addUserToRole(user.id, editorRole.id, 'default'); + await accessService.addUserToRole(user.id, editorRole.id, "default"); const roleWithPermission = await accessService.getRoleData(editorRole.id); @@ -511,19 +511,19 @@ test('should return role with permissions and users', async () => { expect(roleWithPermission.permissions.length > 2).toBe(true); expect( roleWithPermission.permissions.find( - (p) => p.name === permissions.CREATE_PROJECT, - ), + (p) => p.name === permissions.CREATE_PROJECT + ) ).toBeTruthy(); //This assert requires other tests to have run in this pack before length > 2 resolves to true // I've set this to be > 1, which allows us to run the test alone and should still satisfy the logic requirement expect(roleWithPermission.users.length > 1).toBe(true); }); -test('should set root role for user', async () => { +test("should set root role for user", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random2255@getunleash.io', + name: "Some User", + email: "random2255@getunleash.io", }); await accessService.setUserRootRole(user.id, editorRole.id); @@ -535,11 +535,11 @@ test('should set root role for user', async () => { expect(roles.length).toBe(1); }); -test('should switch root role for user', async () => { +test("should switch root role for user", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random22Read@getunleash.io', + name: "Some User", + email: "random22Read@getunleash.io", }); await accessService.setUserRootRole(user.id, editorRole.id); @@ -551,24 +551,24 @@ test('should switch root role for user', async () => { expect(roles[0].name).toBe(RoleName.VIEWER); }); -test('should switch project roles on when multiple roles are present for same user', async () => { +test("should switch project roles on when multiple roles are present for same user", async () => { const { userStore, roleStore, accessStore } = stores; const userOne = await userStore.insert({ - name: 'Some User With Expected Roles', - email: 'random42Read@getunleash.io', + name: "Some User With Expected Roles", + email: "random42Read@getunleash.io", }); const customRole = await roleStore.create({ - name: 'Some Arbitrary Role', - roleType: 'custom', - description: 'This does nothing', + name: "Some Arbitrary Role", + roleType: "custom", + description: "This does nothing", }); const targetRole = await roleStore.create({ - name: 'Another Arbitrary Role', - roleType: 'custom', - description: 'This does nothing', + name: "Another Arbitrary Role", + roleType: "custom", + description: "This does nothing", }); await accessService.setUserRootRole(userOne.id, editorRole.id); @@ -577,16 +577,16 @@ test('should switch project roles on when multiple roles are present for same us await accessService.updateUserProjectRole( userOne.id, targetRole.id, - DEFAULT_PROJECT, + DEFAULT_PROJECT ); }); -test('should not crash if user does not have permission', async () => { +test("should not crash if user does not have permission", async () => { const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'random55Read@getunleash.io', + name: "Some User", + email: "random55Read@getunleash.io", }); await accessService.setUserRootRole(user.id, readRole.id); @@ -594,7 +594,7 @@ test('should not crash if user does not have permission', async () => { const { UPDATE_CONTEXT_FIELD } = permissions; const hasAccess = await accessService.hasPermission( user, - UPDATE_CONTEXT_FIELD, + UPDATE_CONTEXT_FIELD ); expect(hasAccess).toBe(false); @@ -604,31 +604,31 @@ test('should support permission with "ALL" environment requirement', async () => const { userStore, roleStore, accessStore } = stores; const user = await userStore.insert({ - name: 'Some User', - email: 'randomEnv1@getunleash.io', + name: "Some User", + email: "randomEnv1@getunleash.io", }); await accessService.setUserRootRole(user.id, readRole.id); const customRole = await roleStore.create({ - name: 'Power user', - roleType: 'custom', - description: 'Grants access to modify all environments', + name: "Power user", + roleType: "custom", + description: "Grants access to modify all environments", }); const { CREATE_FEATURE_STRATEGY } = permissions; await accessStore.addPermissionsToRole( customRole.id, [{ name: CREATE_FEATURE_STRATEGY }], - 'production', + "production" ); await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS); const hasAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - 'default', - 'production', + "default", + "production" ); expect(hasAccess).toBe(true); @@ -636,100 +636,100 @@ test('should support permission with "ALL" environment requirement', async () => const hasNotAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - 'default', - 'development', + "default", + "development" ); expect(hasNotAccess).toBe(false); }); -test('Should have access to create a strategy in an environment', async () => { +test("Should have access to create a strategy in an environment", async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - 'default', - 'development', - ), + "default", + "development" + ) ).toBe(true); }); -test('Should be denied access to create a strategy in an environment the user does not have access to', async () => { +test("Should be denied access to create a strategy in an environment the user does not have access to", async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - 'default', - 'noaccess', - ), + "default", + "noaccess" + ) ).toBe(false); }); -test('Should have access to edit a strategy in an environment', async () => { +test("Should have access to edit a strategy in an environment", async () => { const { UPDATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, - 'default', - 'development', - ), + "default", + "development" + ) ).toBe(true); }); -test('Should have access to delete a strategy in an environment', async () => { +test("Should have access to delete a strategy in an environment", async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, - 'default', - 'development', - ), + "default", + "development" + ) ).toBe(true); }); -test('Should be denied access to delete a strategy in an environment the user does not have access to', async () => { +test("Should be denied access to delete a strategy in an environment the user does not have access to", async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, - 'default', - 'noaccess', - ), + "default", + "noaccess" + ) ).toBe(false); }); -test('Should be denied access to delete a role that is in use', async () => { +test("Should be denied access to delete a role that is in use", async () => { const user = editorUser; const project = { - id: 'projectToUseRole', - name: 'New project', - description: 'Blah', + id: "projectToUseRole", + name: "New project", + description: "Blah", }; await projectService.createProject(project, user.id); const projectMember = await stores.userStore.insert({ - name: 'CustomProjectMember', - email: 'custom@getunleash.io', + name: "CustomProjectMember", + email: "custom@getunleash.io", }); const customRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); @@ -739,34 +739,34 @@ test('Should be denied access to delete a role that is in use', async () => { await accessService.deleteRole(customRole.id); } catch (e) { expect(e.toString()).toBe( - 'RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups.', + "RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups." ); } }); -test('Should be denied move feature toggle to project where the user does not have access', async () => { +test("Should be denied move feature toggle to project where the user does not have access", async () => { const user = editorUser; const editorUser2 = await createUser(editorRole.id); const projectOrigin = { - id: 'projectOrigin', - name: 'New project', - description: 'Blah', + id: "projectOrigin", + name: "New project", + description: "Blah", }; const projectDest = { - id: 'projectDest', - name: 'New project', - description: 'Blah', + id: "projectDest", + name: "New project", + description: "Blah", }; await projectService.createProject(projectOrigin, user.id); await projectService.createProject(projectDest, editorUser2.id); - const featureToggle = { name: 'moveableToggle' }; + const featureToggle = { name: "moveableToggle" }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username, + user.username ); try { @@ -774,69 +774,69 @@ test('Should be denied move feature toggle to project where the user does not ha projectDest.id, featureToggle.name, user, - projectOrigin.id, + projectOrigin.id ); } catch (e) { - expect(e.name).toContain('Permission'); - expect(e.message.includes('permission')).toBeTruthy(); + expect(e.name).toContain("Permission"); + expect(e.message.includes("permission")).toBeTruthy(); expect( - e.message.includes(permissions.MOVE_FEATURE_TOGGLE), + e.message.includes(permissions.MOVE_FEATURE_TOGGLE) ).toBeTruthy(); } }); -test('Should be allowed move feature toggle to project when the user has access', async () => { +test("Should be allowed move feature toggle to project when the user has access", async () => { const user = editorUser; const projectOrigin = { - id: 'projectOrigin1', - name: 'New project', - description: 'Blah', + id: "projectOrigin1", + name: "New project", + description: "Blah", }; const projectDest = { - id: 'projectDest2', - name: 'New project', - description: 'Blah', + id: "projectDest2", + name: "New project", + description: "Blah", }; await projectService.createProject(projectOrigin, user); await projectService.createProject(projectDest, user); - const featureToggle = { name: 'moveableToggle2' }; + const featureToggle = { name: "moveableToggle2" }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username, + user.username ); await projectService.changeProject( projectDest.id, featureToggle.name, user, - projectOrigin.id, + projectOrigin.id ); }); -test('Should not be allowed to edit a root role', async () => { +test("Should not be allowed to edit a root role", async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); const roleUpdate = { id: editRole.id, - name: 'NoLongerTheEditor', - description: '', + name: "NoLongerTheEditor", + description: "", }; try { await accessService.updateRole(roleUpdate); } catch (e) { expect(e.toString()).toBe( - 'InvalidOperationError: You cannot change built in roles.', + "InvalidOperationError: You cannot change built in roles." ); } }); -test('Should not be allowed to delete a root role', async () => { +test("Should not be allowed to delete a root role", async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); @@ -845,31 +845,31 @@ test('Should not be allowed to delete a root role', async () => { await accessService.deleteRole(editRole.id); } catch (e) { expect(e.toString()).toBe( - 'InvalidOperationError: You cannot change built in roles.', + "InvalidOperationError: You cannot change built in roles." ); } }); -test('Should not be allowed to edit a project role', async () => { +test("Should not be allowed to edit a project role", async () => { expect.assertions(1); const ownerRole = await accessService.getRoleByName(RoleName.OWNER); const roleUpdate = { id: ownerRole.id, - name: 'NoLongerTheEditor', - description: '', + name: "NoLongerTheEditor", + description: "", }; try { await accessService.updateRole(roleUpdate); } catch (e) { expect(e.toString()).toBe( - 'InvalidOperationError: You cannot change built in roles.', + "InvalidOperationError: You cannot change built in roles." ); } }); -test('Should not be allowed to delete a project role', async () => { +test("Should not be allowed to delete a project role", async () => { expect.assertions(1); const ownerRole = await accessService.getRoleByName(RoleName.OWNER); @@ -878,15 +878,15 @@ test('Should not be allowed to delete a project role', async () => { await accessService.deleteRole(ownerRole.id); } catch (e) { expect(e.toString()).toBe( - 'InvalidOperationError: You cannot change built in roles.', + "InvalidOperationError: You cannot change built in roles." ); } }); -test('Should be allowed move feature toggle to project when given access through group', async () => { +test("Should be allowed move feature toggle to project when given access through group", async () => { const project = { - id: 'yet-another-project1', - name: 'yet-another-project1', + id: "yet-another-project1", + name: "yet-another-project1", }; const viewerUser = await createUser(readRole.id); @@ -904,17 +904,17 @@ test('Should be allowed move feature toggle to project when given access through await accessService.addGroupToRole( groupWithProjectAccess.id!, projectRole.id, - 'SomeAdminUser', - project.id, + "SomeAdminUser", + project.id ); await hasCommonProjectAccess(viewerUser, project.id, true); }); -test('Should not lose user role access when given permissions from a group', async () => { +test("Should not lose user role access when given permissions from a group", async () => { const project = { - id: 'yet-another-project-lose', - name: 'yet-another-project-lose', + id: "yet-another-project-lose", + name: "yet-another-project-lose", }; const user = editorUser; @@ -929,23 +929,23 @@ test('Should not lose user role access when given permissions from a group', asy await accessService.addGroupToRole( groupWithNoAccess.id!, viewerRole.id, - 'SomeAdminUser', - project.id, + "SomeAdminUser", + project.id ); await hasFullProjectAccess(user, project.id, true); }); -test('Should allow user to take multiple group roles and have expected permissions on each project', async () => { +test("Should allow user to take multiple group roles and have expected permissions on each project", async () => { const projectForCreate = { - id: 'project-that-should-have-create-toggle-permission', - name: 'project-that-should-have-create-toggle-permission', - description: 'Blah', + id: "project-that-should-have-create-toggle-permission", + name: "project-that-should-have-create-toggle-permission", + description: "Blah", }; const projectForDelete = { - id: 'project-that-should-have-delete-toggle-permission', - name: 'project-that-should-have-delete-toggle-permission', - description: 'Blah', + id: "project-that-should-have-delete-toggle-permission", + name: "project-that-should-have-delete-toggle-permission", + description: "Blah", }; const viewerUser = await createUser(readRole.id); @@ -964,63 +964,63 @@ test('Should allow user to take multiple group roles and have expected permissio const createFeatureRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, ]); const deleteFeatureRole = await createRole([ { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); await accessService.addGroupToRole( groupWithCreateAccess.id!, deleteFeatureRole.id, - 'SomeAdminUser', - projectForDelete.id, + "SomeAdminUser", + projectForDelete.id ); await accessService.addGroupToRole( groupWithDeleteAccess.id!, createFeatureRole.id, - 'SomeAdminUser', - projectForCreate.id, + "SomeAdminUser", + projectForCreate.id ); expect( await accessService.hasPermission( viewerUser, permissions.CREATE_FEATURE, - projectForCreate.id, - ), + projectForCreate.id + ) ).toBe(true); expect( await accessService.hasPermission( viewerUser, permissions.DELETE_FEATURE, - projectForCreate.id, - ), + projectForCreate.id + ) ).toBe(false); expect( await accessService.hasPermission( viewerUser, permissions.CREATE_FEATURE, - projectForDelete.id, - ), + projectForDelete.id + ) ).toBe(false); expect( await accessService.hasPermission( viewerUser, permissions.DELETE_FEATURE, - projectForDelete.id, - ), + projectForDelete.id + ) ).toBe(true); }); -test('Should allow user to take on root role through a group that has a root role defined', async () => { +test("Should allow user to take on root role through a group that has a root role defined", async () => { const viewerUser = await createUser(readRole.id); await createGroup({ @@ -1029,11 +1029,11 @@ test('Should allow user to take on root role through a group that has a root rol }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN), + await accessService.hasPermission(viewerUser, permissions.ADMIN) ).toBe(true); }); -test('Should not elevate permissions for a user that is not present in a root role group', async () => { +test("Should not elevate permissions for a user that is not present in a root role group", async () => { const viewerUser = await createUser(readRole.id); const viewerUserNotInGroup = await createUser(readRole.id); @@ -1044,18 +1044,18 @@ test('Should not elevate permissions for a user that is not present in a root ro }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN), + await accessService.hasPermission(viewerUser, permissions.ADMIN) ).toBe(true); expect( await accessService.hasPermission( viewerUserNotInGroup, - permissions.ADMIN, - ), + permissions.ADMIN + ) ).toBe(false); }); -test('Should not reduce permissions for an admin user that enters an editor group', async () => { +test("Should not reduce permissions for an admin user that enters an editor group", async () => { const adminUser = await createUser(adminRole.id); await createGroup({ @@ -1064,18 +1064,18 @@ test('Should not reduce permissions for an admin user that enters an editor grou }); expect( - await accessService.hasPermission(adminUser, permissions.ADMIN), + await accessService.hasPermission(adminUser, permissions.ADMIN) ).toBe(true); }); -test('Should not change permissions for a user in a group without a root role', async () => { +test("Should not change permissions for a user in a group without a root role", async () => { const groupStore = stores.groupStore; const viewerUser = await createUser(readRole.id); const groupWithoutRootRole = await groupStore.create({ - name: 'GroupWithNoRootRole', - description: '', + name: "GroupWithNoRootRole", + description: "", }); const preAddedToGroupPermissions = @@ -1084,7 +1084,7 @@ test('Should not change permissions for a user in a group without a root role', await groupStore.addUsersToGroup( groupWithoutRootRole.id!, [{ user: viewerUser }], - 'Admin', + "Admin" ); const postAddedToGroupPermissions = @@ -1092,11 +1092,11 @@ test('Should not change permissions for a user in a group without a root role', expect( JSON.stringify(preAddedToGroupPermissions) === - JSON.stringify(postAddedToGroupPermissions), + JSON.stringify(postAddedToGroupPermissions) ).toBe(true); }); -test('Should add permissions to user when a group is given a root role after the user has been added to the group', async () => { +test("Should add permissions to user when a group is given a root role after the user has been added to the group", async () => { const groupStore = stores.groupStore; const viewerUser = await createUser(readRole.id); @@ -1106,23 +1106,23 @@ test('Should add permissions to user when a group is given a root role after the }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN), + await accessService.hasPermission(viewerUser, permissions.ADMIN) ).toBe(false); await groupStore.update({ id: groupWithoutRootRole.id!, - name: 'GroupWithNoRootRole', + name: "GroupWithNoRootRole", rootRole: adminRole.id, users: [{ user: viewerUser }], }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN), + await accessService.hasPermission(viewerUser, permissions.ADMIN) ).toBe(true); }); -test('Should give full project access to the default project to user in a group with an editor root role', async () => { - const projectName = 'default'; +test("Should give full project access to the default project to user in a group with an editor root role", async () => { + const projectName = "default"; const viewerUser = await createUser(readRole.id); @@ -1134,29 +1134,29 @@ test('Should give full project access to the default project to user in a group await hasFullProjectAccess(viewerUser, projectName, true); }); -test('if user has two roles user has union of permissions from the two roles', async () => { - const projectName = 'default'; +test("if user has two roles user has union of permissions from the two roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1166,38 +1166,38 @@ test('if user has two roles user has union of permissions from the two roles', a ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission), + assignedPermissions.map((p) => p.permission) ); expect(permissionNameSet.size).toBe(3); }); -test('calling set for user overwrites existing roles', async () => { - const projectName = 'default'; +test("calling set for user overwrites existing roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1207,10 +1207,10 @@ test('calling set for user overwrites existing roles', async () => { ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission), + assignedPermissions.map((p) => p.permission) ); expect(permissionNameSet.size).toBe(3); @@ -1220,22 +1220,22 @@ test('calling set for user overwrites existing roles', async () => { ]); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(2); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: 'CREATE_FEATURE', + permission: "CREATE_FEATURE", }); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: 'DELETE_FEATURE', + permission: "DELETE_FEATURE", }); }); -test('if group has two roles user has union of permissions from the two roles', async () => { - const projectName = 'default'; +test("if group has two roles user has union of permissions from the two roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); @@ -1246,21 +1246,21 @@ test('if group has two roles user has union of permissions from the two roles', const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1268,21 +1268,21 @@ test('if group has two roles user has union of permissions from the two roles', projectName, emptyGroup.id, [firstRole.id, secondRole.id], - 'testusr', + "testusr" ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission), + assignedPermissions.map((p) => p.permission) ); expect(permissionNameSet.size).toBe(3); }); -test('calling set for group overwrites existing roles', async () => { - const projectName = 'default'; +test("calling set for group overwrites existing roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); @@ -1293,21 +1293,21 @@ test('calling set for group overwrites existing roles', async () => { const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1315,14 +1315,14 @@ test('calling set for group overwrites existing roles', async () => { projectName, emptyGroup.id, [firstRole.id, secondRole.id], - 'testusr', + "testusr" ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission), + assignedPermissions.map((p) => p.permission) ); expect(permissionNameSet.size).toBe(3); @@ -1331,26 +1331,26 @@ test('calling set for group overwrites existing roles', async () => { projectName, emptyGroup.id, [firstRole.id], - 'testusr', + "testusr" ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(2); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: 'CREATE_FEATURE', + permission: "CREATE_FEATURE", }); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: 'DELETE_FEATURE', + permission: "DELETE_FEATURE", }); }); -test('group with root role can be assigned a project specific role', async () => { - const projectName = 'default'; +test("group with root role can be assigned a project specific role", async () => { + const projectName = "default"; const emptyUser = await createUser(); @@ -1362,7 +1362,7 @@ test('group with root role can be assigned a project specific role', async () => const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, ]); @@ -1370,21 +1370,21 @@ test('group with root role can be assigned a project specific role', async () => projectName, emptyGroup.id, [firstRole.id], - 'testusr', + "testusr" ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(assignedPermissions).toContainEqual({ project: projectName, - permission: 'CREATE_FEATURE', + permission: "CREATE_FEATURE", }); }); -test('calling add access with invalid project role ids should not assign those roles', async () => { - const projectName = 'default'; +test("calling add access with invalid project role ids should not assign those roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const adminRootRole = await accessService.getRoleByName(RoleName.ADMIN); @@ -1394,18 +1394,18 @@ test('calling add access with invalid project role ids should not assign those r [], [emptyUser.id], projectName, - 'some-admin-user', + "some-admin-user" ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('calling set roles for user with invalid project role ids should not assign those roles', async () => { - const projectName = 'default'; +test("calling set roles for user with invalid project role ids should not assign those roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const adminRootRole = await accessService.getRoleByName(RoleName.ADMIN); @@ -1416,20 +1416,20 @@ test('calling set roles for user with invalid project role ids should not assign ]); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('calling set roles for user with empty role array removes all roles', async () => { - const projectName = 'default'; +test("calling set roles for user with empty role array removes all roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const role = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, ]); @@ -1438,7 +1438,7 @@ test('calling set roles for user with empty role array removes all roles', async ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(assignedPermissions.length).toBe(1); @@ -1446,24 +1446,24 @@ test('calling set roles for user with empty role array removes all roles', async await accessService.setProjectRolesForUser(projectName, emptyUser.id, []); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('calling set roles for user with empty role array should not remove root roles', async () => { - const projectName = 'default'; +test("calling set roles for user with empty role array should not remove root roles", async () => { + const projectName = "default"; const adminUser = await createUser(adminRole.id); const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); @@ -1472,7 +1472,7 @@ test('calling set roles for user with empty role array should not remove root ro ]); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(assignedPermissions.length).toBe(3); @@ -1480,32 +1480,32 @@ test('calling set roles for user with empty role array should not remove root ro await accessService.setProjectRolesForUser(projectName, adminUser.id, []); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test('remove user access should remove all project roles', async () => { - const projectName = 'default'; +test("remove user access should remove all project roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1515,7 +1515,7 @@ test('remove user access should remove all project roles', async () => { ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(assignedPermissions.length).toBe(3); @@ -1523,31 +1523,31 @@ test('remove user access should remove all project roles', async () => { await accessService.removeUserAccess(projectName, emptyUser.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('remove user access should remove all project roles, while leaving root roles untouched', async () => { - const projectName = 'default'; +test("remove user access should remove all project roles, while leaving root roles untouched", async () => { + const projectName = "default"; const adminUser = await createUser(adminRole.id); const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1557,7 +1557,7 @@ test('remove user access should remove all project roles, while leaving root rol ]); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(assignedPermissions.length).toBe(4); @@ -1565,15 +1565,15 @@ test('remove user access should remove all project roles, while leaving root rol await accessService.removeUserAccess(projectName, adminUser.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test('calling set roles for group with invalid project role ids should not assign those roles', async () => { - const projectName = 'default'; +test("calling set roles for group with invalid project role ids should not assign those roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const emptyGroup = await createGroup({ @@ -1586,18 +1586,18 @@ test('calling set roles for group with invalid project role ids should not assig projectName, emptyGroup.id, [adminRootRole.id, 9999], - 'admin', + "admin" ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('calling set roles for group with empty role array removes all roles', async () => { - const projectName = 'default'; +test("calling set roles for group with empty role array removes all roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const emptyGroup = await createGroup({ @@ -1607,7 +1607,7 @@ test('calling set roles for group with empty role array removes all roles', asyn const role = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, ]); @@ -1615,11 +1615,11 @@ test('calling set roles for group with empty role array removes all roles', asyn projectName, emptyGroup.id, [role.id], - 'admin', + "admin" ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(assignedPermissions.length).toBe(1); @@ -1628,18 +1628,18 @@ test('calling set roles for group with empty role array removes all roles', asyn projectName, emptyGroup.id, [], - 'admin', + "admin" ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('calling set roles for group with empty role array should not remove root roles', async () => { - const projectName = 'default'; +test("calling set roles for group with empty role array should not remove root roles", async () => { + const projectName = "default"; const adminUser = await createUser(adminRole.id); const group = await createGroup({ @@ -1649,11 +1649,11 @@ test('calling set roles for group with empty role array should not remove root r const role = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); @@ -1661,11 +1661,11 @@ test('calling set roles for group with empty role array should not remove root r projectName, group.id, [role.id], - 'admin', + "admin" ); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(assignedPermissions.length).toBe(3); @@ -1674,19 +1674,19 @@ test('calling set roles for group with empty role array should not remove root r projectName, group.id, [], - 'admin', + "admin" ); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test('remove group access should remove all project roles', async () => { - const projectName = 'default'; +test("remove group access should remove all project roles", async () => { + const projectName = "default"; const emptyUser = await createUser(); const group = await createGroup({ users: [{ user: emptyUser }], @@ -1695,18 +1695,18 @@ test('remove group access should remove all project roles', async () => { const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1714,11 +1714,11 @@ test('remove group access should remove all project roles', async () => { projectName, group.id, [firstRole.id, secondRole.id], - 'admin', + "admin" ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(assignedPermissions.length).toBe(3); @@ -1726,14 +1726,14 @@ test('remove group access should remove all project roles', async () => { await accessService.removeGroupAccess(projectName, group.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser, + emptyUser ); expect(newAssignedPermissions.length).toBe(0); }); -test('remove group access should remove all project roles, while leaving root roles untouched', async () => { - const projectName = 'default'; +test("remove group access should remove all project roles, while leaving root roles untouched", async () => { + const projectName = "default"; const adminUser = await createUser(adminRole.id); const group = await createGroup({ users: [{ user: adminUser }], @@ -1742,18 +1742,18 @@ test('remove group access should remove all project roles, while leaving root ro const firstRole = await createRole([ { id: 2, - name: 'CREATE_FEATURE', + name: "CREATE_FEATURE", }, { id: 8, - name: 'DELETE_FEATURE', + name: "DELETE_FEATURE", }, ]); const secondRole = await createRole([ { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); @@ -1761,11 +1761,11 @@ test('remove group access should remove all project roles, while leaving root ro projectName, group.id, [firstRole.id, secondRole.id], - 'admin', + "admin" ); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(assignedPermissions.length).toBe(4); @@ -1773,19 +1773,19 @@ test('remove group access should remove all project roles, while leaving root ro await accessService.removeGroupAccess(projectName, group.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser, + adminUser ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test('access overview should have admin access and default project for admin user', async () => { - const email = 'a-person@places.com'; +test("access overview should have admin access and default project for admin user", async () => { + const email = "a-person@places.com"; const { userStore } = stores; const user = await userStore.insert({ - name: 'Some User', + name: "Some User", email, }); @@ -1794,28 +1794,28 @@ test('access overview should have admin access and default project for admin use const accessOverView: IUserAccessOverview[] = await accessService.getUserAccessOverview(); const userAccess = accessOverView.find( - (overviewRow) => overviewRow.userId === user.id, + (overviewRow) => overviewRow.userId === user.id )!; expect(userAccess.userId).toBe(user.id); - expect(userAccess.rootRole).toBe('Admin'); - expect(userAccess.accessibleProjects).toStrictEqual(['default']); + expect(userAccess.rootRole).toBe("Admin"); + expect(userAccess.accessibleProjects).toStrictEqual(["default"]); }); -test('access overview should have group access for groups that they are in', async () => { - const email = 'a-nother-person@places.com'; +test("access overview should have group access for groups that they are in", async () => { + const email = "a-nother-person@places.com"; const { userStore } = stores; const user = await userStore.insert({ - name: 'Some Other User', + name: "Some Other User", email, }); await accessService.setUserRootRole(user.id, adminRole.id); const group = await stores.groupStore.create({ - name: 'Test Group', + name: "Test Group", }); await stores.groupStore.addUsersToGroup( @@ -1827,33 +1827,33 @@ test('access overview should have group access for groups that they are in', asy }, }, ], - 'Admin', + "Admin" ); const someGroupRole = await createRole([ { id: 13, - name: 'UPDATE_PROJECT', + name: "UPDATE_PROJECT", }, ]); await accessService.addGroupToRole( group.id, someGroupRole.id, - 'creator', - 'default', + "creator", + "default" ); const accessOverView: IUserAccessOverview[] = await accessService.getUserAccessOverview(); const userAccess = accessOverView.find( - (overviewRow) => overviewRow.userId === user.id, + (overviewRow) => overviewRow.userId === user.id )!; expect(userAccess.userId).toBe(user.id); - expect(userAccess.rootRole).toBe('Admin'); - expect(userAccess.groups).toStrictEqual(['Test Group']); + expect(userAccess.rootRole).toBe("Admin"); + expect(userAccess.groups).toStrictEqual(["Test Group"]); - expect(userAccess.groupProjects).toStrictEqual(['default']); + expect(userAccess.groupProjects).toStrictEqual(["default"]); }); diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index 0e131a8969d6..a5a3e3273ac6 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -1,13 +1,13 @@ -import dbInit from '../helpers/database-init'; -import getLogger from '../../fixtures/no-logger'; -import { ApiTokenService } from '../../../lib/services/api-token-service'; -import { createTestConfig } from '../../config/test-config'; -import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token'; -import { DEFAULT_ENV } from '../../../lib/util/constants'; -import { addDays, subDays } from 'date-fns'; -import ProjectService from '../../../lib/services/project-service'; -import { EventService } from '../../../lib/services'; -import { createProjectService } from '../../../lib/features'; +import dbInit from "../helpers/database-init"; +import getLogger from "../../fixtures/no-logger"; +import { ApiTokenService } from "../../../lib/services/api-token-service"; +import { createTestConfig } from "../../config/test-config"; +import { ApiTokenType, IApiToken } from "../../../lib/types/models/api-token"; +import { DEFAULT_ENV } from "../../../lib/util/constants"; +import { addDays, subDays } from "date-fns"; +import ProjectService from "../../../lib/services/project-service"; +import { createProjectService } from "../../../lib/features"; +import { EventService } from "../../../lib/services"; let db; let stores; @@ -16,21 +16,21 @@ let projectService: ProjectService; beforeAll(async () => { const config = createTestConfig({ - server: { baseUriPath: '/test' }, + server: { baseUriPath: "/test" }, }); - db = await dbInit('api_token_service_serial', getLogger); + db = await dbInit("api_token_service_serial", getLogger); stores = db.stores; const eventService = new EventService(stores, config); const project = { - id: 'test-project', - name: 'Test Project', - description: 'Fancy', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-project", + name: "Test Project", + description: "Fancy", + mode: "open" as const, + defaultStickiness: "clientId", }; const user = await stores.userStore.insert({ - name: 'Some Name', - email: 'test@getunleash.io', + name: "Some Name", + email: "test@getunleash.io", }); projectService = createProjectService(db.rawDatabase, config); @@ -47,23 +47,23 @@ afterAll(async () => { afterEach(async () => { const tokens = await stores.apiTokenStore.getAll(); const deleteAll = tokens.map((t: IApiToken) => - stores.apiTokenStore.delete(t.secret), + stores.apiTokenStore.delete(t.secret) ); await Promise.all(deleteAll); }); -test('should have empty list of tokens', async () => { +test("should have empty list of tokens", async () => { const allTokens = await apiTokenService.getAllTokens(); const activeTokens = await apiTokenService.getAllTokens(); expect(allTokens.length).toBe(0); expect(activeTokens.length).toBe(0); }); -test('should create client token', async () => { +test("should create client token", async () => { const token = await apiTokenService.createApiToken({ - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, - project: '*', + project: "*", environment: DEFAULT_ENV, }); const allTokens = await apiTokenService.getAllTokens(); @@ -71,29 +71,29 @@ test('should create client token', async () => { expect(allTokens.length).toBe(1); expect(token.secret.length > 32).toBe(true); expect(token.type).toBe(ApiTokenType.CLIENT); - expect(token.username).toBe('default-client'); + expect(token.username).toBe("default-client"); expect(allTokens[0].secret).toBe(token.secret); }); -test('should create admin token', async () => { +test("should create admin token", async () => { const token = await apiTokenService.createApiToken({ - tokenName: 'admin', + tokenName: "admin", type: ApiTokenType.ADMIN, - project: '*', - environment: '*', + project: "*", + environment: "*", }); expect(token.secret.length > 32).toBe(true); expect(token.type).toBe(ApiTokenType.ADMIN); }); -test('should set expiry of token', async () => { - const time = new Date('2022-01-01'); +test("should set expiry of token", async () => { + const time = new Date("2022-01-01"); await apiTokenService.createApiToken({ - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, expiresAt: time, - project: '*', + project: "*", environment: DEFAULT_ENV, }); @@ -102,46 +102,46 @@ test('should set expiry of token', async () => { expect(token.expiresAt).toEqual(time); }); -test('should update expiry of token', async () => { - const time = new Date('2022-01-01'); - const newTime = new Date('2023-01-01'); +test("should update expiry of token", async () => { + const time = new Date("2022-01-01"); + const newTime = new Date("2023-01-01"); const token = await apiTokenService.createApiToken( { - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, expiresAt: time, - project: '*', + project: "*", environment: DEFAULT_ENV, }, - 'tester', + "tester" ); - await apiTokenService.updateExpiry(token.secret, newTime, 'tester'); + await apiTokenService.updateExpiry(token.secret, newTime, "tester"); const [updatedToken] = await apiTokenService.getAllTokens(); expect(updatedToken.expiresAt).toEqual(newTime); }); -test('should only return valid tokens', async () => { +test("should only return valid tokens", async () => { const now = Date.now(); const yesterday = subDays(now, 1); const tomorrow = addDays(now, 1); await apiTokenService.createApiToken({ - tokenName: 'default-expired', + tokenName: "default-expired", type: ApiTokenType.CLIENT, expiresAt: yesterday, - project: '*', + project: "*", environment: DEFAULT_ENV, }); const activeToken = await apiTokenService.createApiToken({ - tokenName: 'default-valid', + tokenName: "default-valid", type: ApiTokenType.CLIENT, expiresAt: tomorrow, - project: '*', + project: "*", environment: DEFAULT_ENV, }); @@ -151,70 +151,70 @@ test('should only return valid tokens', async () => { expect(activeToken.secret).toBe(tokens[0].secret); }); -test('should create client token with project list', async () => { +test("should create client token with project list", async () => { const token = await apiTokenService.createApiToken({ - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, - projects: ['default', 'test-project'], + projects: ["default", "test-project"], environment: DEFAULT_ENV, }); - expect(token.secret.slice(0, 2)).toEqual('[]'); - expect(token.projects).toStrictEqual(['default', 'test-project']); + expect(token.secret.slice(0, 2)).toEqual("[]"); + expect(token.projects).toStrictEqual(["default", "test-project"]); }); -test('should strip all other projects if ALL_PROJECTS is present', async () => { +test("should strip all other projects if ALL_PROJECTS is present", async () => { const token = await apiTokenService.createApiToken({ - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, - projects: ['*', 'default'], + projects: ["*", "default"], environment: DEFAULT_ENV, }); - expect(token.projects).toStrictEqual(['*']); + expect(token.projects).toStrictEqual(["*"]); }); -test('should return user with multiple projects', async () => { +test("should return user with multiple projects", async () => { const now = Date.now(); const tomorrow = addDays(now, 1); await apiTokenService.createApiToken({ - tokenName: 'default-valid', + tokenName: "default-valid", type: ApiTokenType.CLIENT, expiresAt: tomorrow, - projects: ['test-project', 'default'], + projects: ["test-project", "default"], environment: DEFAULT_ENV, }); await apiTokenService.createApiToken({ - tokenName: 'default-also-valid', + tokenName: "default-also-valid", type: ApiTokenType.CLIENT, expiresAt: tomorrow, - projects: ['test-project'], + projects: ["test-project"], environment: DEFAULT_ENV, }); const tokens = await apiTokenService.getAllActiveTokens(); const multiProjectUser = await apiTokenService.getUserForToken( - tokens[0].secret, + tokens[0].secret ); const singleProjectUser = await apiTokenService.getUserForToken( - tokens[1].secret, + tokens[1].secret ); expect(multiProjectUser!.projects).toStrictEqual([ - 'test-project', - 'default', + "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 () => { +test("should not partially create token if projects are invalid", async () => { try { await apiTokenService.createApiTokenWithProjects({ - tokenName: 'default-client', + tokenName: "default-client", type: ApiTokenType.CLIENT, - projects: ['non-existent-project'], + projects: ["non-existent-project"], environment: DEFAULT_ENV, }); } catch (e) {} diff --git a/src/test/e2e/services/last-seen-service.e2e.test.ts b/src/test/e2e/services/last-seen-service.e2e.test.ts index c00f05d2a94c..88853686d5e4 100644 --- a/src/test/e2e/services/last-seen-service.e2e.test.ts +++ b/src/test/e2e/services/last-seen-service.e2e.test.ts @@ -1,7 +1,7 @@ import { createTestConfig } from '../../config/test-config'; import dbInit from '../helpers/database-init'; import { IUnleashStores } from '../../../lib/types/stores'; -import { LastSeenService } from '../../../lib/services/client-metrics/last-seen-service'; +import { LastSeenService } from '../../../lib/services/client-metrics/last-seen/last-seen-service'; import { IClientMetricsEnv } from '../../../lib/types/stores/client-metrics-store-v2'; let stores: IUnleashStores; @@ -21,7 +21,7 @@ afterAll(async () => { }); test('Should update last seen for known toggles', async () => { - const service = new LastSeenService(stores, config); + const service = new LastSeenService(stores.lastSeenStore, config); const time = Date.now() - 100; await stores.featureToggleStore.create('default', { name: 'ta1' }); @@ -56,7 +56,7 @@ test('Should update last seen for known toggles', async () => { test('Should not update last seen toggles with 0 metrics', async () => { // jest.useFakeTimers(); - const service = new LastSeenService(stores, config, 30); + const service = new LastSeenService(stores.lastSeenStore, config, 30); const time = Date.now(); await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb2' }); @@ -96,7 +96,7 @@ test('Should not update last seen toggles with 0 metrics', async () => { test('Should not update anything for 0 toggles', async () => { // jest.useFakeTimers(); - const service = new LastSeenService(stores, config, 30); + const service = new LastSeenService(stores.lastSeenStore, config, 30); const time = Date.now(); await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb2' }); diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index be6de7e8363e..6bcfd4bccfd9 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -1,14 +1,14 @@ -import dbInit, { ITestDb } from '../helpers/database-init'; -import getLogger from '../../fixtures/no-logger'; -import ProjectHealthService from '../../../lib/services/project-health-service'; -import { createTestConfig } from '../../config/test-config'; -import { IUnleashStores } from '../../../lib/types'; -import { IUser } from '../../../lib/server-impl'; -import { EventService } from '../../../lib/services'; +import dbInit, { ITestDb } from "../helpers/database-init"; +import getLogger from "../../fixtures/no-logger"; +import ProjectHealthService from "../../../lib/services/project-health-service"; +import { createTestConfig } from "../../config/test-config"; +import { IUnleashStores } from "../../../lib/types"; +import { IUser } from "../../../lib/server-impl"; import { createFeatureToggleService, createProjectService, -} from '../../../lib/features'; +} from "../../../lib/features"; +import { EventService } from "../../../lib/services"; let stores: IUnleashStores; let db: ITestDb; @@ -20,11 +20,11 @@ let user: IUser; beforeAll(async () => { const config = createTestConfig(); - db = await dbInit('project_health_service_serial', getLogger); + db = await dbInit("project_health_service_serial", getLogger); stores = db.stores; user = await stores.userStore.insert({ - name: 'Some Name', - email: 'test@getunleash.io', + name: "Some Name", + email: "test@getunleash.io", }); eventService = new EventService(stores, config); @@ -34,95 +34,95 @@ beforeAll(async () => { projectHealthService = new ProjectHealthService( stores, config, - projectService, + projectService ); }); afterAll(async () => { await db.destroy(); }); -test('Project with no stale toggles should have 100% health rating', async () => { +test("Project with no stale toggles should have 100% health rating", async () => { const project = { - id: 'health-rating', - name: 'Health rating', - description: 'Fancy', + id: "health-rating", + name: "Health rating", + description: "Fancy", }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create('health-rating', { - name: 'health-rating-not-stale', - description: 'new', + await stores.featureToggleStore.create("health-rating", { + name: "health-rating-not-stale", + description: "new", stale: false, }); - await stores.featureToggleStore.create('health-rating', { - name: 'health-rating-not-stale-2', - description: 'new too', + await stores.featureToggleStore.create("health-rating", { + name: "health-rating-not-stale-2", + description: "new too", stale: false, }); const rating = await projectHealthService.calculateHealthRating( - savedProject, + savedProject ); expect(rating).toBe(100); }); -test('Project with two stale toggles and two non stale should have 50% health rating', async () => { +test("Project with two stale toggles and two non stale should have 50% health rating", async () => { const project = { - id: 'health-rating-2', - name: 'Health rating', - description: 'Fancy', + id: "health-rating-2", + name: "Health rating", + description: "Fancy", }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create('health-rating-2', { - name: 'health-rating-2-not-stale', - description: 'new', + await stores.featureToggleStore.create("health-rating-2", { + name: "health-rating-2-not-stale", + description: "new", stale: false, }); - await stores.featureToggleStore.create('health-rating-2', { - name: 'health-rating-2-not-stale-2', - description: 'new too', + await stores.featureToggleStore.create("health-rating-2", { + name: "health-rating-2-not-stale-2", + description: "new too", stale: false, }); - await stores.featureToggleStore.create('health-rating-2', { - name: 'health-rating-2-stale-1', - description: 'stale', + await stores.featureToggleStore.create("health-rating-2", { + name: "health-rating-2-stale-1", + description: "stale", stale: true, }); - await stores.featureToggleStore.create('health-rating-2', { - name: 'health-rating-2-stale-2', - description: 'stale too', + await stores.featureToggleStore.create("health-rating-2", { + name: "health-rating-2-stale-2", + description: "stale too", stale: true, }); const rating = await projectHealthService.calculateHealthRating( - savedProject, + savedProject ); expect(rating).toBe(50); }); -test('Project with one non-stale, one potentially stale and one stale should have 33% health rating', async () => { +test("Project with one non-stale, one potentially stale and one stale should have 33% health rating", async () => { const project = { - id: 'health-rating-3', - name: 'Health rating', - description: 'Fancy', + id: "health-rating-3", + name: "Health rating", + description: "Fancy", }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create('health-rating-3', { - name: 'health-rating-3-not-stale', - description: 'new', + await stores.featureToggleStore.create("health-rating-3", { + name: "health-rating-3-not-stale", + description: "new", stale: false, }); - await stores.featureToggleStore.create('health-rating-3', { - name: 'health-rating-3-potentially-stale', - description: 'new too', - type: 'release', + await stores.featureToggleStore.create("health-rating-3", { + name: "health-rating-3-potentially-stale", + description: "new too", + type: "release", stale: false, createdAt: new Date(Date.UTC(2020, 1, 1)), }); - await stores.featureToggleStore.create('health-rating-3', { - name: 'health-rating-3-stale', - description: 'stale', + await stores.featureToggleStore.create("health-rating-3", { + name: "health-rating-3-stale", + description: "stale", stale: true, }); const rating = await projectHealthService.calculateHealthRating( - savedProject, + savedProject ); expect(rating).toBe(33); }); diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index d539ecab860b..0ab780032dc0 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -1,22 +1,22 @@ -import dbInit, { ITestDb } from '../helpers/database-init'; -import getLogger from '../../fixtures/no-logger'; -import FeatureToggleService from '../../../lib/services/feature-toggle-service'; -import ProjectService from '../../../lib/services/project-service'; -import { AccessService } from '../../../lib/services/access-service'; -import { MOVE_FEATURE_TOGGLE } from '../../../lib/types/permissions'; -import { createTestConfig } from '../../config/test-config'; -import { RoleName } from '../../../lib/types/model'; -import { randomId } from '../../../lib/util/random-id'; -import EnvironmentService from '../../../lib/services/environment-service'; -import IncompatibleProjectError from '../../../lib/error/incompatible-project-error'; -import { EventService } from '../../../lib/services'; -import { FeatureEnvironmentEvent } from '../../../lib/types/events'; -import { addDays, subDays } from 'date-fns'; +import dbInit, { ITestDb } from "../helpers/database-init"; +import getLogger from "../../fixtures/no-logger"; +import FeatureToggleService from "../../../lib/services/feature-toggle-service"; +import ProjectService from "../../../lib/services/project-service"; +import { AccessService } from "../../../lib/services/access-service"; +import { MOVE_FEATURE_TOGGLE } from "../../../lib/types/permissions"; +import { createTestConfig } from "../../config/test-config"; +import { RoleName } from "../../../lib/types/model"; +import { randomId } from "../../../lib/util/random-id"; +import EnvironmentService from "../../../lib/services/environment-service"; +import IncompatibleProjectError from "../../../lib/error/incompatible-project-error"; +import { EventService } from "../../../lib/services"; +import { FeatureEnvironmentEvent } from "../../../lib/types/events"; +import { addDays, subDays } from "date-fns"; import { createAccessService, createFeatureToggleService, createProjectService, -} from '../../../lib/features'; +} from "../../../lib/features"; let stores; let db: ITestDb; @@ -31,19 +31,19 @@ let user; const isProjectUser = async ( userId: number, projectName: string, - condition: boolean, + condition: boolean ) => { expect(await projectService.isProjectUser(userId, projectName)).toBe( - condition, + condition ); }; beforeAll(async () => { - db = await dbInit('project_service_serial', getLogger); + db = await dbInit("project_service_serial", getLogger); stores = db.stores; user = await stores.userStore.insert({ - name: 'Some Name', - email: 'test@getunleash.io', + name: "Some Name", + email: "test@getunleash.io", }); const config = createTestConfig({ getLogger, @@ -68,7 +68,7 @@ afterAll(async () => { afterEach(async () => { const envs = await stores.environmentStore.getAll(); const deleteEnvs = envs - .filter((env) => env.name !== 'default') + .filter((env) => env.name !== "default") .map(async (env) => { await stores.environmentStore.delete(env.name); }); @@ -81,19 +81,19 @@ afterEach(async () => { await Promise.allSettled(wipeUserPermissions); }); -test('should have default project', async () => { - const project = await projectService.getProject('default'); +test("should have default project", async () => { + const project = await projectService.getProject("default"); expect(project).toBeDefined(); - expect(project.id).toBe('default'); + expect(project.id).toBe("default"); }); -test('should list all projects', async () => { +test("should list all projects", async () => { const project = { - id: 'test-list', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'default', + id: "test-list", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "default", }; await projectService.createProject(project, user); @@ -102,45 +102,45 @@ test('should list all projects', async () => { expect(projects.find((p) => p.name === project.name)?.memberCount).toBe(1); }); -test('should create new project', async () => { +test("should create new project", async () => { const project = { - id: 'test', - name: 'New project', - description: 'Blah', - defaultStickiness: 'default', + id: "test", + name: "New project", + description: "Blah", + defaultStickiness: "default", }; await projectService.createProject(project, user); - const ret = await projectService.getProject('test'); + const ret = await projectService.getProject("test"); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); }); -test('should create new private project', async () => { +test("should create new private project", async () => { const project = { - id: 'testPrivate', - name: 'New private project', - description: 'Blah', - defaultStickiness: 'default', + id: "testPrivate", + name: "New private project", + description: "Blah", + defaultStickiness: "default", }; await projectService.createProject(project, user); - const ret = await projectService.getProject('testPrivate'); + const ret = await projectService.getProject("testPrivate"); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); }); -test('should delete project', async () => { +test("should delete project", async () => { const project = { - id: 'test-delete', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'default', + id: "test-delete", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "default", }; await projectService.createProject(project, user); @@ -149,95 +149,95 @@ test('should delete project', async () => { try { await projectService.getProject(project.id); } catch (err) { - expect(err.message).toBe('No project found'); + expect(err.message).toBe("No project found"); } }); -test('should not be able to delete project with toggles', async () => { +test("should not be able to delete project with toggles", async () => { const project = { - id: 'test-delete-with-toggles', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-delete-with-toggles", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: 'test-project-delete', + name: "test-project-delete", project: project.id, enabled: false, - defaultStickiness: 'default', + defaultStickiness: "default", }); try { await projectService.deleteProject(project.id, user); } catch (err) { expect(err.message).toBe( - 'You can not delete a project with active feature toggles', + "You can not delete a project with active feature toggles" ); } }); test('should not delete "default" project', async () => { try { - await projectService.deleteProject('default', user); + await projectService.deleteProject("default", user); } catch (err) { - expect(err.message).toBe('You can not delete the default project!'); + expect(err.message).toBe("You can not delete the default project!"); } }); -test('should validate name, legal', async () => { - const result = await projectService.validateId('new_name'); +test("should validate name, legal", async () => { + const result = await projectService.validateId("new_name"); expect(result).toBe(true); }); -test('should not be able to create existing project', async () => { +test("should not be able to create existing project", async () => { const project = { - id: 'test-delete', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'default', + id: "test-delete", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "default", }; try { await projectService.createProject(project, user); await projectService.createProject(project, user); } catch (err) { - expect(err.message).toBe('A project with this id already exists.'); + expect(err.message).toBe("A project with this id already exists."); } }); -test('should require URL friendly ID', async () => { +test("should require URL friendly ID", async () => { try { - await projectService.validateId('new name øæå'); + await projectService.validateId("new name øæå"); } catch (err) { expect(err.message).toBe('"value" must be URL friendly'); } }); -test('should require unique ID', async () => { +test("should require unique ID", async () => { try { - await projectService.validateId('default'); + await projectService.validateId("default"); } catch (err) { - expect(err.message).toBe('A project with this id already exists.'); + expect(err.message).toBe("A project with this id already exists."); } }); -test('should update project', async () => { +test("should update project", async () => { const project = { - id: 'test-update', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'default', + id: "test-update", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "default", }; const updatedProject = { - id: 'test-update', - name: 'New name', - description: 'Blah longer desc', - mode: 'protected' as const, - defaultStickiness: 'userId', + id: "test-update", + name: "New name", + description: "Blah longer desc", + mode: "protected" as const, + defaultStickiness: "userId", }; await projectService.createProject(project, user); @@ -247,30 +247,30 @@ test('should update project', async () => { expect(updatedProject.name).toBe(readProject.name); expect(updatedProject.description).toBe(readProject.description); - expect(updatedProject.mode).toBe('protected'); - expect(updatedProject.defaultStickiness).toBe('userId'); + expect(updatedProject.mode).toBe("protected"); + expect(updatedProject.defaultStickiness).toBe("userId"); }); -test('should update project without existing settings', async () => { +test("should update project without existing settings", async () => { const project = { - id: 'test-update-legacy', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'default', + id: "test-update-legacy", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "default", }; const updatedProject = { - id: 'test-update-legacy', - name: 'New name', - description: 'Blah longer desc', - mode: 'protected' as const, - defaultStickiness: 'clientId', + id: "test-update-legacy", + name: "New name", + description: "Blah longer desc", + mode: "protected" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); await db - .rawDatabase('project_settings') + .rawDatabase("project_settings") .del() .where({ project: project.id }); await projectService.updateProject(updatedProject, user); @@ -279,25 +279,25 @@ test('should update project without existing settings', async () => { expect(updatedProject.name).toBe(readProject.name); expect(updatedProject.description).toBe(readProject.description); - expect(updatedProject.mode).toBe('protected'); - expect(updatedProject.defaultStickiness).toBe('clientId'); + expect(updatedProject.mode).toBe("protected"); + expect(updatedProject.defaultStickiness).toBe("clientId"); }); -test('should give error when getting unknown project', async () => { +test("should give error when getting unknown project", async () => { try { - await projectService.getProject('unknown'); + await projectService.getProject("unknown"); } catch (err) { - expect(err.message).toBe('No project found'); + expect(err.message).toBe("No project found"); } }); -test('should get list of users with access to project', async () => { +test("should get list of users with access to project", async () => { const project = { - id: 'test-roles-access', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-roles-access", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const { users } = await projectService.getAccessToProject(project.id); @@ -314,23 +314,23 @@ test('should get list of users with access to project', async () => { await isProjectUser(users[0].id, project.id, true); }); -test('should add a member user to the project', async () => { +test("should add a member user to the project", async () => { const project = { - id: 'add-users', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "add-users", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'member1@getunleash.io', + name: "Some Member", + email: "member1@getunleash.io", }); const projectMember2 = await stores.userStore.insert({ - name: 'Some Member 2', - email: 'member2@getunleash.io', + name: "Some Member 2", + email: "member2@getunleash.io", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -339,13 +339,13 @@ test('should add a member user to the project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await projectService.addUser( project.id, memberRole.id, projectMember2.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -371,23 +371,23 @@ test('should add a member user to the project', async () => { ]); }); -test('should add admin users to the project', async () => { +test("should add admin users to the project", async () => { const project = { - id: 'add-admin-users', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "add-admin-users", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectAdmin1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'admin1@getunleash.io', + name: "Some Member", + email: "admin1@getunleash.io", }); const projectAdmin2 = await stores.userStore.insert({ - name: 'Some Member 2', - email: 'admin2@getunleash.io', + name: "Some Member 2", + email: "admin2@getunleash.io", }); const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); @@ -396,13 +396,13 @@ test('should add admin users to the project', async () => { project.id, ownerRole.id, projectAdmin1.id, - 'test', + "test" ); await projectService.addUser( project.id, ownerRole.id, projectAdmin2.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -419,19 +419,19 @@ test('should add admin users to the project', async () => { await isProjectUser(adminUsers[2].id, project.id, true); }); -test('add user should fail if user already have access', async () => { +test("add user should fail if user already have access", async () => { const project = { - id: 'add-users-twice', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "add-users-twice", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'member42@getunleash.io', + name: "Some Member", + email: "member42@getunleash.io", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -440,7 +440,7 @@ test('add user should fail if user already have access', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await expect(async () => @@ -448,26 +448,26 @@ test('add user should fail if user already have access', async () => { project.id, memberRole.id, projectMember1.id, - 'test', - ), + "test" + ) ).rejects.toThrow( - new Error('User already has access to project=add-users-twice'), + new Error("User already has access to project=add-users-twice") ); }); -test('should remove user from the project', async () => { +test("should remove user from the project", async () => { const project = { - id: 'remove-users', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "remove-users", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'member99@getunleash.io', + name: "Some Member", + email: "member99@getunleash.io", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -476,13 +476,13 @@ test('should remove user from the project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await projectService.removeUser( project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -491,13 +491,13 @@ test('should remove user from the project', async () => { expect(memberUsers).toHaveLength(0); }); -test('should not remove user from the project', async () => { +test("should not remove user from the project", async () => { const project = { - id: 'remove-users-not-allowed', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "remove-users-not-allowed", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); @@ -509,87 +509,87 @@ test('should not remove user from the project', async () => { project.id, ownerRole.id, user.id, - 'test', + "test" ); }).rejects.toThrowError( - new Error('A project must have at least one owner'), + new Error("A project must have at least one owner") ); }); -test('should not change project if feature toggle project does not match current project id', async () => { +test("should not change project if feature toggle project does not match current project id", async () => { const project = { - id: 'test-change-project', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-change-project", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; - const toggle = { name: 'test-toggle' }; + const toggle = { name: "test-toggle" }; await projectService.createProject(project, user); await featureToggleService.createFeatureToggle(project.id, toggle, user); try { await projectService.changeProject( - 'newProject', + "newProject", toggle.name, user, - 'wrong-project-id', + "wrong-project-id" ); } catch (err) { - expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); + expect(err.message.toLowerCase().includes("permission")).toBeTruthy(); expect(err.message.includes(MOVE_FEATURE_TOGGLE)).toBeTruthy(); } }); -test('should return 404 if no project is found with the project id', async () => { +test("should return 404 if no project is found with the project id", async () => { const project = { - id: 'test-change-project-2', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-change-project-2", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; - const toggle = { name: 'test-toggle-2' }; + const toggle = { name: "test-toggle-2" }; await projectService.createProject(project, user); await featureToggleService.createFeatureToggle(project.id, toggle, user); try { await projectService.changeProject( - 'newProject', + "newProject", toggle.name, user, - project.id, + project.id ); } catch (err) { expect(err.message).toBe(`No project found`); } }); -test('should fail if user is not authorized', async () => { +test("should fail if user is not authorized", async () => { const project = { - id: 'test-change-project-3', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-change-project-3", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; const projectDestination = { - id: 'test-change-project-dest', - name: 'New project 2', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-change-project-dest", + name: "New project 2", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; - const toggle = { name: 'test-toggle-3' }; + const toggle = { name: "test-toggle-3" }; const projectAdmin1 = await stores.userStore.insert({ - name: 'test-change-project-creator', - email: 'admin-change-project@getunleash.io', + name: "test-change-project-creator", + email: "admin-change-project@getunleash.io", }); await projectService.createProject(project, user); @@ -601,26 +601,26 @@ test('should fail if user is not authorized', async () => { projectDestination.id, toggle.name, user, - project.id, + project.id ); } catch (err) { - expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); + expect(err.message.toLowerCase().includes("permission")).toBeTruthy(); expect(err.message.includes(MOVE_FEATURE_TOGGLE)).toBeTruthy(); } }); -test('should change project when checks pass', async () => { +test("should change project when checks pass", async () => { const projectA = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'clientId', + mode: "open" as const, + defaultStickiness: "clientId", }; const projectB = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'clientId', + mode: "open" as const, + defaultStickiness: "clientId", }; const toggle = { name: randomId() }; @@ -631,7 +631,7 @@ test('should change project when checks pass', async () => { projectB.id, toggle.name, user, - projectA.id, + projectA.id ); const updatedFeature = await featureToggleService.getFeature({ @@ -640,18 +640,18 @@ test('should change project when checks pass', async () => { expect(updatedFeature.project).toBe(projectB.id); }); -test('changing project should emit event even if user does not have a username set', async () => { +test("changing project should emit event even if user does not have a username set", async () => { const projectA = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'default', + mode: "open" as const, + defaultStickiness: "default", }; const projectB = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'clientId', + mode: "open" as const, + defaultStickiness: "clientId", }; const toggle = { name: randomId() }; await projectService.createProject(projectA, user); @@ -662,26 +662,26 @@ test('changing project should emit event even if user does not have a username s projectB.id, toggle.name, user, - projectA.id, + projectA.id ); const eventsAfterChange = await stores.eventStore.getEvents(); expect(eventsAfterChange.length).toBe(eventsBeforeChange.length + 1); }, 10000); -test('should require equal project environments to move features', async () => { +test("should require equal project environments to move features", async () => { const projectA = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'clientId', + mode: "open" as const, + defaultStickiness: "clientId", }; const projectB = { id: randomId(), name: randomId(), - mode: 'open' as const, - defaultStickiness: 'clientId', + mode: "open" as const, + defaultStickiness: "clientId", }; - const environment = { name: randomId(), type: 'production' }; + const environment = { name: randomId(), type: "production" }; const toggle = { name: randomId() }; await projectService.createProject(projectA, user); @@ -690,7 +690,7 @@ test('should require equal project environments to move features', async () => { await stores.environmentStore.create(environment); await environmentService.addEnvironmentToProject( environment.name, - projectB.id, + projectB.id ); await expect(() => @@ -698,28 +698,28 @@ test('should require equal project environments to move features', async () => { projectB.id, toggle.name, user, - projectA.id, - ), + projectA.id + ) ).rejects.toThrowError(IncompatibleProjectError); }); -test('A newly created project only gets connected to enabled environments', async () => { +test("A newly created project only gets connected to enabled environments", async () => { const project = { - id: 'environment-test', - name: 'New environment project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "environment-test", + name: "New environment project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; - const enabledEnv = 'connection_test'; + const enabledEnv = "connection_test"; await db.stores.environmentStore.create({ name: enabledEnv, - type: 'test', + type: "test", }); - const disabledEnv = 'do_not_connect'; + const disabledEnv = "do_not_connect"; await db.stores.environmentStore.create({ name: disabledEnv, - type: 'test', + type: "test", enabled: false, }); @@ -728,43 +728,43 @@ test('A newly created project only gets connected to enabled environments', asyn await db.stores.projectStore.getEnvironmentsForProject(project.id); expect(connectedEnvs).toHaveLength(2); // default, connection_test expect( - connectedEnvs.some((e) => e.environment === enabledEnv), + connectedEnvs.some((e) => e.environment === enabledEnv) ).toBeTruthy(); expect( - connectedEnvs.some((e) => e.environment === disabledEnv), + connectedEnvs.some((e) => e.environment === disabledEnv) ).toBeFalsy(); }); -test('should have environments sorted in order', async () => { +test("should have environments sorted in order", async () => { const project = { - id: 'environment-order-test', - name: 'Environment testing project', - description: '', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "environment-order-test", + name: "Environment testing project", + description: "", + mode: "open" as const, + defaultStickiness: "clientId", }; - const first = 'test'; - const second = 'abc'; - const third = 'example'; - const fourth = 'mock'; + const first = "test"; + const second = "abc"; + const third = "example"; + const fourth = "mock"; await db.stores.environmentStore.create({ name: first, - type: 'test', + type: "test", sortOrder: 1, }); await db.stores.environmentStore.create({ name: fourth, - type: 'test', + type: "test", sortOrder: 4, }); await db.stores.environmentStore.create({ name: third, - type: 'test', + type: "test", sortOrder: 3, }); await db.stores.environmentStore.create({ name: second, - type: 'test', + type: "test", sortOrder: 2, }); @@ -773,7 +773,7 @@ test('should have environments sorted in order', async () => { await db.stores.projectStore.getEnvironmentsForProject(project.id); expect(connectedEnvs.map((e) => e.environment)).toEqual([ - 'default', + "default", first, second, third, @@ -781,24 +781,24 @@ test('should have environments sorted in order', async () => { ]); }); -test('should add a user to the project with a custom role', async () => { +test("should add a user to the project with a custom role", async () => { const project = { - id: 'add-users-custom-role', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "add-users-custom-role", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Custom', - email: 'custom@getunleash.io', + name: "Custom", + email: "custom@getunleash.io", }); const customRole = await accessService.createRole({ - name: 'Service Engineer2', - description: '', + name: "Service Engineer2", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -813,7 +813,7 @@ test('should add a user to the project with a custom role', async () => { project.id, customRole.id, projectMember1.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -825,30 +825,30 @@ test('should add a user to the project with a custom role', async () => { expect(customRoleMember[0].name).toBe(projectMember1.name); }); -test('should delete role entries when deleting project', async () => { +test("should delete role entries when deleting project", async () => { const project = { - id: 'test-delete-users-1', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-delete-users-1", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const user1 = await stores.userStore.insert({ - name: 'Projectuser1', - email: 'project1@getunleash.io', + name: "Projectuser1", + email: "project1@getunleash.io", }); const user2 = await stores.userStore.insert({ - name: 'Projectuser2', - email: 'project2@getunleash.io', + name: "Projectuser2", + email: "project2@getunleash.io", }); const customRole = await accessService.createRole({ - name: 'Service Engineer', - description: '', + name: "Service Engineer", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -859,8 +859,8 @@ test('should delete role entries when deleting project', async () => { ], }); - await projectService.addUser(project.id, customRole.id, user1.id, 'test'); - await projectService.addUser(project.id, customRole.id, user2.id, 'test'); + await projectService.addUser(project.id, customRole.id, user1.id, "test"); + await projectService.addUser(project.id, customRole.id, user2.id, "test"); let usersForRole = await accessService.getUsersForRole(customRole.id); expect(usersForRole.length).toBe(2); @@ -870,25 +870,25 @@ test('should delete role entries when deleting project', async () => { expect(usersForRole.length).toBe(0); }); -test('should change a users role in the project', async () => { +test("should change a users role in the project", async () => { const project = { - id: 'test-change-user-role', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "test-change-user-role", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectUser = await stores.userStore.insert({ - name: 'Projectuser3', - email: 'project3@getunleash.io', + name: "Projectuser3", + email: "project3@getunleash.io", }); const customRole = await accessService.createRole({ - name: 'Service Engineer3', - description: '', + name: "Service Engineer3", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -900,7 +900,7 @@ test('should change a users role in the project', async () => { }); const member = await stores.roleStore.getRoleByName(RoleName.MEMBER); - await projectService.addUser(project.id, member.id, projectUser.id, 'test'); + await projectService.addUser(project.id, member.id, projectUser.id, "test"); const { users } = await projectService.getAccessToProject(project.id); const memberUser = users.filter((u) => u.roleId === member.id); @@ -911,17 +911,17 @@ test('should change a users role in the project', async () => { project.id, member.id, projectUser.id, - 'test', + "test" ); await projectService.addUser( project.id, customRole.id, projectUser.id, - 'test', + "test" ); const { users: updatedUsers } = await projectService.getAccessToProject( - project.id, + project.id ); const customUser = updatedUsers.filter((u) => u.roleId === customRole.id); @@ -930,19 +930,19 @@ test('should change a users role in the project', async () => { expect(customUser[0].name).toBe(projectUser.name); }); -test('should update role for user on project', async () => { +test("should update role for user on project", async () => { const project = { - id: 'update-users', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "update-users", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'update99@getunleash.io', + name: "Some Member", + email: "update99@getunleash.io", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -952,13 +952,13 @@ test('should update role for user on project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await projectService.changeRole( project.id, ownerRole.id, projectMember1.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -969,25 +969,25 @@ test('should update role for user on project', async () => { expect(ownerUsers).toHaveLength(2); }); -test('should able to assign role without existing members', async () => { +test("should able to assign role without existing members", async () => { const project = { - id: 'update-users-test', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "update-users-test", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'update1999@getunleash.io', + name: "Some Member", + email: "update1999@getunleash.io", }); const testRole = await stores.roleStore.create({ - name: 'Power user', - roleType: 'custom', - description: 'Grants access to modify all environments', + name: "Power user", + roleType: "custom", + description: "Grants access to modify all environments", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -996,13 +996,13 @@ test('should able to assign role without existing members', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await projectService.changeRole( project.id, testRole.id, projectMember1.id, - 'test', + "test" ); const { users } = await projectService.getAccessToProject(project.id); @@ -1013,19 +1013,19 @@ test('should able to assign role without existing members', async () => { expect(testUsers).toHaveLength(1); }); -test('should not update role for user on project when she is the owner', async () => { +test("should not update role for user on project when she is the owner", async () => { const project = { - id: 'update-users-not-allowed', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "update-users-not-allowed", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: 'Some Member', - email: 'update991@getunleash.io', + name: "Some Member", + email: "update991@getunleash.io", }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -1034,7 +1034,7 @@ test('should not update role for user on project when she is the owner', async ( project.id, memberRole.id, projectMember1.id, - 'test', + "test" ); await expect(async () => { @@ -1042,36 +1042,36 @@ test('should not update role for user on project when she is the owner', async ( project.id, memberRole.id, user.id, - 'test', + "test" ); }).rejects.toThrowError( - new Error('A project must have at least one owner'), + new Error("A project must have at least one owner") ); }); -test('Should allow bulk update of group permissions', async () => { +test("Should allow bulk update of group permissions", async () => { const project = { - id: 'bulk-update-project', - name: 'bulk-update-project', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "bulk-update-project", + name: "bulk-update-project", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const groupStore = stores.groupStore; const user1 = await stores.userStore.insert({ - name: 'Vanessa Viewer', - email: 'vanv@getunleash.io', + name: "Vanessa Viewer", + email: "vanv@getunleash.io", }); const group1 = await groupStore.create({ - name: 'ViewersOnly', - description: '', + name: "ViewersOnly", + description: "", }); const createFeatureRole = await accessService.createRole({ - name: 'CreateRole', - description: '', + name: "CreateRole", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -1084,21 +1084,21 @@ test('Should allow bulk update of group permissions', async () => { [createFeatureRole.id], [group1.id], [user1.id], - 'some-admin-user', + "some-admin-user" ); }); -test('Should bulk update of only users', async () => { - const project = 'bulk-update-project-users'; +test("Should bulk update of only users", async () => { + const project = "bulk-update-project-users"; const user1 = await stores.userStore.insert({ - name: 'Van Viewer', - email: 'vv@getunleash.io', + name: "Van Viewer", + email: "vv@getunleash.io", }); const createFeatureRole = await accessService.createRole({ - name: 'CreateRoleForUsers', - description: '', + name: "CreateRoleForUsers", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -1111,29 +1111,29 @@ test('Should bulk update of only users', async () => { [createFeatureRole.id], [], [user1.id], - 'some-admin-user', + "some-admin-user" ); }); -test('Should allow bulk update of only groups', async () => { +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, - defaultStickiness: 'clientId', + id: "bulk-update-project-only", + name: "bulk-update-project-only", + mode: "open" as const, + defaultStickiness: "clientId", }; const groupStore = stores.groupStore; await projectService.createProject(project, user.id); const group1 = await groupStore.create({ - name: 'ViewersOnly', - description: '', + name: "ViewersOnly", + description: "", }); const createFeatureRole = await accessService.createRole({ - name: 'CreateRoleForGroups', - description: '', + name: "CreateRoleForGroups", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -1146,43 +1146,43 @@ test('Should allow bulk update of only groups', async () => { [createFeatureRole.id], [group1.id], [], - 'some-admin-user', + "some-admin-user" ); }); -test('Should allow permutations of roles, groups and users when adding a new access', async () => { +test("Should allow permutations of roles, groups and users when adding a new access", async () => { const project = { - id: 'project-access-permutations', - name: 'project-access-permutations', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "project-access-permutations", + name: "project-access-permutations", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const group1 = await stores.groupStore.create({ - name: 'permutation-group-1', - description: '', + name: "permutation-group-1", + description: "", }); const group2 = await stores.groupStore.create({ - name: 'permutation-group-2', - description: '', + name: "permutation-group-2", + description: "", }); const user1 = await stores.userStore.insert({ - name: 'permutation-user-1', - email: 'pu1@getunleash.io', + name: "permutation-user-1", + email: "pu1@getunleash.io", }); const user2 = await stores.userStore.insert({ - name: 'permutation-user-2', - email: 'pu2@getunleash.io', + name: "permutation-user-2", + email: "pu2@getunleash.io", }); const role1 = await accessService.createRole({ - name: 'permutation-role-1', - description: '', + name: "permutation-role-1", + description: "", permissions: [ { id: 2, // CREATE_FEATURE @@ -1191,8 +1191,8 @@ test('Should allow permutations of roles, groups and users when adding a new acc }); const role2 = await accessService.createRole({ - name: 'permutation-role-2', - description: '', + name: "permutation-role-2", + description: "", permissions: [ { id: 7, // UPDATE_FEATURE @@ -1205,11 +1205,11 @@ test('Should allow permutations of roles, groups and users when adding a new acc [role1.id, role2.id], [group1.id, group2.id], [user1.id, user2.id], - 'some-admin-user', + "some-admin-user" ); const { users, groups } = await projectService.getAccessToProject( - project.id, + project.id ); expect(users).toHaveLength(2); @@ -1219,53 +1219,53 @@ test('Should allow permutations of roles, groups and users when adding a new acc expect(groups[0].roles).toStrictEqual([role1.id, role2.id]); }); -test('should only count active feature toggles for project', async () => { +test("should only count active feature toggles for project", async () => { const project = { - id: 'only-active', - name: 'New project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "only-active", + name: "New project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: 'only-active-t1', + name: "only-active-t1", project: project.id, enabled: false, }); await stores.featureToggleStore.create(project.id, { - name: 'only-active-t2', + name: "only-active-t2", project: project.id, enabled: false, }); - await featureToggleService.archiveToggle('only-active-t2', 'me'); + await featureToggleService.archiveToggle("only-active-t2", "me"); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); expect(theProject?.featureCount).toBe(1); }); -test('should list projects with all features archived', async () => { +test("should list projects with all features archived", async () => { const project = { - id: 'only-archived', - name: 'Listed project', - description: 'Blah', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "only-archived", + name: "Listed project", + description: "Blah", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: 'archived-toggle', + name: "archived-toggle", project: project.id, enabled: false, }); - await featureToggleService.archiveToggle('archived-toggle', 'me'); + await featureToggleService.archiveToggle("archived-toggle", "me"); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); @@ -1274,34 +1274,34 @@ test('should list projects with all features archived', async () => { const updateEventCreatedAt = async (date: Date, featureName: string) => { return db.rawDatabase - .table('events') + .table("events") .update({ created_at: date }) .where({ feature_name: featureName }); }; const updateFeature = async (featureName: string, update: any) => { return db.rawDatabase - .table('features') + .table("features") .update(update) .where({ name: featureName }); }; -test('should calculate average time to production', async () => { +test("should calculate average time to production", async () => { const project = { - id: 'average-time-to-prod', - name: 'average-time-to-prod', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod", + name: "average-time-to-prod", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const toggles = [ - { name: 'average-prod-time' }, - { name: 'average-prod-time-2' }, - { name: 'average-prod-time-3' }, - { name: 'average-prod-time-4' }, - { name: 'average-prod-time-5' }, + { name: "average-prod-time" }, + { name: "average-prod-time-2" }, + { name: "average-prod-time-3" }, + { name: "average-prod-time-4" }, + { name: "average-prod-time-5" }, ]; const featureToggles = await Promise.all( @@ -1309,9 +1309,9 @@ test('should calculate average time to production', async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user, + user ); - }), + }) ); await Promise.all( @@ -1321,22 +1321,22 @@ test('should calculate average time to production', async () => { enabled: true, project: project.id, featureName: toggle.name, - environment: 'default', - createdBy: 'Fredrik', - }), + environment: "default", + createdBy: "Fredrik", + }) ); - }), + }) ); - await updateEventCreatedAt(subDays(new Date(), 31), 'average-prod-time-5'); + await updateEventCreatedAt(subDays(new Date(), 31), "average-prod-time-5"); await Promise.all( featureToggles.map((toggle) => - updateFeature(toggle.name, { created_at: subDays(new Date(), 15) }), - ), + updateFeature(toggle.name, { created_at: subDays(new Date(), 15) }) + ) ); - await updateFeature('average-prod-time-5', { + await updateFeature("average-prod-time-5", { created_at: subDays(new Date(), 33), }); @@ -1344,110 +1344,110 @@ test('should calculate average time to production', async () => { expect(result.updates.avgTimeToProdCurrentWindow).toBe(11.4); }); -test('should calculate average time to production ignoring some items', async () => { +test("should calculate average time to production ignoring some items", async () => { const project = { - id: 'average-time-to-prod-corner-cases', - name: 'average-time-to-prod', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod-corner-cases", + name: "average-time-to-prod", + mode: "open" as const, + defaultStickiness: "clientId", }; const makeEvent = (featureName: string) => ({ enabled: true, project: project.id, featureName, - environment: 'default', - createdBy: 'Fredrik', + environment: "default", + createdBy: "Fredrik", tags: [], }); await projectService.createProject(project, user.id); await stores.environmentStore.create({ - name: 'customEnv', - type: 'development', + name: "customEnv", + type: "development", }); - await environmentService.addEnvironmentToProject('customEnv', project.id); + await environmentService.addEnvironmentToProject("customEnv", project.id); // actual toggle we take for calculations - const toggle = { name: 'main-toggle' }; + const toggle = { name: "main-toggle" }; await featureToggleService.createFeatureToggle(project.id, toggle, user); await updateFeature(toggle.name, { created_at: subDays(new Date(), 20), }); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(toggle.name)), + new FeatureEnvironmentEvent(makeEvent(toggle.name)) ); // ignore events added after first enabled await updateEventCreatedAt(addDays(new Date(), 1), toggle.name); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(toggle.name)), + new FeatureEnvironmentEvent(makeEvent(toggle.name)) ); // ignore toggles enabled in non-prod envs - const devToggle = { name: 'dev-toggle' }; + const devToggle = { name: "dev-toggle" }; await featureToggleService.createFeatureToggle(project.id, devToggle, user); await eventService.storeEvent( new FeatureEnvironmentEvent({ ...makeEvent(devToggle.name), - environment: 'customEnv', - }), + environment: "customEnv", + }) ); // ignore toggles from other projects - const otherProjectToggle = { name: 'other-project' }; + const otherProjectToggle = { name: "other-project" }; await featureToggleService.createFeatureToggle( - 'default', + "default", otherProjectToggle, - user, + user ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)), + new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)) ); // ignore non-release toggles - const nonReleaseToggle = { name: 'permission-toggle', type: 'permission' }; + const nonReleaseToggle = { name: "permission-toggle", type: "permission" }; await featureToggleService.createFeatureToggle( project.id, nonReleaseToggle, - user, + user ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)), + new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)) ); // ignore toggles with events before toggle creation time - const previouslyDeleteToggle = { name: 'previously-deleted' }; + const previouslyDeleteToggle = { name: "previously-deleted" }; await featureToggleService.createFeatureToggle( project.id, previouslyDeleteToggle, - user, + user ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)), + new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)) ); await updateEventCreatedAt( subDays(new Date(), 30), - previouslyDeleteToggle.name, + previouslyDeleteToggle.name ); const result = await projectService.getStatusUpdates(project.id); expect(result.updates.avgTimeToProdCurrentWindow).toBe(20); }); -test('should get correct amount of features created in current and past window', async () => { +test("should get correct amount of features created in current and past window", async () => { const project = { - id: 'features-created', - name: 'features-created', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "features-created", + name: "features-created", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const toggles = [ - { name: 'features-created' }, - { name: 'features-created-2' }, - { name: 'features-created-3' }, - { name: 'features-created-4' }, + { name: "features-created" }, + { name: "features-created-2" }, + { name: "features-created-3" }, + { name: "features-created-4" }, ]; await Promise.all( @@ -1455,9 +1455,9 @@ test('should get correct amount of features created in current and past window', return featureToggleService.createFeatureToggle( project.id, toggle, - user, + user ); - }), + }) ); await Promise.all([ @@ -1470,21 +1470,21 @@ test('should get correct amount of features created in current and past window', expect(result.updates.createdPastWindow).toBe(2); }); -test('should get correct amount of features archived in current and past window', async () => { +test("should get correct amount of features archived in current and past window", async () => { const project = { - id: 'features-archived', - name: 'features-archived', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "features-archived", + name: "features-archived", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const toggles = [ - { name: 'features-archived' }, - { name: 'features-archived-2' }, - { name: 'features-archived-3' }, - { name: 'features-archived-4' }, + { name: "features-archived" }, + { name: "features-archived-2" }, + { name: "features-archived-3" }, + { name: "features-archived-4" }, ]; await Promise.all( @@ -1492,9 +1492,9 @@ test('should get correct amount of features archived in current and past window' return featureToggleService.createFeatureToggle( project.id, toggle, - user, + user ); - }), + }) ); await Promise.all([ @@ -1521,26 +1521,26 @@ test('should get correct amount of features archived in current and past window' expect(result.updates.archivedPastWindow).toBe(2); }); -test('should get correct amount of project members for current and past window', async () => { +test("should get correct amount of project members for current and past window", async () => { const project = { - id: 'features-members', - name: 'features-members', - mode: 'open' as const, - defaultStickiness: 'default', + id: "features-members", + name: "features-members", + mode: "open" as const, + defaultStickiness: "default", }; await projectService.createProject(project, user.id); const users = [ - { name: 'memberOne', email: 'memberOne@getunleash.io' }, - { name: 'memberTwo', email: 'memberTwo@getunleash.io' }, - { name: 'memberThree', email: 'memberThree@getunleash.io' }, - { name: 'memberFour', email: 'memberFour@getunleash.io' }, - { name: 'memberFive', email: 'memberFive@getunleash.io' }, + { name: "memberOne", email: "memberOne@getunleash.io" }, + { name: "memberTwo", email: "memberTwo@getunleash.io" }, + { name: "memberThree", email: "memberThree@getunleash.io" }, + { name: "memberFour", email: "memberFour@getunleash.io" }, + { name: "memberFive", email: "memberFive@getunleash.io" }, ]; const createdUsers = await Promise.all( - users.map((userObj) => stores.userStore.insert(userObj)), + users.map((userObj) => stores.userStore.insert(userObj)) ); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -1550,9 +1550,9 @@ test('should get correct amount of project members for current and past window', project.id, memberRole.id, createdUser.id, - 'test', - ), - ), + "test" + ) + ) ); const result = await projectService.getStatusUpdates(project.id); @@ -1561,22 +1561,22 @@ test('should get correct amount of project members for current and past window', expect(result.updates.projectActivityPastWindow).toBe(0); }); -test('should return average time to production per toggle', async () => { +test("should return average time to production per toggle", async () => { const project = { - id: 'average-time-to-prod-per-toggle', - name: 'average-time-to-prod-per-toggle', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod-per-toggle", + name: "average-time-to-prod-per-toggle", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project, user.id); const toggles = [ - { name: 'average-prod-time-pt', subdays: 7 }, - { name: 'average-prod-time-pt-2', subdays: 14 }, - { name: 'average-prod-time-pt-3', subdays: 40 }, - { name: 'average-prod-time-pt-4', subdays: 15 }, - { name: 'average-prod-time-pt-5', subdays: 2 }, + { name: "average-prod-time-pt", subdays: 7 }, + { name: "average-prod-time-pt-2", subdays: 14 }, + { name: "average-prod-time-pt-3", subdays: 40 }, + { name: "average-prod-time-pt-4", subdays: 15 }, + { name: "average-prod-time-pt-5", subdays: 2 }, ]; const featureToggles = await Promise.all( @@ -1584,9 +1584,9 @@ test('should return average time to production per toggle', async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user, + user ); - }), + }) ); await Promise.all( @@ -1596,19 +1596,19 @@ test('should return average time to production per toggle', async () => { enabled: true, project: project.id, featureName: toggle.name, - environment: 'default', - createdBy: 'Fredrik', - }), + environment: "default", + createdBy: "Fredrik", + }) ); - }), + }) ); await Promise.all( toggles.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }), - ), + }) + ) ); const result = await projectService.getDoraMetrics(project.id); @@ -1618,33 +1618,33 @@ test('should return average time to production per toggle', async () => { expect(result.projectAverage).toBeTruthy(); }); -test('should return average time to production per toggle for a specific project', async () => { +test("should return average time to production per toggle for a specific project", async () => { const project1 = { - id: 'average-time-to-prod-per-toggle-1', - name: 'Project 1', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod-per-toggle-1", + name: "Project 1", + mode: "open" as const, + defaultStickiness: "clientId", }; const project2 = { - id: 'average-time-to-prod-per-toggle-2', - name: 'Project 2', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod-per-toggle-2", + name: "Project 2", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project1, user.id); await projectService.createProject(project2, user.id); const togglesProject1 = [ - { name: 'average-prod-time-pt-10', subdays: 7 }, - { name: 'average-prod-time-pt-11', subdays: 14 }, - { name: 'average-prod-time-pt-12', subdays: 40 }, + { name: "average-prod-time-pt-10", subdays: 7 }, + { name: "average-prod-time-pt-11", subdays: 14 }, + { name: "average-prod-time-pt-12", subdays: 40 }, ]; const togglesProject2 = [ - { name: 'average-prod-time-pt-13', subdays: 15 }, - { name: 'average-prod-time-pt-14', subdays: 2 }, + { name: "average-prod-time-pt-13", subdays: 15 }, + { name: "average-prod-time-pt-14", subdays: 2 }, ]; const featureTogglesProject1 = await Promise.all( @@ -1652,9 +1652,9 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project1.id, toggle, - user, + user ); - }), + }) ); const featureTogglesProject2 = await Promise.all( @@ -1662,9 +1662,9 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project2.id, toggle, - user, + user ); - }), + }) ); await Promise.all( @@ -1674,11 +1674,11 @@ test('should return average time to production per toggle for a specific project enabled: true, project: project1.id, featureName: toggle.name, - environment: 'default', - createdBy: 'Fredrik', - }), + environment: "default", + createdBy: "Fredrik", + }) ); - }), + }) ); await Promise.all( @@ -1688,27 +1688,27 @@ test('should return average time to production per toggle for a specific project enabled: true, project: project2.id, featureName: toggle.name, - environment: 'default', - createdBy: 'Fredrik', - }), + environment: "default", + createdBy: "Fredrik", + }) ); - }), + }) ); await Promise.all( togglesProject1.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }), - ), + }) + ) ); await Promise.all( togglesProject2.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }), - ), + }) + ) ); const resultProject1 = await projectService.getDoraMetrics(project1.id); @@ -1718,20 +1718,20 @@ test('should return average time to production per toggle for a specific project expect(resultProject2.features).toHaveLength(2); }); -test('should return average time to production per toggle and include archived toggles', async () => { +test("should return average time to production per toggle and include archived toggles", async () => { const project1 = { - id: 'average-time-to-prod-per-toggle-12', - name: 'Project 1', - mode: 'open' as const, - defaultStickiness: 'clientId', + id: "average-time-to-prod-per-toggle-12", + name: "Project 1", + mode: "open" as const, + defaultStickiness: "clientId", }; await projectService.createProject(project1, user.id); const togglesProject1 = [ - { name: 'average-prod-time-pta-10', subdays: 7 }, - { name: 'average-prod-time-pta-11', subdays: 14 }, - { name: 'average-prod-time-pta-12', subdays: 40 }, + { name: "average-prod-time-pta-10", subdays: 7 }, + { name: "average-prod-time-pta-11", subdays: 14 }, + { name: "average-prod-time-pta-12", subdays: 40 }, ]; const featureTogglesProject1 = await Promise.all( @@ -1739,9 +1739,9 @@ test('should return average time to production per toggle and include archived t return featureToggleService.createFeatureToggle( project1.id, toggle, - user, + user ); - }), + }) ); await Promise.all( @@ -1751,42 +1751,42 @@ test('should return average time to production per toggle and include archived t enabled: true, project: project1.id, featureName: toggle.name, - environment: 'default', - createdBy: 'Fredrik', - }), + environment: "default", + createdBy: "Fredrik", + }) ); - }), + }) ); await Promise.all( togglesProject1.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }), - ), + }) + ) ); - await featureToggleService.archiveToggle('average-prod-time-pta-12', user); + await featureToggleService.archiveToggle("average-prod-time-pta-12", user); const resultProject1 = await projectService.getDoraMetrics(project1.id); expect(resultProject1.features).toHaveLength(3); }); -describe('feature flag naming patterns', () => { +describe("feature flag naming patterns", () => { test(`should clear existing example and description if the payload doesn't contain them`, async () => { const featureNaming = { - pattern: '.+', - example: 'example', - description: 'description', + pattern: ".+", + example: "example", + description: "description", }; const project = { - id: 'feature-flag-naming-patterns-cleanup', - name: 'Project', - mode: 'open' as const, - defaultStickiness: 'clientId', - description: 'description', + id: "feature-flag-naming-patterns-cleanup", + name: "Project", + mode: "open" as const, + defaultStickiness: "clientId", + description: "description", featureNaming, }; @@ -1795,16 +1795,16 @@ describe('feature flag naming patterns', () => { await projectService.updateProjectEnterpriseSettings(project, user); expect( - (await projectService.getProject(project.id)).featureNaming, + (await projectService.getProject(project.id)).featureNaming ).toMatchObject(featureNaming); - const newPattern = 'new-pattern.+'; + const newPattern = "new-pattern.+"; await projectService.updateProjectEnterpriseSettings( { ...project, featureNaming: { pattern: newPattern }, }, - user.id, + user.id ); const updatedProject = await projectService.getProject(project.id); diff --git a/src/test/fixtures/fake-feature-toggle-store.ts b/src/test/fixtures/fake-feature-toggle-store.ts index 19a191211cd9..bb6835592eda 100644 --- a/src/test/fixtures/fake-feature-toggle-store.ts +++ b/src/test/fixtures/fake-feature-toggle-store.ts @@ -9,7 +9,7 @@ import { IFeatureEnvironment, IVariant, } from 'lib/types/model'; -import { LastSeenInput } from '../../lib/services/client-metrics/last-seen-service'; +import { LastSeenInput } from '../../lib/services/client-metrics/last-seen/last-seen-service'; import { EnvironmentFeatureNames } from '../../lib/db/feature-toggle-store'; export default class FakeFeatureToggleStore implements IFeatureToggleStore { diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 805583442034..bfa2015695b2 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -85,6 +85,7 @@ const createStores: () => IUnleashStores = () => { importTogglesStore: {} as IImportTogglesStore, privateProjectStore: {} as IPrivateProjectStore, dependentFeaturesStore: new FakeDependentFeaturesStore(), + lastSeenStore: { setLastSeen: async () => {} }, }; }; From 5e95ffcbd6e5ed0500599d6acf6d1ade1042ce28 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Wed, 4 Oct 2023 21:11:32 +0200 Subject: [PATCH 02/16] feat: map last seen to environments --- .../last-seen/fake-last-seen-read-model.ts | 7 ++-- .../last-seen/last-seen-read-model.ts | 29 +++++++++++--- .../types/last-seen-read-model-type.ts | 6 +-- src/lib/services/project-service.ts | 38 ++++++++++++++++--- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts index 063f3b59dfab..e0f143af9d54 100644 --- a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts +++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts @@ -1,10 +1,9 @@ +import { IFeatureLastSeenResults } from './last-seen-read-model'; import { ILastSeenReadModel } from './types/last-seen-read-model-type'; export class FakeLastSeenReadModel implements ILastSeenReadModel { - getForFeature( - features: string[], - ): Promise<{ lastSeen: Date; environment: string }[]> { + getForFeature(features: string[]): Promise { features.map((feature) => feature); - return Promise.resolve([]); + return Promise.resolve({}); } } diff --git a/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts index 2514f236bd57..9980f98332e8 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-read-model.ts @@ -3,6 +3,13 @@ import { ILastSeenReadModel } from './types/last-seen-read-model-type'; const TABLE = 'last_seen_at_metrics'; +export interface IFeatureLastSeenResults { + [featureName: string]: { + [environment: string]: { + lastSeen: string; + }; + }; +} export class LastSeenAtReadModel implements ILastSeenReadModel { private db: Db; @@ -10,13 +17,25 @@ export class LastSeenAtReadModel implements ILastSeenReadModel { this.db = db; } - async getForFeature( - features: string[], - ): Promise<{ lastSeen: Date; environment: string }[]> { + async getForFeature(features: string[]): Promise { const rows = await this.db(TABLE).whereIn('feature_name', features); - console.log(rows); + const result = rows.reduce((acc, curr) => { + if (!acc[curr.feature_name]) { + acc[curr.feature_name] = {}; + + acc[curr.feature_name][curr.environment] = { + lastSeen: curr.last_seen_at, + }; + } else { + acc[curr.feature_name][curr.environment] = { + lastSeen: curr.last_seen_at, + }; + } + + return acc; + }, {}); - return [{ lastSeen: new Date(), environment: 'string' }]; + return result; } } diff --git a/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts index 60d540798204..ec06efd63671 100644 --- a/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts +++ b/src/lib/services/client-metrics/last-seen/types/last-seen-read-model-type.ts @@ -1,5 +1,5 @@ +import { IFeatureLastSeenResults } from '../last-seen-read-model'; + export interface ILastSeenReadModel { - getForFeature( - features: string[], - ): Promise<{ lastSeen: Date; environment: string }[]>; + getForFeature(features: string[]): Promise; } diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 45036e76078c..dcd876943d05 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -1076,11 +1076,39 @@ export default class ProjectService { this.projectStatsStore.getProjectStats(projectId), ]); - //const featureNames = features.map((feature) => feature.name); - // const lastSeenAtPerEnvironment = - // await this.lastSeenReadModel.getForFeature(featureNames); + const featureNames = features.map((feature) => feature.name); + const lastSeenAtPerEnvironment = + await this.lastSeenReadModel.getForFeature(featureNames); // Get - // console.log(features); + + const mappedFeatures = features.map((feature) => { + if (!feature.environments) { + console.warn('Feature without environments:', feature); + return feature; + } + + feature.environments = feature.environments.map((environment) => { + if ( + !lastSeenAtPerEnvironment[feature.name] || + !lastSeenAtPerEnvironment[feature.name][environment.name] + ) { + console.warn( + 'No last seen data for environment:', + environment, + ); + return environment; + } + + environment.lastSeenAt = new Date( + lastSeenAtPerEnvironment[feature.name][ + environment.name + ].lastSeen, + ); + return environment; + }); + + return feature; + }); return { stats: projectStats, @@ -1095,7 +1123,7 @@ export default class ProjectService { updatedAt: project.updatedAt, createdAt: project.createdAt, environments, - features, + features: mappedFeatures, members, version: 1, }; From 93d4ffddc67ff399f17ed6eea4e45fa6f40c3890 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 10:39:35 +0200 Subject: [PATCH 03/16] fix: add feature flag --- .../client-metrics/last-seen-service.test.ts | 8 +- .../last-seen/createLastSeenService.ts | 13 +- .../last-seen/last-seen-service.ts | 19 ++- .../client-metrics/metrics-service-v2.test.ts | 8 +- src/lib/services/project-service.ts | 1 + src/lib/types/experimental.ts | 127 +++++++++--------- .../services/last-seen-service.e2e.test.ts | 26 +++- 7 files changed, 132 insertions(+), 70 deletions(-) diff --git a/src/lib/services/client-metrics/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen-service.test.ts index b4da24078c27..8275963fdf6b 100644 --- a/src/lib/services/client-metrics/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen-service.test.ts @@ -20,7 +20,13 @@ function initLastSeenService(flagEnabled = true) { }, } as unknown as IUnleashConfig; - const lastSeenService = new LastSeenService(stores.lastSeenStore, config); + const lastSeenService = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + ); return { lastSeenService, featureToggleStore: stores.featureToggleStore }; } diff --git a/src/lib/services/client-metrics/last-seen/createLastSeenService.ts b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts index 288a5692d4b3..1dac8feeeb48 100644 --- a/src/lib/services/client-metrics/last-seen/createLastSeenService.ts +++ b/src/lib/services/client-metrics/last-seen/createLastSeenService.ts @@ -1,3 +1,5 @@ +import FakeFeatureToggleStore from '../../../../test/fixtures/fake-feature-toggle-store'; +import FeatureToggleStore from '../../../db/feature-toggle-store'; import { Db, IUnleashConfig } from '../../../server-impl'; import { FakeLastSeenStore } from './fake-last-seen-store'; import { LastSeenService } from './last-seen-service'; @@ -13,13 +15,20 @@ export const createLastSeenService = ( config.getLogger, ); - return new LastSeenService(lastSeenStore, config); + const featureToggleStore = new FeatureToggleStore( + db, + config.eventBus, + config.getLogger, + ); + + return new LastSeenService({ lastSeenStore, featureToggleStore }, config); }; export const createFakeLastSeenService = ( config: IUnleashConfig, ): LastSeenService => { const lastSeenStore = new FakeLastSeenStore(); + const featureToggleStore = new FakeFeatureToggleStore(); - return new LastSeenService(lastSeenStore, config); + return new LastSeenService({ lastSeenStore, featureToggleStore }, config); }; diff --git a/src/lib/services/client-metrics/last-seen/last-seen-service.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.ts index a05d1c0ac6b7..2f438a95c890 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-service.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-service.ts @@ -3,6 +3,7 @@ import { Logger } from '../../../logger'; import { IUnleashConfig } from '../../../server-impl'; import { IClientMetricsEnv } from '../../../types/stores/client-metrics-store-v2'; import { ILastSeenStore } from './types/last-seen-store-type'; +import { IFeatureToggleStore, IUnleashStores } from '../../../../lib/types'; export type LastSeenInput = { featureName: string; @@ -18,15 +19,24 @@ export class LastSeenService { private lastSeenStore: ILastSeenStore; + private featureToggleStore: IFeatureToggleStore; + + private config: IUnleashConfig; + constructor( - lastSeenStore: ILastSeenStore, + { + featureToggleStore, + lastSeenStore, + }: Pick, config: IUnleashConfig, lastSeenInterval = secondsToMilliseconds(30), ) { this.lastSeenStore = lastSeenStore; + this.featureToggleStore = featureToggleStore; this.logger = config.getLogger( '/services/client-metrics/last-seen-service.ts', ); + this.config = config; this.timers.push( setInterval(() => this.store(), lastSeenInterval).unref(), @@ -41,7 +51,12 @@ export class LastSeenService { this.logger.debug( `Updating last seen for ${lastSeenToggles.length} toggles`, ); - await this.lastSeenStore.setLastSeen(lastSeenToggles); + + if (this.config.flagResolver.isEnabled('useLastSeenRefactor')) { + await this.lastSeenStore.setLastSeen(lastSeenToggles); + } else { + await this.featureToggleStore.setLastSeen(lastSeenToggles); + } } return count; } diff --git a/src/lib/services/client-metrics/metrics-service-v2.test.ts b/src/lib/services/client-metrics/metrics-service-v2.test.ts index 5fe9671ac8b3..c744689d9d9e 100644 --- a/src/lib/services/client-metrics/metrics-service-v2.test.ts +++ b/src/lib/services/client-metrics/metrics-service-v2.test.ts @@ -23,7 +23,13 @@ function initClientMetrics(flagEnabled = true) { }, } as unknown as IUnleashConfig; - const lastSeenService = new LastSeenService(stores.lastSeenStore, config); + const lastSeenService = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + ); lastSeenService.updateLastSeen = jest.fn(); const service = new ClientMetricsServiceV2(stores, config, lastSeenService); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index dcd876943d05..735db2572e3f 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -1104,6 +1104,7 @@ export default class ProjectService { environment.name ].lastSeen, ); + return environment; }); diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index a02a311e8916..d50cd792c7b8 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -1,38 +1,39 @@ -import { Variant, PayloadType } from 'unleash-client'; -import { parseEnvVarBoolean } from '../util'; -import { getDefaultVariant } from 'unleash-client/lib/variant'; +import { Variant, PayloadType } from "unleash-client"; +import { parseEnvVarBoolean } from "../util"; +import { getDefaultVariant } from "unleash-client/lib/variant"; export type IFlagKey = - | 'anonymiseEventLog' - | 'embedProxy' - | 'embedProxyFrontend' - | 'responseTimeWithAppNameKillSwitch' - | 'maintenanceMode' - | 'messageBanner' - | 'featuresExportImport' - | 'caseInsensitiveInOperators' - | 'strictSchemaValidation' - | 'proPlanAutoCharge' - | 'personalAccessTokensKillSwitch' - | 'migrationLock' - | 'demo' - | 'googleAuthEnabled' - | 'disableBulkToggle' - | 'disableNotifications' - | 'advancedPlayground' - | 'filterInvalidClientMetrics' - | 'lastSeenByEnvironment' - | 'customRootRolesKillSwitch' - | 'multipleRoles' - | 'featureNamingPattern' - | 'doraMetrics' - | 'variantTypeNumber' - | 'accessOverview' - | 'privateProjects' - | 'dependentFeatures' - | 'datadogJsonTemplate' - | 'disableMetrics' - | 'transactionalDecorator'; + | "anonymiseEventLog" + | "embedProxy" + | "embedProxyFrontend" + | "responseTimeWithAppNameKillSwitch" + | "maintenanceMode" + | "messageBanner" + | "featuresExportImport" + | "caseInsensitiveInOperators" + | "strictSchemaValidation" + | "proPlanAutoCharge" + | "personalAccessTokensKillSwitch" + | "migrationLock" + | "demo" + | "googleAuthEnabled" + | "disableBulkToggle" + | "disableNotifications" + | "advancedPlayground" + | "filterInvalidClientMetrics" + | "lastSeenByEnvironment" + | "customRootRolesKillSwitch" + | "multipleRoles" + | "featureNamingPattern" + | "doraMetrics" + | "variantTypeNumber" + | "accessOverview" + | "privateProjects" + | "dependentFeatures" + | "datadogJsonTemplate" + | "disableMetrics" + | "transactionalDecorator" + | "useLastSeenRefactor"; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -40,117 +41,121 @@ const flags: IFlags = { anonymiseEventLog: false, embedProxy: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY, - true, + true ), embedProxyFrontend: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY_FRONTEND, - true, + true ), responseTimeWithAppNameKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_RESPONSE_TIME_WITH_APP_NAME_KILL_SWITCH, - false, + false ), maintenanceMode: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE, - false, + false ), messageBanner: { - name: 'message-banner', + name: "message-banner", enabled: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER, - false, + false ), payload: { type: PayloadType.JSON, value: - process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER_PAYLOAD ?? '', + process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER_PAYLOAD ?? "", }, }, featuresExportImport: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_FEATURES_EXPORT_IMPORT, - true, + true ), caseInsensitiveInOperators: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CASE_INSENSITIVE_IN_OPERATORS, - false, + false ), strictSchemaValidation: parseEnvVarBoolean( process.env.UNLEASH_STRICT_SCHEMA_VALIDTION, - false, + false ), proPlanAutoCharge: parseEnvVarBoolean( process.env.UNLEASH_PRO_PLAN_AUTO_CHARGE, - false, + false ), personalAccessTokensKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_PAT_KILL_SWITCH, - false, + false ), migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, true), demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false), googleAuthEnabled: parseEnvVarBoolean( process.env.GOOGLE_AUTH_ENABLED, - false, + false ), disableBulkToggle: parseEnvVarBoolean( process.env.DISABLE_BULK_TOGGLE, - false, + false ), disableNotifications: parseEnvVarBoolean( process.env.DISABLE_NOTIFICATIONS, - false, + false ), filterInvalidClientMetrics: parseEnvVarBoolean( process.env.FILTER_INVALID_CLIENT_METRICS, - false, + false ), lastSeenByEnvironment: parseEnvVarBoolean( process.env.LAST_SEEN_BY_ENVIRONMENT, - false, + false ), customRootRolesKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH, - false, + false ), multipleRoles: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MULTIPLE_ROLES, - false, + false ), featureNamingPattern: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_FEATURE_NAMING_PATTERN, - false, + false ), doraMetrics: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DORA_METRICS, - false, + false ), dependentFeatures: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DEPENDENT_FEATURES, - false, + false ), variantTypeNumber: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER, - false, + false ), privateProjects: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS, - false, + false ), accessOverview: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW, - false, + false ), datadogJsonTemplate: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DATADOG_JSON_TEMPLATE, - false, + false ), disableMetrics: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS, - false, + false ), transactionalDecorator: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_TRANSACTIONAL_DECORATOR, - false, + false + ), + useLastSeenRefactor: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR, + false ), }; diff --git a/src/test/e2e/services/last-seen-service.e2e.test.ts b/src/test/e2e/services/last-seen-service.e2e.test.ts index 88853686d5e4..c07cd061ec8f 100644 --- a/src/test/e2e/services/last-seen-service.e2e.test.ts +++ b/src/test/e2e/services/last-seen-service.e2e.test.ts @@ -21,7 +21,13 @@ afterAll(async () => { }); test('Should update last seen for known toggles', async () => { - const service = new LastSeenService(stores.lastSeenStore, config); + const service = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + ); const time = Date.now() - 100; await stores.featureToggleStore.create('default', { name: 'ta1' }); @@ -56,7 +62,14 @@ test('Should update last seen for known toggles', async () => { test('Should not update last seen toggles with 0 metrics', async () => { // jest.useFakeTimers(); - const service = new LastSeenService(stores.lastSeenStore, config, 30); + const service = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + 30, + ); const time = Date.now(); await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb2' }); @@ -96,7 +109,14 @@ test('Should not update last seen toggles with 0 metrics', async () => { test('Should not update anything for 0 toggles', async () => { // jest.useFakeTimers(); - const service = new LastSeenService(stores.lastSeenStore, config, 30); + const service = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + 30, + ); const time = Date.now(); await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb2' }); From 1e65a1a644aba13d8d958fe82b2bcc3a1be5b961 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 10:41:47 +0200 Subject: [PATCH 04/16] fix: restore test --- .../last-seen/last-seen-service.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts index e69de29bb2d1..729d83557221 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts @@ -0,0 +1,62 @@ +import createStores from '../../../../test/fixtures/store'; +import EventEmitter from 'events'; +import getLogger from '../../../../test/fixtures/no-logger'; +import { IUnleashConfig } from '../../../types'; + +import { createFakeLastSeenService } from './createLastSeenService'; + +function initLastSeenService(flagEnabled = true) { + const stores = createStores(); + + const eventBus = new EventEmitter(); + eventBus.emit = jest.fn(); + + const config = { + eventBus, + getLogger, + flagResolver: { + isEnabled: () => { + return flagEnabled; + }, + }, + } as unknown as IUnleashConfig; + + const lastSeenService = createFakeLastSeenService(config); + + return { lastSeenService, featureToggleStore: stores.featureToggleStore }; +} + +test('should not add duplicates per feature/environment', async () => { + const { lastSeenService, featureToggleStore } = initLastSeenService(); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + featureToggleStore.setLastSeen = jest.fn(); + await lastSeenService.store(); + + expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([ + { + environment: 'development', + featureName: 'myFeature', + }, + ]); +}); From 03c8364234be2881b228f0b713478bb77b71af78 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 13:51:58 +0200 Subject: [PATCH 05/16] feat: test mapper --- .../client-metrics/last-seen-service.test.ts | 46 ++++++++- .../last-seen/fake-last-seen-read-model.ts | 2 +- .../last-seen/last-seen-mapper.ts | 40 ++++++++ .../last-seen/tests/last-seen-mapper.test.ts | 95 +++++++++++++++++++ src/lib/services/project-service.ts | 46 +++------ src/server-dev.ts | 35 +++---- 6 files changed, 213 insertions(+), 51 deletions(-) create mode 100644 src/lib/services/client-metrics/last-seen/last-seen-mapper.ts create mode 100644 src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts diff --git a/src/lib/services/client-metrics/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen-service.test.ts index 8275963fdf6b..9cd3ea43b012 100644 --- a/src/lib/services/client-metrics/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen-service.test.ts @@ -28,11 +28,15 @@ function initLastSeenService(flagEnabled = true) { config, ); - return { lastSeenService, featureToggleStore: stores.featureToggleStore }; + return { + lastSeenService, + featureToggleStore: stores.featureToggleStore, + lastSeenStore: stores.lastSeenStore, + }; } test('should not add duplicates per feature/environment', async () => { - const { lastSeenService, featureToggleStore } = initLastSeenService(); + const { lastSeenService, featureToggleStore } = initLastSeenService(false); lastSeenService.updateLastSeen([ { @@ -65,3 +69,41 @@ test('should not add duplicates per feature/environment', async () => { }, ]); }); + +test('should call last seen at store with correct data', async () => { + const { lastSeenService, lastSeenStore, featureToggleStore } = + initLastSeenService(true); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + lastSeenStore.setLastSeen = jest.fn(); + featureToggleStore.setLastSeen = jest.fn(); + await lastSeenService.store(); + + expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([ + { + environment: 'development', + featureName: 'myFeature', + }, + ]); + expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0); +}); diff --git a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts index e0f143af9d54..119a64f4ae21 100644 --- a/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts +++ b/src/lib/services/client-metrics/last-seen/fake-last-seen-read-model.ts @@ -2,8 +2,8 @@ import { IFeatureLastSeenResults } from './last-seen-read-model'; import { ILastSeenReadModel } from './types/last-seen-read-model-type'; export class FakeLastSeenReadModel implements ILastSeenReadModel { + // eslint-disable-next-line getForFeature(features: string[]): Promise { - features.map((feature) => feature); return Promise.resolve({}); } } diff --git a/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts new file mode 100644 index 000000000000..9056ef2ca541 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts @@ -0,0 +1,40 @@ +import { Logger } from '../../../logger'; +import { IFeatureOverview } from '../../../types'; +import { IFeatureLastSeenResults } from './last-seen-read-model'; + +export class LastSeenMapper { + mapToFeatures( + features: IFeatureOverview[], + lastSeenAtPerEnvironment: IFeatureLastSeenResults, + logger: Logger, + ): IFeatureOverview[] { + return features.map((feature) => { + if (!feature.environments) { + logger.warn('Feature without environments:', feature); + return feature; + } + + feature.environments = feature.environments.map((environment) => { + const noData = + !lastSeenAtPerEnvironment[feature.name] || + !lastSeenAtPerEnvironment[feature.name][environment.name]; + + if (noData) { + logger.warn( + 'No last seen data for environment:', + environment, + ); + return environment; + } + + environment.lastSeenAt = new Date( + lastSeenAtPerEnvironment[feature.name][ + environment.name + ].lastSeen, + ); + return environment; + }); + return feature; + }); + } +} diff --git a/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts new file mode 100644 index 000000000000..023d873ba2a4 --- /dev/null +++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts @@ -0,0 +1,95 @@ +import { IFeatureOverview } from '../../../../types'; +import { LastSeenMapper } from '../last-seen-mapper'; +import getLogger from '../../../../../test/fixtures/no-logger'; + +test('should produce correct output when mapped', () => { + const mapper = new LastSeenMapper(); + + const inputLastSeen = { + exp: { + production: { lastSeen: '2023-10-05T07:27:04.286Z' }, + development: { lastSeen: '2023-10-04T19:03:29.682Z' }, + }, + 'payment-system': { + production: { lastSeen: '2023-10-05T07:27:04.286Z' }, + development: { lastSeen: '2023-10-04T19:03:29.682Z' }, + }, + }; + + const inputFeatures: IFeatureOverview[] = [ + { + type: 'release', + description: null, + favorite: false, + name: 'payment-system', + // @ts-ignore + createdAt: '2023-06-30T12:57:20.476Z', + // @ts-ignore + lastSeenAt: '2023-10-03T13:08:16.263Z', + stale: false, + impressionData: false, + environments: [ + { + name: 'development', + enabled: false, + type: 'development', + sortOrder: 2, + variantCount: 0, + // @ts-ignore + lastSeenAt: '2023-10-04T19:03:29.682Z', + }, + { + name: 'production', + enabled: true, + type: 'production', + sortOrder: 3, + variantCount: 0, + // @ts-ignore + lastSeenAt: '2023-10-05T07:27:04.286Z', + }, + ], + }, + { + type: 'experiment', + description: null, + favorite: false, + name: 'exp', + // @ts-ignore + createdAt: '2023-09-13T08:08:28.211Z', + // @ts-ignore + lastSeenAt: '2023-10-03T13:08:16.263Z', + stale: false, + impressionData: false, + environments: [ + { + name: 'development', + enabled: false, + type: 'development', + sortOrder: 2, + variantCount: 0, + // @ts-ignore - this type is wrong. Should be string + lastSeenAt: '2023-10-04T19:03:29.682Z', + }, + { + name: 'production', + enabled: true, + type: 'production', + sortOrder: 3, + variantCount: 0, + // @ts-ignore - this type is wrong, should be string + lastSeenAt: '2023-10-05T07:27:04.286Z', + }, + ], + }, + ]; + + const logger = getLogger(); + + const result = mapper.mapToFeatures(inputFeatures, inputLastSeen, logger); + + expect(result[0].environments[0].name).toBe('development'); + expect(result[0].name).toBe('payment-system'); + expect(result[0].environments[0].lastSeenAt).toEqual( + new Date(inputLastSeen['payment-system'].development.lastSeen), + ); +}); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 735db2572e3f..574acc5df0ee 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -66,6 +66,7 @@ import { checkFeatureNamingData } from '../features/feature-naming-pattern/featu import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; import EventService from './event-service'; import { ILastSeenReadModel } from './client-metrics/last-seen/types/last-seen-read-model-type'; +import { LastSeenMapper } from './client-metrics/last-seen/last-seen-mapper'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; @@ -1076,40 +1077,23 @@ export default class ProjectService { this.projectStatsStore.getProjectStats(projectId), ]); - const featureNames = features.map((feature) => feature.name); - const lastSeenAtPerEnvironment = - await this.lastSeenReadModel.getForFeature(featureNames); - // Get + let decoratedFeatures = features; - const mappedFeatures = features.map((feature) => { - if (!feature.environments) { - console.warn('Feature without environments:', feature); - return feature; - } + if (this.flagResolver.isEnabled('useLastSeenRefactor')) { + const mapper = new LastSeenMapper(); - feature.environments = feature.environments.map((environment) => { - if ( - !lastSeenAtPerEnvironment[feature.name] || - !lastSeenAtPerEnvironment[feature.name][environment.name] - ) { - console.warn( - 'No last seen data for environment:', - environment, - ); - return environment; - } - - environment.lastSeenAt = new Date( - lastSeenAtPerEnvironment[feature.name][ - environment.name - ].lastSeen, - ); + const featureNames = features.map((feature) => feature.name); + const lastSeenAtPerEnvironment = + await this.lastSeenReadModel.getForFeature(featureNames); - return environment; - }); + console.log(decoratedFeatures, lastSeenAtPerEnvironment); - return feature; - }); + decoratedFeatures = mapper.mapToFeatures( + decoratedFeatures, + lastSeenAtPerEnvironment, + this.logger, + ); + } return { stats: projectStats, @@ -1124,7 +1108,7 @@ export default class ProjectService { updatedAt: project.updatedAt, createdAt: project.createdAt, environments, - features: mappedFeatures, + features: decoratedFeatures, members, version: 1, }; diff --git a/src/server-dev.ts b/src/server-dev.ts index dd282fb59291..a9e5ec04781b 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -1,25 +1,25 @@ -import { start } from './lib/server-impl'; -import { createConfig } from './lib/create-config'; -import { LogLevel } from './lib/logger'; -import { ApiTokenType } from './lib/types/models/api-token'; +import { start } from "./lib/server-impl"; +import { createConfig } from "./lib/create-config"; +import { LogLevel } from "./lib/logger"; +import { ApiTokenType } from "./lib/types/models/api-token"; process.nextTick(async () => { try { await start( createConfig({ db: { - user: 'unleash_user', - password: 'password', - host: 'localhost', + user: "unleash_user", + password: "password", + host: "localhost", port: 5432, - database: process.env.UNLEASH_DATABASE_NAME || 'unleash', + database: process.env.UNLEASH_DATABASE_NAME || "unleash", schema: process.env.UNLEASH_DATABASE_SCHEMA, ssl: false, - applicationName: 'unleash', + applicationName: "unleash", }, server: { enableRequestLogger: true, - baseUriPath: '', + baseUriPath: "", // keepAliveTimeout: 1, gracefulShutdownEnable: true, // cdnPrefix: 'https://cdn.getunleash.io/unleash/v4.4.1', @@ -46,16 +46,17 @@ process.nextTick(async () => { datadogJsonTemplate: true, dependentFeatures: true, transactionalDecorator: true, + useLastSeenRefactor: true, }, }, authentication: { initApiTokens: [ { - environment: '*', - project: '*', - secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5', + environment: "*", + project: "*", + secret: "*:*.964a287e1b728cb5f4f3e0120df92cb5", type: ApiTokenType.ADMIN, - tokenName: 'some-user', + tokenName: "some-user", }, ], }, @@ -65,12 +66,12 @@ process.nextTick(async () => { maxAge: 4000, }, */ - }), + }) ); } catch (error) { - if (error.code === 'EADDRINUSE') { + if (error.code === "EADDRINUSE") { // eslint-disable-next-line no-console - console.warn('Port in use. You might want to reload once more.'); + console.warn("Port in use. You might want to reload once more."); } else { // eslint-disable-next-line no-console console.error(error); From 3d4991df3429826296a4c86da44feee8105b98ce Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 13:55:05 +0200 Subject: [PATCH 06/16] feat: add foreign key --- src/migrations/20231003113443-last-seen-at-metrics-table.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js index e332d3a425c4..4c67bb59de76 100644 --- a/src/migrations/20231003113443-last-seen-at-metrics-table.js +++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js @@ -8,6 +8,8 @@ exports.up = function (db, callback) { environment VARCHAR(100), last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (feature_name, environment) + FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE + FOREIGN KEY (feature_name) REFERENCES features(feature_name) ON DELETE CASCADE ); CREATE INDEX idx_feature_name From 0fe9c75d2a469addd0db92b89e3566f0c6d9aefe Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 14:53:11 +0200 Subject: [PATCH 07/16] fix: remove foreign key on features --- src/migrations/20231003113443-last-seen-at-metrics-table.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js index 4c67bb59de76..7cce0d8a0100 100644 --- a/src/migrations/20231003113443-last-seen-at-metrics-table.js +++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js @@ -9,7 +9,6 @@ exports.up = function (db, callback) { last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (feature_name, environment) FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE - FOREIGN KEY (feature_name) REFERENCES features(feature_name) ON DELETE CASCADE ); CREATE INDEX idx_feature_name From 9a961a7aaa52da81e2663e0c401e0681ae4411a4 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Thu, 5 Oct 2023 14:57:53 +0200 Subject: [PATCH 08/16] fix: console.log --- src/lib/services/project-service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 574acc5df0ee..d26c710e1a11 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -1086,8 +1086,6 @@ export default class ProjectService { const lastSeenAtPerEnvironment = await this.lastSeenReadModel.getForFeature(featureNames); - console.log(decoratedFeatures, lastSeenAtPerEnvironment); - decoratedFeatures = mapper.mapToFeatures( decoratedFeatures, lastSeenAtPerEnvironment, From d93d6a54f77cf1889f732ec65b5d68988135facb Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 10:38:04 +0200 Subject: [PATCH 09/16] fix: formatting --- .../ContextList/ContextList.test.tsx | 4 +- .../last-seen/last-seen-mapper.ts | 5 +- src/lib/types/experimental.ts | 126 +- src/server-dev.ts | 34 +- .../e2e/services/access-service.e2e.test.ts | 778 ++++++------ .../services/api-token-service.e2e.test.ts | 134 +- .../project-health-service.e2e.test.ts | 110 +- .../e2e/services/project-service.e2e.test.ts | 1094 ++++++++--------- 8 files changed, 1141 insertions(+), 1144 deletions(-) diff --git a/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx b/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx index 90b523d12eb9..609c794ff835 100644 --- a/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx +++ b/frontend/src/component/context/ContextList/ContextList/ContextList.test.tsx @@ -24,9 +24,7 @@ const setupRoutes = () => { test('should show the count of projects and features used in', async () => { setupRoutes(); - render( - , - ); + render(); await screen.findByText('2 feature toggles'); await screen.findByText('3 projects'); diff --git a/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts index 9056ef2ca541..06b075e5c16f 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-mapper.ts @@ -28,9 +28,8 @@ export class LastSeenMapper { } environment.lastSeenAt = new Date( - lastSeenAtPerEnvironment[feature.name][ - environment.name - ].lastSeen, + lastSeenAtPerEnvironment[feature.name][environment.name] + .lastSeen, ); return environment; }); diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index d50cd792c7b8..e23e535278d4 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -1,39 +1,39 @@ -import { Variant, PayloadType } from "unleash-client"; -import { parseEnvVarBoolean } from "../util"; -import { getDefaultVariant } from "unleash-client/lib/variant"; +import { Variant, PayloadType } from 'unleash-client'; +import { parseEnvVarBoolean } from '../util'; +import { getDefaultVariant } from 'unleash-client/lib/variant'; export type IFlagKey = - | "anonymiseEventLog" - | "embedProxy" - | "embedProxyFrontend" - | "responseTimeWithAppNameKillSwitch" - | "maintenanceMode" - | "messageBanner" - | "featuresExportImport" - | "caseInsensitiveInOperators" - | "strictSchemaValidation" - | "proPlanAutoCharge" - | "personalAccessTokensKillSwitch" - | "migrationLock" - | "demo" - | "googleAuthEnabled" - | "disableBulkToggle" - | "disableNotifications" - | "advancedPlayground" - | "filterInvalidClientMetrics" - | "lastSeenByEnvironment" - | "customRootRolesKillSwitch" - | "multipleRoles" - | "featureNamingPattern" - | "doraMetrics" - | "variantTypeNumber" - | "accessOverview" - | "privateProjects" - | "dependentFeatures" - | "datadogJsonTemplate" - | "disableMetrics" - | "transactionalDecorator" - | "useLastSeenRefactor"; + | 'anonymiseEventLog' + | 'embedProxy' + | 'embedProxyFrontend' + | 'responseTimeWithAppNameKillSwitch' + | 'maintenanceMode' + | 'messageBanner' + | 'featuresExportImport' + | 'caseInsensitiveInOperators' + | 'strictSchemaValidation' + | 'proPlanAutoCharge' + | 'personalAccessTokensKillSwitch' + | 'migrationLock' + | 'demo' + | 'googleAuthEnabled' + | 'disableBulkToggle' + | 'disableNotifications' + | 'advancedPlayground' + | 'filterInvalidClientMetrics' + | 'lastSeenByEnvironment' + | 'customRootRolesKillSwitch' + | 'multipleRoles' + | 'featureNamingPattern' + | 'doraMetrics' + | 'variantTypeNumber' + | 'accessOverview' + | 'privateProjects' + | 'dependentFeatures' + | 'datadogJsonTemplate' + | 'disableMetrics' + | 'transactionalDecorator' + | 'useLastSeenRefactor'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -41,121 +41,121 @@ const flags: IFlags = { anonymiseEventLog: false, embedProxy: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY, - true + true, ), embedProxyFrontend: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY_FRONTEND, - true + true, ), responseTimeWithAppNameKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_RESPONSE_TIME_WITH_APP_NAME_KILL_SWITCH, - false + false, ), maintenanceMode: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE, - false + false, ), messageBanner: { - name: "message-banner", + name: 'message-banner', enabled: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER, - false + false, ), payload: { type: PayloadType.JSON, value: - process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER_PAYLOAD ?? "", + process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER_PAYLOAD ?? '', }, }, featuresExportImport: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_FEATURES_EXPORT_IMPORT, - true + true, ), caseInsensitiveInOperators: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CASE_INSENSITIVE_IN_OPERATORS, - false + false, ), strictSchemaValidation: parseEnvVarBoolean( process.env.UNLEASH_STRICT_SCHEMA_VALIDTION, - false + false, ), proPlanAutoCharge: parseEnvVarBoolean( process.env.UNLEASH_PRO_PLAN_AUTO_CHARGE, - false + false, ), personalAccessTokensKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_PAT_KILL_SWITCH, - false + false, ), migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, true), demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false), googleAuthEnabled: parseEnvVarBoolean( process.env.GOOGLE_AUTH_ENABLED, - false + false, ), disableBulkToggle: parseEnvVarBoolean( process.env.DISABLE_BULK_TOGGLE, - false + false, ), disableNotifications: parseEnvVarBoolean( process.env.DISABLE_NOTIFICATIONS, - false + false, ), filterInvalidClientMetrics: parseEnvVarBoolean( process.env.FILTER_INVALID_CLIENT_METRICS, - false + false, ), lastSeenByEnvironment: parseEnvVarBoolean( process.env.LAST_SEEN_BY_ENVIRONMENT, - false + false, ), customRootRolesKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH, - false + false, ), multipleRoles: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_MULTIPLE_ROLES, - false + false, ), featureNamingPattern: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_FEATURE_NAMING_PATTERN, - false + false, ), doraMetrics: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DORA_METRICS, - false + false, ), dependentFeatures: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DEPENDENT_FEATURES, - false + false, ), variantTypeNumber: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER, - false + false, ), privateProjects: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS, - false + false, ), accessOverview: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW, - false + false, ), datadogJsonTemplate: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DATADOG_JSON_TEMPLATE, - false + false, ), disableMetrics: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS, - false + false, ), transactionalDecorator: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_TRANSACTIONAL_DECORATOR, - false + false, ), useLastSeenRefactor: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR, - false + false, ), }; diff --git a/src/server-dev.ts b/src/server-dev.ts index a9e5ec04781b..6f412585a9d0 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -1,25 +1,25 @@ -import { start } from "./lib/server-impl"; -import { createConfig } from "./lib/create-config"; -import { LogLevel } from "./lib/logger"; -import { ApiTokenType } from "./lib/types/models/api-token"; +import { start } from './lib/server-impl'; +import { createConfig } from './lib/create-config'; +import { LogLevel } from './lib/logger'; +import { ApiTokenType } from './lib/types/models/api-token'; process.nextTick(async () => { try { await start( createConfig({ db: { - user: "unleash_user", - password: "password", - host: "localhost", + user: 'unleash_user', + password: 'password', + host: 'localhost', port: 5432, - database: process.env.UNLEASH_DATABASE_NAME || "unleash", + database: process.env.UNLEASH_DATABASE_NAME || 'unleash', schema: process.env.UNLEASH_DATABASE_SCHEMA, ssl: false, - applicationName: "unleash", + applicationName: 'unleash', }, server: { enableRequestLogger: true, - baseUriPath: "", + baseUriPath: '', // keepAliveTimeout: 1, gracefulShutdownEnable: true, // cdnPrefix: 'https://cdn.getunleash.io/unleash/v4.4.1', @@ -52,11 +52,11 @@ process.nextTick(async () => { authentication: { initApiTokens: [ { - environment: "*", - project: "*", - secret: "*:*.964a287e1b728cb5f4f3e0120df92cb5", + environment: '*', + project: '*', + secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5', type: ApiTokenType.ADMIN, - tokenName: "some-user", + tokenName: 'some-user', }, ], }, @@ -66,12 +66,12 @@ process.nextTick(async () => { maxAge: 4000, }, */ - }) + }), ); } catch (error) { - if (error.code === "EADDRINUSE") { + if (error.code === 'EADDRINUSE') { // eslint-disable-next-line no-console - console.warn("Port in use. You might want to reload once more."); + console.warn('Port in use. You might want to reload once more.'); } else { // eslint-disable-next-line no-console console.error(error); diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index c55d6088987a..b28b279863bc 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -1,26 +1,26 @@ -import dbInit, { ITestDb } from "../helpers/database-init"; -import getLogger from "../../fixtures/no-logger"; +import dbInit, { ITestDb } from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; import { AccessService, PermissionRef, -} from "../../../lib/services/access-service"; +} from '../../../lib/services/access-service'; -import * as permissions from "../../../lib/types/permissions"; -import { RoleName } from "../../../lib/types/model"; +import * as permissions from '../../../lib/types/permissions'; +import { RoleName } from '../../../lib/types/model'; import { ICreateGroupUserModel, IUnleashStores, IUserAccessOverview, -} from "../../../lib/types"; -import { createTestConfig } from "../../config/test-config"; -import { DEFAULT_PROJECT } from "../../../lib/types/project"; -import { ALL_PROJECTS } from "../../../lib/util/constants"; +} from '../../../lib/types'; +import { createTestConfig } from '../../config/test-config'; +import { DEFAULT_PROJECT } from '../../../lib/types/project'; +import { ALL_PROJECTS } from '../../../lib/util/constants'; import { createAccessService, createFeatureToggleService, createProjectService, -} from "../../../lib/features"; +} from '../../../lib/features'; let db: ITestDb; let stores: IUnleashStores; @@ -44,7 +44,7 @@ const createUser = async (role?: number) => { await accessService.addUserToRole( user.id, role, - role === readRole.id ? ALL_PROJECTS : DEFAULT_PROJECT + role === readRole.id ? ALL_PROJECTS : DEFAULT_PROJECT, ); return user; }; @@ -62,7 +62,7 @@ const createGroup = async ({ name: `Group ${groupIndex}`, rootRole: role, }); - if (users) await groupStore.addUsersToGroup(group.id, users, "Admin"); + if (users) await groupStore.addUsersToGroup(group.id, users, 'Admin'); return group; }; @@ -76,9 +76,9 @@ const createRole = async (rolePermissions: PermissionRef[]) => { }; const hasCommonProjectAccess = async (user, projectName, condition) => { - const defaultEnv = "default"; - const developmentEnv = "development"; - const productionEnv = "production"; + const defaultEnv = 'default'; + const developmentEnv = 'development'; + const productionEnv = 'production'; const { CREATE_FEATURE, @@ -91,116 +91,116 @@ const hasCommonProjectAccess = async (user, projectName, condition) => { UPDATE_FEATURE_VARIANTS, } = permissions; expect( - await accessService.hasPermission(user, CREATE_FEATURE, projectName) + await accessService.hasPermission(user, CREATE_FEATURE, projectName), ).toBe(condition); expect( - await accessService.hasPermission(user, UPDATE_FEATURE, projectName) + await accessService.hasPermission(user, UPDATE_FEATURE, projectName), ).toBe(condition); expect( - await accessService.hasPermission(user, DELETE_FEATURE, projectName) + await accessService.hasPermission(user, DELETE_FEATURE, projectName), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_VARIANTS, - projectName - ) + projectName, + ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - defaultEnv - ) + defaultEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - defaultEnv - ) + defaultEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - defaultEnv - ) + defaultEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - defaultEnv - ) + defaultEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - developmentEnv - ) + developmentEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - developmentEnv - ) + developmentEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - developmentEnv - ) + developmentEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - developmentEnv - ) + developmentEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, - productionEnv - ) + productionEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, - productionEnv - ) + productionEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, - productionEnv - ) + productionEnv, + ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, - productionEnv - ) + productionEnv, + ), ).toBe(condition); }; @@ -208,23 +208,23 @@ const hasFullProjectAccess = async (user, projectName: string, condition) => { const { DELETE_PROJECT, UPDATE_PROJECT, MOVE_FEATURE_TOGGLE } = permissions; expect( - await accessService.hasPermission(user, DELETE_PROJECT, projectName) + await accessService.hasPermission(user, DELETE_PROJECT, projectName), ).toBe(condition); expect( - await accessService.hasPermission(user, UPDATE_PROJECT, projectName) + await accessService.hasPermission(user, UPDATE_PROJECT, projectName), ).toBe(condition); expect( await accessService.hasPermission( user, MOVE_FEATURE_TOGGLE, - projectName - ) + projectName, + ), ); await hasCommonProjectAccess(user, projectName, condition); }; beforeAll(async () => { - db = await dbInit("access_service_serial", getLogger); + db = await dbInit('access_service_serial', getLogger); stores = db.stores; // projectStore = stores.projectStore; const config = createTestConfig({ @@ -246,20 +246,20 @@ beforeAll(async () => { const testAdmin = await createUser(adminRole.id); await projectService.createProject( { - id: "some-project", - name: "Some project", - description: "Used in the test", + id: 'some-project', + name: 'Some project', + description: 'Used in the test', }, - testAdmin + testAdmin, ); await projectService.createProject( { - id: "unusedprojectname", - name: "Another project not used", - description: "Also used in the test", + id: 'unusedprojectname', + name: 'Another project not used', + description: 'Also used in the test', }, - testAdmin + testAdmin, ); }); @@ -267,7 +267,7 @@ afterAll(async () => { await db.destroy(); }); -test("should have access to admin addons", async () => { +test('should have access to admin addons', async () => { const { CREATE_ADDON, UPDATE_ADDON, DELETE_ADDON } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_ADDON)).toBe(true); @@ -275,7 +275,7 @@ test("should have access to admin addons", async () => { expect(await accessService.hasPermission(user, DELETE_ADDON)).toBe(true); }); -test("should have access to admin strategies", async () => { +test('should have access to admin strategies', async () => { const { CREATE_STRATEGY, UPDATE_STRATEGY, DELETE_STRATEGY } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_STRATEGY)).toBe(true); @@ -283,77 +283,77 @@ test("should have access to admin strategies", async () => { expect(await accessService.hasPermission(user, DELETE_STRATEGY)).toBe(true); }); -test("should have access to admin contexts", async () => { +test('should have access to admin contexts', async () => { const { CREATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_CONTEXT_FIELD)).toBe( - true + true, ); expect(await accessService.hasPermission(user, UPDATE_CONTEXT_FIELD)).toBe( - true + true, ); expect(await accessService.hasPermission(user, DELETE_CONTEXT_FIELD)).toBe( - true + true, ); }); -test("should have access to create projects", async () => { +test('should have access to create projects', async () => { const { CREATE_PROJECT } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_PROJECT)).toBe(true); }); -test("should have access to update applications", async () => { +test('should have access to update applications', async () => { const { UPDATE_APPLICATION } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, UPDATE_APPLICATION)).toBe( - true + true, ); }); -test("should not have admin permission", async () => { +test('should not have admin permission', async () => { const { ADMIN } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, ADMIN)).toBe(false); }); -test("should have project admin to default project as editor", async () => { - const projectName = "default"; +test('should have project admin to default project as editor', async () => { + const projectName = 'default'; const user = editorUser; await hasFullProjectAccess(user, projectName, true); }); -test("should not have project admin to other projects as editor", async () => { - const projectName = "unusedprojectname"; +test('should not have project admin to other projects as editor', async () => { + const projectName = 'unusedprojectname'; const user = editorUser; await hasFullProjectAccess(user, projectName, false); }); -test("cannot add CREATE_FEATURE without defining project", async () => { +test('cannot add CREATE_FEATURE without defining project', async () => { await expect(async () => { await accessService.addPermissionToRole( editorRole.id, - permissions.CREATE_FEATURE + permissions.CREATE_FEATURE, ); }).rejects.toThrow( - new Error("ProjectId cannot be empty for permission=CREATE_FEATURE") + new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), ); }); -test("cannot remove CREATE_FEATURE without defining project", async () => { +test('cannot remove CREATE_FEATURE without defining project', async () => { await expect(async () => { await accessService.removePermissionFromRole( editorRole.id, - permissions.CREATE_FEATURE + permissions.CREATE_FEATURE, ); }).rejects.toThrow( - new Error("ProjectId cannot be empty for permission=CREATE_FEATURE") + new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), ); }); -test("should remove CREATE_FEATURE on default environment", async () => { +test('should remove CREATE_FEATURE on default environment', async () => { const { CREATE_FEATURE } = permissions; const user = editorUser; const editRole = await accessService.getRoleByName(RoleName.EDITOR); @@ -361,7 +361,7 @@ test("should remove CREATE_FEATURE on default environment", async () => { await accessService.addPermissionToRole( editRole.id, permissions.CREATE_FEATURE, - "*" + '*', ); // TODO: to validate the remove works, we should make sure that we had permission before removing it @@ -373,15 +373,15 @@ test("should remove CREATE_FEATURE on default environment", async () => { await accessService.removePermissionFromRole( editRole.id, permissions.CREATE_FEATURE, - "*" + '*', ); expect( - await accessService.hasPermission(user, CREATE_FEATURE, "some-project") + await accessService.hasPermission(user, CREATE_FEATURE, 'some-project'), ).toBe(false); }); -test("admin should be admin", async () => { +test('admin should be admin', async () => { const { DELETE_PROJECT, UPDATE_PROJECT, @@ -392,40 +392,40 @@ test("admin should be admin", async () => { } = permissions; const user = await createUser(adminRole.id); expect( - await accessService.hasPermission(user, DELETE_PROJECT, "default") + await accessService.hasPermission(user, DELETE_PROJECT, 'default'), ).toBe(true); expect( - await accessService.hasPermission(user, UPDATE_PROJECT, "default") + await accessService.hasPermission(user, UPDATE_PROJECT, 'default'), ).toBe(true); expect( - await accessService.hasPermission(user, CREATE_FEATURE, "default") + await accessService.hasPermission(user, CREATE_FEATURE, 'default'), ).toBe(true); expect( - await accessService.hasPermission(user, UPDATE_FEATURE, "default") + await accessService.hasPermission(user, UPDATE_FEATURE, 'default'), ).toBe(true); expect( - await accessService.hasPermission(user, DELETE_FEATURE, "default") + await accessService.hasPermission(user, DELETE_FEATURE, 'default'), ).toBe(true); expect(await accessService.hasPermission(user, ADMIN)).toBe(true); }); -test("should create default roles to project", async () => { - const project = "some-project"; +test('should create default roles to project', async () => { + const project = 'some-project'; const user = editorUser; await accessService.createDefaultProjectRoles(user, project); await hasFullProjectAccess(user, project, true); }); -test("should require name when create default roles to project", async () => { +test('should require name when create default roles to project', async () => { await expect(async () => { // @ts-ignore await accessService.createDefaultProjectRoles(editorUser); - }).rejects.toThrow(new Error("ProjectId cannot be empty")); + }).rejects.toThrow(new Error('ProjectId cannot be empty')); }); -test("should grant user access to project", async () => { +test('should grant user access to project', async () => { const { DELETE_PROJECT, UPDATE_PROJECT } = permissions; - const project = "another-project"; + const project = 'another-project'; const user = editorUser; const sUser = await createUser(readRole.id); await accessService.createDefaultProjectRoles(user, project); @@ -438,15 +438,15 @@ test("should grant user access to project", async () => { // Should not be able to admin the project itself. expect( - await accessService.hasPermission(sUser, UPDATE_PROJECT, project) + await accessService.hasPermission(sUser, UPDATE_PROJECT, project), ).toBe(false); expect( - await accessService.hasPermission(sUser, DELETE_PROJECT, project) + await accessService.hasPermission(sUser, DELETE_PROJECT, project), ).toBe(false); }); -test("should not get access if not specifying project", async () => { - const project = "another-project-2"; +test('should not get access if not specifying project', async () => { + const project = 'another-project-2'; const user = editorUser; const sUser = await createUser(readRole.id); await accessService.createDefaultProjectRoles(user, project); @@ -459,51 +459,51 @@ test("should not get access if not specifying project", async () => { await hasCommonProjectAccess(sUser, undefined, false); }); -test("should remove user from role", async () => { +test('should remove user from role', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random123@getunleash.io", + name: 'Some User', + email: 'random123@getunleash.io', }); - await accessService.addUserToRole(user.id, editorRole.id, "default"); + await accessService.addUserToRole(user.id, editorRole.id, 'default'); // check user has one role const userRoles = await accessService.getRolesForUser(user.id); expect(userRoles.length).toBe(1); expect(userRoles[0].name).toBe(RoleName.EDITOR); - await accessService.removeUserFromRole(user.id, editorRole.id, "default"); + await accessService.removeUserFromRole(user.id, editorRole.id, 'default'); const userRolesAfterRemove = await accessService.getRolesForUser(user.id); expect(userRolesAfterRemove.length).toBe(0); }); -test("should return role with users", async () => { +test('should return role with users', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random2223@getunleash.io", + name: 'Some User', + email: 'random2223@getunleash.io', }); - await accessService.addUserToRole(user.id, editorRole.id, "default"); + await accessService.addUserToRole(user.id, editorRole.id, 'default'); const roleWithUsers = await accessService.getRoleData(editorRole.id); expect(roleWithUsers.role.name).toBe(RoleName.EDITOR); expect(roleWithUsers.users.length >= 2).toBe(true); expect(roleWithUsers.users.find((u) => u.id === user.id)).toBeTruthy(); expect( - roleWithUsers.users.find((u) => u.email === user.email) + roleWithUsers.users.find((u) => u.email === user.email), ).toBeTruthy(); }); -test("should return role with permissions and users", async () => { +test('should return role with permissions and users', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random2244@getunleash.io", + name: 'Some User', + email: 'random2244@getunleash.io', }); - await accessService.addUserToRole(user.id, editorRole.id, "default"); + await accessService.addUserToRole(user.id, editorRole.id, 'default'); const roleWithPermission = await accessService.getRoleData(editorRole.id); @@ -511,19 +511,19 @@ test("should return role with permissions and users", async () => { expect(roleWithPermission.permissions.length > 2).toBe(true); expect( roleWithPermission.permissions.find( - (p) => p.name === permissions.CREATE_PROJECT - ) + (p) => p.name === permissions.CREATE_PROJECT, + ), ).toBeTruthy(); //This assert requires other tests to have run in this pack before length > 2 resolves to true // I've set this to be > 1, which allows us to run the test alone and should still satisfy the logic requirement expect(roleWithPermission.users.length > 1).toBe(true); }); -test("should set root role for user", async () => { +test('should set root role for user', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random2255@getunleash.io", + name: 'Some User', + email: 'random2255@getunleash.io', }); await accessService.setUserRootRole(user.id, editorRole.id); @@ -535,11 +535,11 @@ test("should set root role for user", async () => { expect(roles.length).toBe(1); }); -test("should switch root role for user", async () => { +test('should switch root role for user', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random22Read@getunleash.io", + name: 'Some User', + email: 'random22Read@getunleash.io', }); await accessService.setUserRootRole(user.id, editorRole.id); @@ -551,24 +551,24 @@ test("should switch root role for user", async () => { expect(roles[0].name).toBe(RoleName.VIEWER); }); -test("should switch project roles on when multiple roles are present for same user", async () => { +test('should switch project roles on when multiple roles are present for same user', async () => { const { userStore, roleStore, accessStore } = stores; const userOne = await userStore.insert({ - name: "Some User With Expected Roles", - email: "random42Read@getunleash.io", + name: 'Some User With Expected Roles', + email: 'random42Read@getunleash.io', }); const customRole = await roleStore.create({ - name: "Some Arbitrary Role", - roleType: "custom", - description: "This does nothing", + name: 'Some Arbitrary Role', + roleType: 'custom', + description: 'This does nothing', }); const targetRole = await roleStore.create({ - name: "Another Arbitrary Role", - roleType: "custom", - description: "This does nothing", + name: 'Another Arbitrary Role', + roleType: 'custom', + description: 'This does nothing', }); await accessService.setUserRootRole(userOne.id, editorRole.id); @@ -577,16 +577,16 @@ test("should switch project roles on when multiple roles are present for same us await accessService.updateUserProjectRole( userOne.id, targetRole.id, - DEFAULT_PROJECT + DEFAULT_PROJECT, ); }); -test("should not crash if user does not have permission", async () => { +test('should not crash if user does not have permission', async () => { const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "random55Read@getunleash.io", + name: 'Some User', + email: 'random55Read@getunleash.io', }); await accessService.setUserRootRole(user.id, readRole.id); @@ -594,7 +594,7 @@ test("should not crash if user does not have permission", async () => { const { UPDATE_CONTEXT_FIELD } = permissions; const hasAccess = await accessService.hasPermission( user, - UPDATE_CONTEXT_FIELD + UPDATE_CONTEXT_FIELD, ); expect(hasAccess).toBe(false); @@ -604,31 +604,31 @@ test('should support permission with "ALL" environment requirement', async () => const { userStore, roleStore, accessStore } = stores; const user = await userStore.insert({ - name: "Some User", - email: "randomEnv1@getunleash.io", + name: 'Some User', + email: 'randomEnv1@getunleash.io', }); await accessService.setUserRootRole(user.id, readRole.id); const customRole = await roleStore.create({ - name: "Power user", - roleType: "custom", - description: "Grants access to modify all environments", + name: 'Power user', + roleType: 'custom', + description: 'Grants access to modify all environments', }); const { CREATE_FEATURE_STRATEGY } = permissions; await accessStore.addPermissionsToRole( customRole.id, [{ name: CREATE_FEATURE_STRATEGY }], - "production" + 'production', ); await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS); const hasAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - "default", - "production" + 'default', + 'production', ); expect(hasAccess).toBe(true); @@ -636,100 +636,100 @@ test('should support permission with "ALL" environment requirement', async () => const hasNotAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - "default", - "development" + 'default', + 'development', ); expect(hasNotAccess).toBe(false); }); -test("Should have access to create a strategy in an environment", async () => { +test('Should have access to create a strategy in an environment', async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - "default", - "development" - ) + 'default', + 'development', + ), ).toBe(true); }); -test("Should be denied access to create a strategy in an environment the user does not have access to", async () => { +test('Should be denied access to create a strategy in an environment the user does not have access to', async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, - "default", - "noaccess" - ) + 'default', + 'noaccess', + ), ).toBe(false); }); -test("Should have access to edit a strategy in an environment", async () => { +test('Should have access to edit a strategy in an environment', async () => { const { UPDATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, - "default", - "development" - ) + 'default', + 'development', + ), ).toBe(true); }); -test("Should have access to delete a strategy in an environment", async () => { +test('Should have access to delete a strategy in an environment', async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, - "default", - "development" - ) + 'default', + 'development', + ), ).toBe(true); }); -test("Should be denied access to delete a strategy in an environment the user does not have access to", async () => { +test('Should be denied access to delete a strategy in an environment the user does not have access to', async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, - "default", - "noaccess" - ) + 'default', + 'noaccess', + ), ).toBe(false); }); -test("Should be denied access to delete a role that is in use", async () => { +test('Should be denied access to delete a role that is in use', async () => { const user = editorUser; const project = { - id: "projectToUseRole", - name: "New project", - description: "Blah", + id: 'projectToUseRole', + name: 'New project', + description: 'Blah', }; await projectService.createProject(project, user.id); const projectMember = await stores.userStore.insert({ - name: "CustomProjectMember", - email: "custom@getunleash.io", + name: 'CustomProjectMember', + email: 'custom@getunleash.io', }); const customRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); @@ -739,34 +739,34 @@ test("Should be denied access to delete a role that is in use", async () => { await accessService.deleteRole(customRole.id); } catch (e) { expect(e.toString()).toBe( - "RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups." + 'RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups.', ); } }); -test("Should be denied move feature toggle to project where the user does not have access", async () => { +test('Should be denied move feature toggle to project where the user does not have access', async () => { const user = editorUser; const editorUser2 = await createUser(editorRole.id); const projectOrigin = { - id: "projectOrigin", - name: "New project", - description: "Blah", + id: 'projectOrigin', + name: 'New project', + description: 'Blah', }; const projectDest = { - id: "projectDest", - name: "New project", - description: "Blah", + id: 'projectDest', + name: 'New project', + description: 'Blah', }; await projectService.createProject(projectOrigin, user.id); await projectService.createProject(projectDest, editorUser2.id); - const featureToggle = { name: "moveableToggle" }; + const featureToggle = { name: 'moveableToggle' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username + user.username, ); try { @@ -774,69 +774,69 @@ test("Should be denied move feature toggle to project where the user does not ha projectDest.id, featureToggle.name, user, - projectOrigin.id + projectOrigin.id, ); } catch (e) { - expect(e.name).toContain("Permission"); - expect(e.message.includes("permission")).toBeTruthy(); + expect(e.name).toContain('Permission'); + expect(e.message.includes('permission')).toBeTruthy(); expect( - e.message.includes(permissions.MOVE_FEATURE_TOGGLE) + e.message.includes(permissions.MOVE_FEATURE_TOGGLE), ).toBeTruthy(); } }); -test("Should be allowed move feature toggle to project when the user has access", async () => { +test('Should be allowed move feature toggle to project when the user has access', async () => { const user = editorUser; const projectOrigin = { - id: "projectOrigin1", - name: "New project", - description: "Blah", + id: 'projectOrigin1', + name: 'New project', + description: 'Blah', }; const projectDest = { - id: "projectDest2", - name: "New project", - description: "Blah", + id: 'projectDest2', + name: 'New project', + description: 'Blah', }; await projectService.createProject(projectOrigin, user); await projectService.createProject(projectDest, user); - const featureToggle = { name: "moveableToggle2" }; + const featureToggle = { name: 'moveableToggle2' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username + user.username, ); await projectService.changeProject( projectDest.id, featureToggle.name, user, - projectOrigin.id + projectOrigin.id, ); }); -test("Should not be allowed to edit a root role", async () => { +test('Should not be allowed to edit a root role', async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); const roleUpdate = { id: editRole.id, - name: "NoLongerTheEditor", - description: "", + name: 'NoLongerTheEditor', + description: '', }; try { await accessService.updateRole(roleUpdate); } catch (e) { expect(e.toString()).toBe( - "InvalidOperationError: You cannot change built in roles." + 'InvalidOperationError: You cannot change built in roles.', ); } }); -test("Should not be allowed to delete a root role", async () => { +test('Should not be allowed to delete a root role', async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); @@ -845,31 +845,31 @@ test("Should not be allowed to delete a root role", async () => { await accessService.deleteRole(editRole.id); } catch (e) { expect(e.toString()).toBe( - "InvalidOperationError: You cannot change built in roles." + 'InvalidOperationError: You cannot change built in roles.', ); } }); -test("Should not be allowed to edit a project role", async () => { +test('Should not be allowed to edit a project role', async () => { expect.assertions(1); const ownerRole = await accessService.getRoleByName(RoleName.OWNER); const roleUpdate = { id: ownerRole.id, - name: "NoLongerTheEditor", - description: "", + name: 'NoLongerTheEditor', + description: '', }; try { await accessService.updateRole(roleUpdate); } catch (e) { expect(e.toString()).toBe( - "InvalidOperationError: You cannot change built in roles." + 'InvalidOperationError: You cannot change built in roles.', ); } }); -test("Should not be allowed to delete a project role", async () => { +test('Should not be allowed to delete a project role', async () => { expect.assertions(1); const ownerRole = await accessService.getRoleByName(RoleName.OWNER); @@ -878,15 +878,15 @@ test("Should not be allowed to delete a project role", async () => { await accessService.deleteRole(ownerRole.id); } catch (e) { expect(e.toString()).toBe( - "InvalidOperationError: You cannot change built in roles." + 'InvalidOperationError: You cannot change built in roles.', ); } }); -test("Should be allowed move feature toggle to project when given access through group", async () => { +test('Should be allowed move feature toggle to project when given access through group', async () => { const project = { - id: "yet-another-project1", - name: "yet-another-project1", + id: 'yet-another-project1', + name: 'yet-another-project1', }; const viewerUser = await createUser(readRole.id); @@ -904,17 +904,17 @@ test("Should be allowed move feature toggle to project when given access through await accessService.addGroupToRole( groupWithProjectAccess.id!, projectRole.id, - "SomeAdminUser", - project.id + 'SomeAdminUser', + project.id, ); await hasCommonProjectAccess(viewerUser, project.id, true); }); -test("Should not lose user role access when given permissions from a group", async () => { +test('Should not lose user role access when given permissions from a group', async () => { const project = { - id: "yet-another-project-lose", - name: "yet-another-project-lose", + id: 'yet-another-project-lose', + name: 'yet-another-project-lose', }; const user = editorUser; @@ -929,23 +929,23 @@ test("Should not lose user role access when given permissions from a group", asy await accessService.addGroupToRole( groupWithNoAccess.id!, viewerRole.id, - "SomeAdminUser", - project.id + 'SomeAdminUser', + project.id, ); await hasFullProjectAccess(user, project.id, true); }); -test("Should allow user to take multiple group roles and have expected permissions on each project", async () => { +test('Should allow user to take multiple group roles and have expected permissions on each project', async () => { const projectForCreate = { - id: "project-that-should-have-create-toggle-permission", - name: "project-that-should-have-create-toggle-permission", - description: "Blah", + id: 'project-that-should-have-create-toggle-permission', + name: 'project-that-should-have-create-toggle-permission', + description: 'Blah', }; const projectForDelete = { - id: "project-that-should-have-delete-toggle-permission", - name: "project-that-should-have-delete-toggle-permission", - description: "Blah", + id: 'project-that-should-have-delete-toggle-permission', + name: 'project-that-should-have-delete-toggle-permission', + description: 'Blah', }; const viewerUser = await createUser(readRole.id); @@ -964,63 +964,63 @@ test("Should allow user to take multiple group roles and have expected permissio const createFeatureRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, ]); const deleteFeatureRole = await createRole([ { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); await accessService.addGroupToRole( groupWithCreateAccess.id!, deleteFeatureRole.id, - "SomeAdminUser", - projectForDelete.id + 'SomeAdminUser', + projectForDelete.id, ); await accessService.addGroupToRole( groupWithDeleteAccess.id!, createFeatureRole.id, - "SomeAdminUser", - projectForCreate.id + 'SomeAdminUser', + projectForCreate.id, ); expect( await accessService.hasPermission( viewerUser, permissions.CREATE_FEATURE, - projectForCreate.id - ) + projectForCreate.id, + ), ).toBe(true); expect( await accessService.hasPermission( viewerUser, permissions.DELETE_FEATURE, - projectForCreate.id - ) + projectForCreate.id, + ), ).toBe(false); expect( await accessService.hasPermission( viewerUser, permissions.CREATE_FEATURE, - projectForDelete.id - ) + projectForDelete.id, + ), ).toBe(false); expect( await accessService.hasPermission( viewerUser, permissions.DELETE_FEATURE, - projectForDelete.id - ) + projectForDelete.id, + ), ).toBe(true); }); -test("Should allow user to take on root role through a group that has a root role defined", async () => { +test('Should allow user to take on root role through a group that has a root role defined', async () => { const viewerUser = await createUser(readRole.id); await createGroup({ @@ -1029,11 +1029,11 @@ test("Should allow user to take on root role through a group that has a root rol }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN) + await accessService.hasPermission(viewerUser, permissions.ADMIN), ).toBe(true); }); -test("Should not elevate permissions for a user that is not present in a root role group", async () => { +test('Should not elevate permissions for a user that is not present in a root role group', async () => { const viewerUser = await createUser(readRole.id); const viewerUserNotInGroup = await createUser(readRole.id); @@ -1044,18 +1044,18 @@ test("Should not elevate permissions for a user that is not present in a root ro }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN) + await accessService.hasPermission(viewerUser, permissions.ADMIN), ).toBe(true); expect( await accessService.hasPermission( viewerUserNotInGroup, - permissions.ADMIN - ) + permissions.ADMIN, + ), ).toBe(false); }); -test("Should not reduce permissions for an admin user that enters an editor group", async () => { +test('Should not reduce permissions for an admin user that enters an editor group', async () => { const adminUser = await createUser(adminRole.id); await createGroup({ @@ -1064,18 +1064,18 @@ test("Should not reduce permissions for an admin user that enters an editor grou }); expect( - await accessService.hasPermission(adminUser, permissions.ADMIN) + await accessService.hasPermission(adminUser, permissions.ADMIN), ).toBe(true); }); -test("Should not change permissions for a user in a group without a root role", async () => { +test('Should not change permissions for a user in a group without a root role', async () => { const groupStore = stores.groupStore; const viewerUser = await createUser(readRole.id); const groupWithoutRootRole = await groupStore.create({ - name: "GroupWithNoRootRole", - description: "", + name: 'GroupWithNoRootRole', + description: '', }); const preAddedToGroupPermissions = @@ -1084,7 +1084,7 @@ test("Should not change permissions for a user in a group without a root role", await groupStore.addUsersToGroup( groupWithoutRootRole.id!, [{ user: viewerUser }], - "Admin" + 'Admin', ); const postAddedToGroupPermissions = @@ -1092,11 +1092,11 @@ test("Should not change permissions for a user in a group without a root role", expect( JSON.stringify(preAddedToGroupPermissions) === - JSON.stringify(postAddedToGroupPermissions) + JSON.stringify(postAddedToGroupPermissions), ).toBe(true); }); -test("Should add permissions to user when a group is given a root role after the user has been added to the group", async () => { +test('Should add permissions to user when a group is given a root role after the user has been added to the group', async () => { const groupStore = stores.groupStore; const viewerUser = await createUser(readRole.id); @@ -1106,23 +1106,23 @@ test("Should add permissions to user when a group is given a root role after the }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN) + await accessService.hasPermission(viewerUser, permissions.ADMIN), ).toBe(false); await groupStore.update({ id: groupWithoutRootRole.id!, - name: "GroupWithNoRootRole", + name: 'GroupWithNoRootRole', rootRole: adminRole.id, users: [{ user: viewerUser }], }); expect( - await accessService.hasPermission(viewerUser, permissions.ADMIN) + await accessService.hasPermission(viewerUser, permissions.ADMIN), ).toBe(true); }); -test("Should give full project access to the default project to user in a group with an editor root role", async () => { - const projectName = "default"; +test('Should give full project access to the default project to user in a group with an editor root role', async () => { + const projectName = 'default'; const viewerUser = await createUser(readRole.id); @@ -1134,29 +1134,29 @@ test("Should give full project access to the default project to user in a group await hasFullProjectAccess(viewerUser, projectName, true); }); -test("if user has two roles user has union of permissions from the two roles", async () => { - const projectName = "default"; +test('if user has two roles user has union of permissions from the two roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1166,38 +1166,38 @@ test("if user has two roles user has union of permissions from the two roles", a ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission) + assignedPermissions.map((p) => p.permission), ); expect(permissionNameSet.size).toBe(3); }); -test("calling set for user overwrites existing roles", async () => { - const projectName = "default"; +test('calling set for user overwrites existing roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1207,10 +1207,10 @@ test("calling set for user overwrites existing roles", async () => { ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission) + assignedPermissions.map((p) => p.permission), ); expect(permissionNameSet.size).toBe(3); @@ -1220,22 +1220,22 @@ test("calling set for user overwrites existing roles", async () => { ]); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(2); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: "CREATE_FEATURE", + permission: 'CREATE_FEATURE', }); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: "DELETE_FEATURE", + permission: 'DELETE_FEATURE', }); }); -test("if group has two roles user has union of permissions from the two roles", async () => { - const projectName = "default"; +test('if group has two roles user has union of permissions from the two roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); @@ -1246,21 +1246,21 @@ test("if group has two roles user has union of permissions from the two roles", const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1268,21 +1268,21 @@ test("if group has two roles user has union of permissions from the two roles", projectName, emptyGroup.id, [firstRole.id, secondRole.id], - "testusr" + 'testusr', ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission) + assignedPermissions.map((p) => p.permission), ); expect(permissionNameSet.size).toBe(3); }); -test("calling set for group overwrites existing roles", async () => { - const projectName = "default"; +test('calling set for group overwrites existing roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); @@ -1293,21 +1293,21 @@ test("calling set for group overwrites existing roles", async () => { const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1315,14 +1315,14 @@ test("calling set for group overwrites existing roles", async () => { projectName, emptyGroup.id, [firstRole.id, secondRole.id], - "testusr" + 'testusr', ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); const permissionNameSet = new Set( - assignedPermissions.map((p) => p.permission) + assignedPermissions.map((p) => p.permission), ); expect(permissionNameSet.size).toBe(3); @@ -1331,26 +1331,26 @@ test("calling set for group overwrites existing roles", async () => { projectName, emptyGroup.id, [firstRole.id], - "testusr" + 'testusr', ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(2); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: "CREATE_FEATURE", + permission: 'CREATE_FEATURE', }); expect(newAssignedPermissions).toContainEqual({ project: projectName, - permission: "DELETE_FEATURE", + permission: 'DELETE_FEATURE', }); }); -test("group with root role can be assigned a project specific role", async () => { - const projectName = "default"; +test('group with root role can be assigned a project specific role', async () => { + const projectName = 'default'; const emptyUser = await createUser(); @@ -1362,7 +1362,7 @@ test("group with root role can be assigned a project specific role", async () => const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, ]); @@ -1370,21 +1370,21 @@ test("group with root role can be assigned a project specific role", async () => projectName, emptyGroup.id, [firstRole.id], - "testusr" + 'testusr', ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(assignedPermissions).toContainEqual({ project: projectName, - permission: "CREATE_FEATURE", + permission: 'CREATE_FEATURE', }); }); -test("calling add access with invalid project role ids should not assign those roles", async () => { - const projectName = "default"; +test('calling add access with invalid project role ids should not assign those roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const adminRootRole = await accessService.getRoleByName(RoleName.ADMIN); @@ -1394,18 +1394,18 @@ test("calling add access with invalid project role ids should not assign those r [], [emptyUser.id], projectName, - "some-admin-user" + 'some-admin-user', ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("calling set roles for user with invalid project role ids should not assign those roles", async () => { - const projectName = "default"; +test('calling set roles for user with invalid project role ids should not assign those roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const adminRootRole = await accessService.getRoleByName(RoleName.ADMIN); @@ -1416,20 +1416,20 @@ test("calling set roles for user with invalid project role ids should not assign ]); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("calling set roles for user with empty role array removes all roles", async () => { - const projectName = "default"; +test('calling set roles for user with empty role array removes all roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const role = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, ]); @@ -1438,7 +1438,7 @@ test("calling set roles for user with empty role array removes all roles", async ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(assignedPermissions.length).toBe(1); @@ -1446,24 +1446,24 @@ test("calling set roles for user with empty role array removes all roles", async await accessService.setProjectRolesForUser(projectName, emptyUser.id, []); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("calling set roles for user with empty role array should not remove root roles", async () => { - const projectName = "default"; +test('calling set roles for user with empty role array should not remove root roles', async () => { + const projectName = 'default'; const adminUser = await createUser(adminRole.id); const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); @@ -1472,7 +1472,7 @@ test("calling set roles for user with empty role array should not remove root ro ]); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(assignedPermissions.length).toBe(3); @@ -1480,32 +1480,32 @@ test("calling set roles for user with empty role array should not remove root ro await accessService.setProjectRolesForUser(projectName, adminUser.id, []); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test("remove user access should remove all project roles", async () => { - const projectName = "default"; +test('remove user access should remove all project roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1515,7 +1515,7 @@ test("remove user access should remove all project roles", async () => { ]); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(assignedPermissions.length).toBe(3); @@ -1523,31 +1523,31 @@ test("remove user access should remove all project roles", async () => { await accessService.removeUserAccess(projectName, emptyUser.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("remove user access should remove all project roles, while leaving root roles untouched", async () => { - const projectName = "default"; +test('remove user access should remove all project roles, while leaving root roles untouched', async () => { + const projectName = 'default'; const adminUser = await createUser(adminRole.id); const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1557,7 +1557,7 @@ test("remove user access should remove all project roles, while leaving root rol ]); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(assignedPermissions.length).toBe(4); @@ -1565,15 +1565,15 @@ test("remove user access should remove all project roles, while leaving root rol await accessService.removeUserAccess(projectName, adminUser.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test("calling set roles for group with invalid project role ids should not assign those roles", async () => { - const projectName = "default"; +test('calling set roles for group with invalid project role ids should not assign those roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const emptyGroup = await createGroup({ @@ -1586,18 +1586,18 @@ test("calling set roles for group with invalid project role ids should not assig projectName, emptyGroup.id, [adminRootRole.id, 9999], - "admin" + 'admin', ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("calling set roles for group with empty role array removes all roles", async () => { - const projectName = "default"; +test('calling set roles for group with empty role array removes all roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const emptyGroup = await createGroup({ @@ -1607,7 +1607,7 @@ test("calling set roles for group with empty role array removes all roles", asyn const role = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, ]); @@ -1615,11 +1615,11 @@ test("calling set roles for group with empty role array removes all roles", asyn projectName, emptyGroup.id, [role.id], - "admin" + 'admin', ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(assignedPermissions.length).toBe(1); @@ -1628,18 +1628,18 @@ test("calling set roles for group with empty role array removes all roles", asyn projectName, emptyGroup.id, [], - "admin" + 'admin', ); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("calling set roles for group with empty role array should not remove root roles", async () => { - const projectName = "default"; +test('calling set roles for group with empty role array should not remove root roles', async () => { + const projectName = 'default'; const adminUser = await createUser(adminRole.id); const group = await createGroup({ @@ -1649,11 +1649,11 @@ test("calling set roles for group with empty role array should not remove root r const role = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); @@ -1661,11 +1661,11 @@ test("calling set roles for group with empty role array should not remove root r projectName, group.id, [role.id], - "admin" + 'admin', ); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(assignedPermissions.length).toBe(3); @@ -1674,19 +1674,19 @@ test("calling set roles for group with empty role array should not remove root r projectName, group.id, [], - "admin" + 'admin', ); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test("remove group access should remove all project roles", async () => { - const projectName = "default"; +test('remove group access should remove all project roles', async () => { + const projectName = 'default'; const emptyUser = await createUser(); const group = await createGroup({ users: [{ user: emptyUser }], @@ -1695,18 +1695,18 @@ test("remove group access should remove all project roles", async () => { const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1714,11 +1714,11 @@ test("remove group access should remove all project roles", async () => { projectName, group.id, [firstRole.id, secondRole.id], - "admin" + 'admin', ); const assignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(assignedPermissions.length).toBe(3); @@ -1726,14 +1726,14 @@ test("remove group access should remove all project roles", async () => { await accessService.removeGroupAccess(projectName, group.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - emptyUser + emptyUser, ); expect(newAssignedPermissions.length).toBe(0); }); -test("remove group access should remove all project roles, while leaving root roles untouched", async () => { - const projectName = "default"; +test('remove group access should remove all project roles, while leaving root roles untouched', async () => { + const projectName = 'default'; const adminUser = await createUser(adminRole.id); const group = await createGroup({ users: [{ user: adminUser }], @@ -1742,18 +1742,18 @@ test("remove group access should remove all project roles, while leaving root ro const firstRole = await createRole([ { id: 2, - name: "CREATE_FEATURE", + name: 'CREATE_FEATURE', }, { id: 8, - name: "DELETE_FEATURE", + name: 'DELETE_FEATURE', }, ]); const secondRole = await createRole([ { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); @@ -1761,11 +1761,11 @@ test("remove group access should remove all project roles, while leaving root ro projectName, group.id, [firstRole.id, secondRole.id], - "admin" + 'admin', ); const assignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(assignedPermissions.length).toBe(4); @@ -1773,19 +1773,19 @@ test("remove group access should remove all project roles, while leaving root ro await accessService.removeGroupAccess(projectName, group.id); const newAssignedPermissions = await accessService.getPermissionsForUser( - adminUser + adminUser, ); expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); -test("access overview should have admin access and default project for admin user", async () => { - const email = "a-person@places.com"; +test('access overview should have admin access and default project for admin user', async () => { + const email = 'a-person@places.com'; const { userStore } = stores; const user = await userStore.insert({ - name: "Some User", + name: 'Some User', email, }); @@ -1794,28 +1794,28 @@ test("access overview should have admin access and default project for admin use const accessOverView: IUserAccessOverview[] = await accessService.getUserAccessOverview(); const userAccess = accessOverView.find( - (overviewRow) => overviewRow.userId === user.id + (overviewRow) => overviewRow.userId === user.id, )!; expect(userAccess.userId).toBe(user.id); - expect(userAccess.rootRole).toBe("Admin"); - expect(userAccess.accessibleProjects).toStrictEqual(["default"]); + expect(userAccess.rootRole).toBe('Admin'); + expect(userAccess.accessibleProjects).toStrictEqual(['default']); }); -test("access overview should have group access for groups that they are in", async () => { - const email = "a-nother-person@places.com"; +test('access overview should have group access for groups that they are in', async () => { + const email = 'a-nother-person@places.com'; const { userStore } = stores; const user = await userStore.insert({ - name: "Some Other User", + name: 'Some Other User', email, }); await accessService.setUserRootRole(user.id, adminRole.id); const group = await stores.groupStore.create({ - name: "Test Group", + name: 'Test Group', }); await stores.groupStore.addUsersToGroup( @@ -1827,33 +1827,33 @@ test("access overview should have group access for groups that they are in", asy }, }, ], - "Admin" + 'Admin', ); const someGroupRole = await createRole([ { id: 13, - name: "UPDATE_PROJECT", + name: 'UPDATE_PROJECT', }, ]); await accessService.addGroupToRole( group.id, someGroupRole.id, - "creator", - "default" + 'creator', + 'default', ); const accessOverView: IUserAccessOverview[] = await accessService.getUserAccessOverview(); const userAccess = accessOverView.find( - (overviewRow) => overviewRow.userId === user.id + (overviewRow) => overviewRow.userId === user.id, )!; expect(userAccess.userId).toBe(user.id); - expect(userAccess.rootRole).toBe("Admin"); - expect(userAccess.groups).toStrictEqual(["Test Group"]); + expect(userAccess.rootRole).toBe('Admin'); + expect(userAccess.groups).toStrictEqual(['Test Group']); - expect(userAccess.groupProjects).toStrictEqual(["default"]); + expect(userAccess.groupProjects).toStrictEqual(['default']); }); diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index a5a3e3273ac6..32e7620f73bc 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -1,13 +1,13 @@ -import dbInit from "../helpers/database-init"; -import getLogger from "../../fixtures/no-logger"; -import { ApiTokenService } from "../../../lib/services/api-token-service"; -import { createTestConfig } from "../../config/test-config"; -import { ApiTokenType, IApiToken } from "../../../lib/types/models/api-token"; -import { DEFAULT_ENV } from "../../../lib/util/constants"; -import { addDays, subDays } from "date-fns"; -import ProjectService from "../../../lib/services/project-service"; -import { createProjectService } from "../../../lib/features"; -import { EventService } from "../../../lib/services"; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import { ApiTokenService } from '../../../lib/services/api-token-service'; +import { createTestConfig } from '../../config/test-config'; +import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token'; +import { DEFAULT_ENV } from '../../../lib/util/constants'; +import { addDays, subDays } from 'date-fns'; +import ProjectService from '../../../lib/services/project-service'; +import { createProjectService } from '../../../lib/features'; +import { EventService } from '../../../lib/services'; let db; let stores; @@ -16,21 +16,21 @@ let projectService: ProjectService; beforeAll(async () => { const config = createTestConfig({ - server: { baseUriPath: "/test" }, + server: { baseUriPath: '/test' }, }); - db = await dbInit("api_token_service_serial", getLogger); + db = await dbInit('api_token_service_serial', getLogger); stores = db.stores; const eventService = new EventService(stores, config); const project = { - id: "test-project", - name: "Test Project", - description: "Fancy", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-project', + name: 'Test Project', + description: 'Fancy', + mode: 'open' as const, + defaultStickiness: 'clientId', }; const user = await stores.userStore.insert({ - name: "Some Name", - email: "test@getunleash.io", + name: 'Some Name', + email: 'test@getunleash.io', }); projectService = createProjectService(db.rawDatabase, config); @@ -47,23 +47,23 @@ afterAll(async () => { afterEach(async () => { const tokens = await stores.apiTokenStore.getAll(); const deleteAll = tokens.map((t: IApiToken) => - stores.apiTokenStore.delete(t.secret) + stores.apiTokenStore.delete(t.secret), ); await Promise.all(deleteAll); }); -test("should have empty list of tokens", async () => { +test('should have empty list of tokens', async () => { const allTokens = await apiTokenService.getAllTokens(); const activeTokens = await apiTokenService.getAllTokens(); expect(allTokens.length).toBe(0); expect(activeTokens.length).toBe(0); }); -test("should create client token", async () => { +test('should create client token', async () => { const token = await apiTokenService.createApiToken({ - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, - project: "*", + project: '*', environment: DEFAULT_ENV, }); const allTokens = await apiTokenService.getAllTokens(); @@ -71,29 +71,29 @@ test("should create client token", async () => { expect(allTokens.length).toBe(1); expect(token.secret.length > 32).toBe(true); expect(token.type).toBe(ApiTokenType.CLIENT); - expect(token.username).toBe("default-client"); + expect(token.username).toBe('default-client'); expect(allTokens[0].secret).toBe(token.secret); }); -test("should create admin token", async () => { +test('should create admin token', async () => { const token = await apiTokenService.createApiToken({ - tokenName: "admin", + tokenName: 'admin', type: ApiTokenType.ADMIN, - project: "*", - environment: "*", + project: '*', + environment: '*', }); expect(token.secret.length > 32).toBe(true); expect(token.type).toBe(ApiTokenType.ADMIN); }); -test("should set expiry of token", async () => { - const time = new Date("2022-01-01"); +test('should set expiry of token', async () => { + const time = new Date('2022-01-01'); await apiTokenService.createApiToken({ - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, expiresAt: time, - project: "*", + project: '*', environment: DEFAULT_ENV, }); @@ -102,46 +102,46 @@ test("should set expiry of token", async () => { expect(token.expiresAt).toEqual(time); }); -test("should update expiry of token", async () => { - const time = new Date("2022-01-01"); - const newTime = new Date("2023-01-01"); +test('should update expiry of token', async () => { + const time = new Date('2022-01-01'); + const newTime = new Date('2023-01-01'); const token = await apiTokenService.createApiToken( { - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, expiresAt: time, - project: "*", + project: '*', environment: DEFAULT_ENV, }, - "tester" + 'tester', ); - await apiTokenService.updateExpiry(token.secret, newTime, "tester"); + await apiTokenService.updateExpiry(token.secret, newTime, 'tester'); const [updatedToken] = await apiTokenService.getAllTokens(); expect(updatedToken.expiresAt).toEqual(newTime); }); -test("should only return valid tokens", async () => { +test('should only return valid tokens', async () => { const now = Date.now(); const yesterday = subDays(now, 1); const tomorrow = addDays(now, 1); await apiTokenService.createApiToken({ - tokenName: "default-expired", + tokenName: 'default-expired', type: ApiTokenType.CLIENT, expiresAt: yesterday, - project: "*", + project: '*', environment: DEFAULT_ENV, }); const activeToken = await apiTokenService.createApiToken({ - tokenName: "default-valid", + tokenName: 'default-valid', type: ApiTokenType.CLIENT, expiresAt: tomorrow, - project: "*", + project: '*', environment: DEFAULT_ENV, }); @@ -151,70 +151,70 @@ test("should only return valid tokens", async () => { expect(activeToken.secret).toBe(tokens[0].secret); }); -test("should create client token with project list", async () => { +test('should create client token with project list', async () => { const token = await apiTokenService.createApiToken({ - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, - projects: ["default", "test-project"], + projects: ['default', 'test-project'], environment: DEFAULT_ENV, }); - expect(token.secret.slice(0, 2)).toEqual("[]"); - expect(token.projects).toStrictEqual(["default", "test-project"]); + expect(token.secret.slice(0, 2)).toEqual('[]'); + expect(token.projects).toStrictEqual(['default', 'test-project']); }); -test("should strip all other projects if ALL_PROJECTS is present", async () => { +test('should strip all other projects if ALL_PROJECTS is present', async () => { const token = await apiTokenService.createApiToken({ - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, - projects: ["*", "default"], + projects: ['*', 'default'], environment: DEFAULT_ENV, }); - expect(token.projects).toStrictEqual(["*"]); + expect(token.projects).toStrictEqual(['*']); }); -test("should return user with multiple projects", async () => { +test('should return user with multiple projects', async () => { const now = Date.now(); const tomorrow = addDays(now, 1); await apiTokenService.createApiToken({ - tokenName: "default-valid", + tokenName: 'default-valid', type: ApiTokenType.CLIENT, expiresAt: tomorrow, - projects: ["test-project", "default"], + projects: ['test-project', 'default'], environment: DEFAULT_ENV, }); await apiTokenService.createApiToken({ - tokenName: "default-also-valid", + tokenName: 'default-also-valid', type: ApiTokenType.CLIENT, expiresAt: tomorrow, - projects: ["test-project"], + projects: ['test-project'], environment: DEFAULT_ENV, }); const tokens = await apiTokenService.getAllActiveTokens(); const multiProjectUser = await apiTokenService.getUserForToken( - tokens[0].secret + tokens[0].secret, ); const singleProjectUser = await apiTokenService.getUserForToken( - tokens[1].secret + tokens[1].secret, ); expect(multiProjectUser!.projects).toStrictEqual([ - "test-project", - "default", + '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 () => { +test('should not partially create token if projects are invalid', async () => { try { await apiTokenService.createApiTokenWithProjects({ - tokenName: "default-client", + tokenName: 'default-client', type: ApiTokenType.CLIENT, - projects: ["non-existent-project"], + projects: ['non-existent-project'], environment: DEFAULT_ENV, }); } catch (e) {} diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index 6bcfd4bccfd9..1d67e3d2fdee 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -1,14 +1,14 @@ -import dbInit, { ITestDb } from "../helpers/database-init"; -import getLogger from "../../fixtures/no-logger"; -import ProjectHealthService from "../../../lib/services/project-health-service"; -import { createTestConfig } from "../../config/test-config"; -import { IUnleashStores } from "../../../lib/types"; -import { IUser } from "../../../lib/server-impl"; +import dbInit, { ITestDb } from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import ProjectHealthService from '../../../lib/services/project-health-service'; +import { createTestConfig } from '../../config/test-config'; +import { IUnleashStores } from '../../../lib/types'; +import { IUser } from '../../../lib/server-impl'; import { createFeatureToggleService, createProjectService, -} from "../../../lib/features"; -import { EventService } from "../../../lib/services"; +} from '../../../lib/features'; +import { EventService } from '../../../lib/services'; let stores: IUnleashStores; let db: ITestDb; @@ -20,11 +20,11 @@ let user: IUser; beforeAll(async () => { const config = createTestConfig(); - db = await dbInit("project_health_service_serial", getLogger); + db = await dbInit('project_health_service_serial', getLogger); stores = db.stores; user = await stores.userStore.insert({ - name: "Some Name", - email: "test@getunleash.io", + name: 'Some Name', + email: 'test@getunleash.io', }); eventService = new EventService(stores, config); @@ -34,95 +34,95 @@ beforeAll(async () => { projectHealthService = new ProjectHealthService( stores, config, - projectService + projectService, ); }); afterAll(async () => { await db.destroy(); }); -test("Project with no stale toggles should have 100% health rating", async () => { +test('Project with no stale toggles should have 100% health rating', async () => { const project = { - id: "health-rating", - name: "Health rating", - description: "Fancy", + id: 'health-rating', + name: 'Health rating', + description: 'Fancy', }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create("health-rating", { - name: "health-rating-not-stale", - description: "new", + await stores.featureToggleStore.create('health-rating', { + name: 'health-rating-not-stale', + description: 'new', stale: false, }); - await stores.featureToggleStore.create("health-rating", { - name: "health-rating-not-stale-2", - description: "new too", + await stores.featureToggleStore.create('health-rating', { + name: 'health-rating-not-stale-2', + description: 'new too', stale: false, }); const rating = await projectHealthService.calculateHealthRating( - savedProject + savedProject, ); expect(rating).toBe(100); }); -test("Project with two stale toggles and two non stale should have 50% health rating", async () => { +test('Project with two stale toggles and two non stale should have 50% health rating', async () => { const project = { - id: "health-rating-2", - name: "Health rating", - description: "Fancy", + id: 'health-rating-2', + name: 'Health rating', + description: 'Fancy', }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create("health-rating-2", { - name: "health-rating-2-not-stale", - description: "new", + await stores.featureToggleStore.create('health-rating-2', { + name: 'health-rating-2-not-stale', + description: 'new', stale: false, }); - await stores.featureToggleStore.create("health-rating-2", { - name: "health-rating-2-not-stale-2", - description: "new too", + await stores.featureToggleStore.create('health-rating-2', { + name: 'health-rating-2-not-stale-2', + description: 'new too', stale: false, }); - await stores.featureToggleStore.create("health-rating-2", { - name: "health-rating-2-stale-1", - description: "stale", + await stores.featureToggleStore.create('health-rating-2', { + name: 'health-rating-2-stale-1', + description: 'stale', stale: true, }); - await stores.featureToggleStore.create("health-rating-2", { - name: "health-rating-2-stale-2", - description: "stale too", + await stores.featureToggleStore.create('health-rating-2', { + name: 'health-rating-2-stale-2', + description: 'stale too', stale: true, }); const rating = await projectHealthService.calculateHealthRating( - savedProject + savedProject, ); expect(rating).toBe(50); }); -test("Project with one non-stale, one potentially stale and one stale should have 33% health rating", async () => { +test('Project with one non-stale, one potentially stale and one stale should have 33% health rating', async () => { const project = { - id: "health-rating-3", - name: "Health rating", - description: "Fancy", + id: 'health-rating-3', + name: 'Health rating', + description: 'Fancy', }; const savedProject = await projectService.createProject(project, user); - await stores.featureToggleStore.create("health-rating-3", { - name: "health-rating-3-not-stale", - description: "new", + await stores.featureToggleStore.create('health-rating-3', { + name: 'health-rating-3-not-stale', + description: 'new', stale: false, }); - await stores.featureToggleStore.create("health-rating-3", { - name: "health-rating-3-potentially-stale", - description: "new too", - type: "release", + await stores.featureToggleStore.create('health-rating-3', { + name: 'health-rating-3-potentially-stale', + description: 'new too', + type: 'release', stale: false, createdAt: new Date(Date.UTC(2020, 1, 1)), }); - await stores.featureToggleStore.create("health-rating-3", { - name: "health-rating-3-stale", - description: "stale", + await stores.featureToggleStore.create('health-rating-3', { + name: 'health-rating-3-stale', + description: 'stale', stale: true, }); const rating = await projectHealthService.calculateHealthRating( - savedProject + savedProject, ); expect(rating).toBe(33); }); diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 0ab780032dc0..d539ecab860b 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -1,22 +1,22 @@ -import dbInit, { ITestDb } from "../helpers/database-init"; -import getLogger from "../../fixtures/no-logger"; -import FeatureToggleService from "../../../lib/services/feature-toggle-service"; -import ProjectService from "../../../lib/services/project-service"; -import { AccessService } from "../../../lib/services/access-service"; -import { MOVE_FEATURE_TOGGLE } from "../../../lib/types/permissions"; -import { createTestConfig } from "../../config/test-config"; -import { RoleName } from "../../../lib/types/model"; -import { randomId } from "../../../lib/util/random-id"; -import EnvironmentService from "../../../lib/services/environment-service"; -import IncompatibleProjectError from "../../../lib/error/incompatible-project-error"; -import { EventService } from "../../../lib/services"; -import { FeatureEnvironmentEvent } from "../../../lib/types/events"; -import { addDays, subDays } from "date-fns"; +import dbInit, { ITestDb } from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import FeatureToggleService from '../../../lib/services/feature-toggle-service'; +import ProjectService from '../../../lib/services/project-service'; +import { AccessService } from '../../../lib/services/access-service'; +import { MOVE_FEATURE_TOGGLE } from '../../../lib/types/permissions'; +import { createTestConfig } from '../../config/test-config'; +import { RoleName } from '../../../lib/types/model'; +import { randomId } from '../../../lib/util/random-id'; +import EnvironmentService from '../../../lib/services/environment-service'; +import IncompatibleProjectError from '../../../lib/error/incompatible-project-error'; +import { EventService } from '../../../lib/services'; +import { FeatureEnvironmentEvent } from '../../../lib/types/events'; +import { addDays, subDays } from 'date-fns'; import { createAccessService, createFeatureToggleService, createProjectService, -} from "../../../lib/features"; +} from '../../../lib/features'; let stores; let db: ITestDb; @@ -31,19 +31,19 @@ let user; const isProjectUser = async ( userId: number, projectName: string, - condition: boolean + condition: boolean, ) => { expect(await projectService.isProjectUser(userId, projectName)).toBe( - condition + condition, ); }; beforeAll(async () => { - db = await dbInit("project_service_serial", getLogger); + db = await dbInit('project_service_serial', getLogger); stores = db.stores; user = await stores.userStore.insert({ - name: "Some Name", - email: "test@getunleash.io", + name: 'Some Name', + email: 'test@getunleash.io', }); const config = createTestConfig({ getLogger, @@ -68,7 +68,7 @@ afterAll(async () => { afterEach(async () => { const envs = await stores.environmentStore.getAll(); const deleteEnvs = envs - .filter((env) => env.name !== "default") + .filter((env) => env.name !== 'default') .map(async (env) => { await stores.environmentStore.delete(env.name); }); @@ -81,19 +81,19 @@ afterEach(async () => { await Promise.allSettled(wipeUserPermissions); }); -test("should have default project", async () => { - const project = await projectService.getProject("default"); +test('should have default project', async () => { + const project = await projectService.getProject('default'); expect(project).toBeDefined(); - expect(project.id).toBe("default"); + expect(project.id).toBe('default'); }); -test("should list all projects", async () => { +test('should list all projects', async () => { const project = { - id: "test-list", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "default", + id: 'test-list', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'default', }; await projectService.createProject(project, user); @@ -102,45 +102,45 @@ test("should list all projects", async () => { expect(projects.find((p) => p.name === project.name)?.memberCount).toBe(1); }); -test("should create new project", async () => { +test('should create new project', async () => { const project = { - id: "test", - name: "New project", - description: "Blah", - defaultStickiness: "default", + id: 'test', + name: 'New project', + description: 'Blah', + defaultStickiness: 'default', }; await projectService.createProject(project, user); - const ret = await projectService.getProject("test"); + const ret = await projectService.getProject('test'); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); }); -test("should create new private project", async () => { +test('should create new private project', async () => { const project = { - id: "testPrivate", - name: "New private project", - description: "Blah", - defaultStickiness: "default", + id: 'testPrivate', + name: 'New private project', + description: 'Blah', + defaultStickiness: 'default', }; await projectService.createProject(project, user); - const ret = await projectService.getProject("testPrivate"); + const ret = await projectService.getProject('testPrivate'); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); }); -test("should delete project", async () => { +test('should delete project', async () => { const project = { - id: "test-delete", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "default", + id: 'test-delete', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'default', }; await projectService.createProject(project, user); @@ -149,95 +149,95 @@ test("should delete project", async () => { try { await projectService.getProject(project.id); } catch (err) { - expect(err.message).toBe("No project found"); + expect(err.message).toBe('No project found'); } }); -test("should not be able to delete project with toggles", async () => { +test('should not be able to delete project with toggles', async () => { const project = { - id: "test-delete-with-toggles", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-delete-with-toggles', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: "test-project-delete", + name: 'test-project-delete', project: project.id, enabled: false, - defaultStickiness: "default", + defaultStickiness: 'default', }); try { await projectService.deleteProject(project.id, user); } catch (err) { expect(err.message).toBe( - "You can not delete a project with active feature toggles" + 'You can not delete a project with active feature toggles', ); } }); test('should not delete "default" project', async () => { try { - await projectService.deleteProject("default", user); + await projectService.deleteProject('default', user); } catch (err) { - expect(err.message).toBe("You can not delete the default project!"); + expect(err.message).toBe('You can not delete the default project!'); } }); -test("should validate name, legal", async () => { - const result = await projectService.validateId("new_name"); +test('should validate name, legal', async () => { + const result = await projectService.validateId('new_name'); expect(result).toBe(true); }); -test("should not be able to create existing project", async () => { +test('should not be able to create existing project', async () => { const project = { - id: "test-delete", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "default", + id: 'test-delete', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'default', }; try { await projectService.createProject(project, user); await projectService.createProject(project, user); } catch (err) { - expect(err.message).toBe("A project with this id already exists."); + expect(err.message).toBe('A project with this id already exists.'); } }); -test("should require URL friendly ID", async () => { +test('should require URL friendly ID', async () => { try { - await projectService.validateId("new name øæå"); + await projectService.validateId('new name øæå'); } catch (err) { expect(err.message).toBe('"value" must be URL friendly'); } }); -test("should require unique ID", async () => { +test('should require unique ID', async () => { try { - await projectService.validateId("default"); + await projectService.validateId('default'); } catch (err) { - expect(err.message).toBe("A project with this id already exists."); + expect(err.message).toBe('A project with this id already exists.'); } }); -test("should update project", async () => { +test('should update project', async () => { const project = { - id: "test-update", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "default", + id: 'test-update', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'default', }; const updatedProject = { - id: "test-update", - name: "New name", - description: "Blah longer desc", - mode: "protected" as const, - defaultStickiness: "userId", + id: 'test-update', + name: 'New name', + description: 'Blah longer desc', + mode: 'protected' as const, + defaultStickiness: 'userId', }; await projectService.createProject(project, user); @@ -247,30 +247,30 @@ test("should update project", async () => { expect(updatedProject.name).toBe(readProject.name); expect(updatedProject.description).toBe(readProject.description); - expect(updatedProject.mode).toBe("protected"); - expect(updatedProject.defaultStickiness).toBe("userId"); + expect(updatedProject.mode).toBe('protected'); + expect(updatedProject.defaultStickiness).toBe('userId'); }); -test("should update project without existing settings", async () => { +test('should update project without existing settings', async () => { const project = { - id: "test-update-legacy", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "default", + id: 'test-update-legacy', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'default', }; const updatedProject = { - id: "test-update-legacy", - name: "New name", - description: "Blah longer desc", - mode: "protected" as const, - defaultStickiness: "clientId", + id: 'test-update-legacy', + name: 'New name', + description: 'Blah longer desc', + mode: 'protected' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); await db - .rawDatabase("project_settings") + .rawDatabase('project_settings') .del() .where({ project: project.id }); await projectService.updateProject(updatedProject, user); @@ -279,25 +279,25 @@ test("should update project without existing settings", async () => { expect(updatedProject.name).toBe(readProject.name); expect(updatedProject.description).toBe(readProject.description); - expect(updatedProject.mode).toBe("protected"); - expect(updatedProject.defaultStickiness).toBe("clientId"); + expect(updatedProject.mode).toBe('protected'); + expect(updatedProject.defaultStickiness).toBe('clientId'); }); -test("should give error when getting unknown project", async () => { +test('should give error when getting unknown project', async () => { try { - await projectService.getProject("unknown"); + await projectService.getProject('unknown'); } catch (err) { - expect(err.message).toBe("No project found"); + expect(err.message).toBe('No project found'); } }); -test("should get list of users with access to project", async () => { +test('should get list of users with access to project', async () => { const project = { - id: "test-roles-access", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-roles-access', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const { users } = await projectService.getAccessToProject(project.id); @@ -314,23 +314,23 @@ test("should get list of users with access to project", async () => { await isProjectUser(users[0].id, project.id, true); }); -test("should add a member user to the project", async () => { +test('should add a member user to the project', async () => { const project = { - id: "add-users", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'add-users', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "member1@getunleash.io", + name: 'Some Member', + email: 'member1@getunleash.io', }); const projectMember2 = await stores.userStore.insert({ - name: "Some Member 2", - email: "member2@getunleash.io", + name: 'Some Member 2', + email: 'member2@getunleash.io', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -339,13 +339,13 @@ test("should add a member user to the project", async () => { project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await projectService.addUser( project.id, memberRole.id, projectMember2.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -371,23 +371,23 @@ test("should add a member user to the project", async () => { ]); }); -test("should add admin users to the project", async () => { +test('should add admin users to the project', async () => { const project = { - id: "add-admin-users", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'add-admin-users', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectAdmin1 = await stores.userStore.insert({ - name: "Some Member", - email: "admin1@getunleash.io", + name: 'Some Member', + email: 'admin1@getunleash.io', }); const projectAdmin2 = await stores.userStore.insert({ - name: "Some Member 2", - email: "admin2@getunleash.io", + name: 'Some Member 2', + email: 'admin2@getunleash.io', }); const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); @@ -396,13 +396,13 @@ test("should add admin users to the project", async () => { project.id, ownerRole.id, projectAdmin1.id, - "test" + 'test', ); await projectService.addUser( project.id, ownerRole.id, projectAdmin2.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -419,19 +419,19 @@ test("should add admin users to the project", async () => { await isProjectUser(adminUsers[2].id, project.id, true); }); -test("add user should fail if user already have access", async () => { +test('add user should fail if user already have access', async () => { const project = { - id: "add-users-twice", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'add-users-twice', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "member42@getunleash.io", + name: 'Some Member', + email: 'member42@getunleash.io', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -440,7 +440,7 @@ test("add user should fail if user already have access", async () => { project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await expect(async () => @@ -448,26 +448,26 @@ test("add user should fail if user already have access", async () => { project.id, memberRole.id, projectMember1.id, - "test" - ) + 'test', + ), ).rejects.toThrow( - new Error("User already has access to project=add-users-twice") + new Error('User already has access to project=add-users-twice'), ); }); -test("should remove user from the project", async () => { +test('should remove user from the project', async () => { const project = { - id: "remove-users", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'remove-users', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "member99@getunleash.io", + name: 'Some Member', + email: 'member99@getunleash.io', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -476,13 +476,13 @@ test("should remove user from the project", async () => { project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await projectService.removeUser( project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -491,13 +491,13 @@ test("should remove user from the project", async () => { expect(memberUsers).toHaveLength(0); }); -test("should not remove user from the project", async () => { +test('should not remove user from the project', async () => { const project = { - id: "remove-users-not-allowed", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'remove-users-not-allowed', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); @@ -509,87 +509,87 @@ test("should not remove user from the project", async () => { project.id, ownerRole.id, user.id, - "test" + 'test', ); }).rejects.toThrowError( - new Error("A project must have at least one owner") + new Error('A project must have at least one owner'), ); }); -test("should not change project if feature toggle project does not match current project id", async () => { +test('should not change project if feature toggle project does not match current project id', async () => { const project = { - id: "test-change-project", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-change-project', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const toggle = { name: "test-toggle" }; + const toggle = { name: 'test-toggle' }; await projectService.createProject(project, user); await featureToggleService.createFeatureToggle(project.id, toggle, user); try { await projectService.changeProject( - "newProject", + 'newProject', toggle.name, user, - "wrong-project-id" + 'wrong-project-id', ); } catch (err) { - expect(err.message.toLowerCase().includes("permission")).toBeTruthy(); + expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); expect(err.message.includes(MOVE_FEATURE_TOGGLE)).toBeTruthy(); } }); -test("should return 404 if no project is found with the project id", async () => { +test('should return 404 if no project is found with the project id', async () => { const project = { - id: "test-change-project-2", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-change-project-2', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const toggle = { name: "test-toggle-2" }; + const toggle = { name: 'test-toggle-2' }; await projectService.createProject(project, user); await featureToggleService.createFeatureToggle(project.id, toggle, user); try { await projectService.changeProject( - "newProject", + 'newProject', toggle.name, user, - project.id + project.id, ); } catch (err) { expect(err.message).toBe(`No project found`); } }); -test("should fail if user is not authorized", async () => { +test('should fail if user is not authorized', async () => { const project = { - id: "test-change-project-3", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-change-project-3', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; const projectDestination = { - id: "test-change-project-dest", - name: "New project 2", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-change-project-dest', + name: 'New project 2', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const toggle = { name: "test-toggle-3" }; + const toggle = { name: 'test-toggle-3' }; const projectAdmin1 = await stores.userStore.insert({ - name: "test-change-project-creator", - email: "admin-change-project@getunleash.io", + name: 'test-change-project-creator', + email: 'admin-change-project@getunleash.io', }); await projectService.createProject(project, user); @@ -601,26 +601,26 @@ test("should fail if user is not authorized", async () => { projectDestination.id, toggle.name, user, - project.id + project.id, ); } catch (err) { - expect(err.message.toLowerCase().includes("permission")).toBeTruthy(); + expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); expect(err.message.includes(MOVE_FEATURE_TOGGLE)).toBeTruthy(); } }); -test("should change project when checks pass", async () => { +test('should change project when checks pass', async () => { const projectA = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "clientId", + mode: 'open' as const, + defaultStickiness: 'clientId', }; const projectB = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "clientId", + mode: 'open' as const, + defaultStickiness: 'clientId', }; const toggle = { name: randomId() }; @@ -631,7 +631,7 @@ test("should change project when checks pass", async () => { projectB.id, toggle.name, user, - projectA.id + projectA.id, ); const updatedFeature = await featureToggleService.getFeature({ @@ -640,18 +640,18 @@ test("should change project when checks pass", async () => { expect(updatedFeature.project).toBe(projectB.id); }); -test("changing project should emit event even if user does not have a username set", async () => { +test('changing project should emit event even if user does not have a username set', async () => { const projectA = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "default", + mode: 'open' as const, + defaultStickiness: 'default', }; const projectB = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "clientId", + mode: 'open' as const, + defaultStickiness: 'clientId', }; const toggle = { name: randomId() }; await projectService.createProject(projectA, user); @@ -662,26 +662,26 @@ test("changing project should emit event even if user does not have a username s projectB.id, toggle.name, user, - projectA.id + projectA.id, ); const eventsAfterChange = await stores.eventStore.getEvents(); expect(eventsAfterChange.length).toBe(eventsBeforeChange.length + 1); }, 10000); -test("should require equal project environments to move features", async () => { +test('should require equal project environments to move features', async () => { const projectA = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "clientId", + mode: 'open' as const, + defaultStickiness: 'clientId', }; const projectB = { id: randomId(), name: randomId(), - mode: "open" as const, - defaultStickiness: "clientId", + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const environment = { name: randomId(), type: "production" }; + const environment = { name: randomId(), type: 'production' }; const toggle = { name: randomId() }; await projectService.createProject(projectA, user); @@ -690,7 +690,7 @@ test("should require equal project environments to move features", async () => { await stores.environmentStore.create(environment); await environmentService.addEnvironmentToProject( environment.name, - projectB.id + projectB.id, ); await expect(() => @@ -698,28 +698,28 @@ test("should require equal project environments to move features", async () => { projectB.id, toggle.name, user, - projectA.id - ) + projectA.id, + ), ).rejects.toThrowError(IncompatibleProjectError); }); -test("A newly created project only gets connected to enabled environments", async () => { +test('A newly created project only gets connected to enabled environments', async () => { const project = { - id: "environment-test", - name: "New environment project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'environment-test', + name: 'New environment project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const enabledEnv = "connection_test"; + const enabledEnv = 'connection_test'; await db.stores.environmentStore.create({ name: enabledEnv, - type: "test", + type: 'test', }); - const disabledEnv = "do_not_connect"; + const disabledEnv = 'do_not_connect'; await db.stores.environmentStore.create({ name: disabledEnv, - type: "test", + type: 'test', enabled: false, }); @@ -728,43 +728,43 @@ test("A newly created project only gets connected to enabled environments", asyn await db.stores.projectStore.getEnvironmentsForProject(project.id); expect(connectedEnvs).toHaveLength(2); // default, connection_test expect( - connectedEnvs.some((e) => e.environment === enabledEnv) + connectedEnvs.some((e) => e.environment === enabledEnv), ).toBeTruthy(); expect( - connectedEnvs.some((e) => e.environment === disabledEnv) + connectedEnvs.some((e) => e.environment === disabledEnv), ).toBeFalsy(); }); -test("should have environments sorted in order", async () => { +test('should have environments sorted in order', async () => { const project = { - id: "environment-order-test", - name: "Environment testing project", - description: "", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'environment-order-test', + name: 'Environment testing project', + description: '', + mode: 'open' as const, + defaultStickiness: 'clientId', }; - const first = "test"; - const second = "abc"; - const third = "example"; - const fourth = "mock"; + const first = 'test'; + const second = 'abc'; + const third = 'example'; + const fourth = 'mock'; await db.stores.environmentStore.create({ name: first, - type: "test", + type: 'test', sortOrder: 1, }); await db.stores.environmentStore.create({ name: fourth, - type: "test", + type: 'test', sortOrder: 4, }); await db.stores.environmentStore.create({ name: third, - type: "test", + type: 'test', sortOrder: 3, }); await db.stores.environmentStore.create({ name: second, - type: "test", + type: 'test', sortOrder: 2, }); @@ -773,7 +773,7 @@ test("should have environments sorted in order", async () => { await db.stores.projectStore.getEnvironmentsForProject(project.id); expect(connectedEnvs.map((e) => e.environment)).toEqual([ - "default", + 'default', first, second, third, @@ -781,24 +781,24 @@ test("should have environments sorted in order", async () => { ]); }); -test("should add a user to the project with a custom role", async () => { +test('should add a user to the project with a custom role', async () => { const project = { - id: "add-users-custom-role", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'add-users-custom-role', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Custom", - email: "custom@getunleash.io", + name: 'Custom', + email: 'custom@getunleash.io', }); const customRole = await accessService.createRole({ - name: "Service Engineer2", - description: "", + name: 'Service Engineer2', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -813,7 +813,7 @@ test("should add a user to the project with a custom role", async () => { project.id, customRole.id, projectMember1.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -825,30 +825,30 @@ test("should add a user to the project with a custom role", async () => { expect(customRoleMember[0].name).toBe(projectMember1.name); }); -test("should delete role entries when deleting project", async () => { +test('should delete role entries when deleting project', async () => { const project = { - id: "test-delete-users-1", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-delete-users-1', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const user1 = await stores.userStore.insert({ - name: "Projectuser1", - email: "project1@getunleash.io", + name: 'Projectuser1', + email: 'project1@getunleash.io', }); const user2 = await stores.userStore.insert({ - name: "Projectuser2", - email: "project2@getunleash.io", + name: 'Projectuser2', + email: 'project2@getunleash.io', }); const customRole = await accessService.createRole({ - name: "Service Engineer", - description: "", + name: 'Service Engineer', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -859,8 +859,8 @@ test("should delete role entries when deleting project", async () => { ], }); - await projectService.addUser(project.id, customRole.id, user1.id, "test"); - await projectService.addUser(project.id, customRole.id, user2.id, "test"); + await projectService.addUser(project.id, customRole.id, user1.id, 'test'); + await projectService.addUser(project.id, customRole.id, user2.id, 'test'); let usersForRole = await accessService.getUsersForRole(customRole.id); expect(usersForRole.length).toBe(2); @@ -870,25 +870,25 @@ test("should delete role entries when deleting project", async () => { expect(usersForRole.length).toBe(0); }); -test("should change a users role in the project", async () => { +test('should change a users role in the project', async () => { const project = { - id: "test-change-user-role", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'test-change-user-role', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectUser = await stores.userStore.insert({ - name: "Projectuser3", - email: "project3@getunleash.io", + name: 'Projectuser3', + email: 'project3@getunleash.io', }); const customRole = await accessService.createRole({ - name: "Service Engineer3", - description: "", + name: 'Service Engineer3', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -900,7 +900,7 @@ test("should change a users role in the project", async () => { }); const member = await stores.roleStore.getRoleByName(RoleName.MEMBER); - await projectService.addUser(project.id, member.id, projectUser.id, "test"); + await projectService.addUser(project.id, member.id, projectUser.id, 'test'); const { users } = await projectService.getAccessToProject(project.id); const memberUser = users.filter((u) => u.roleId === member.id); @@ -911,17 +911,17 @@ test("should change a users role in the project", async () => { project.id, member.id, projectUser.id, - "test" + 'test', ); await projectService.addUser( project.id, customRole.id, projectUser.id, - "test" + 'test', ); const { users: updatedUsers } = await projectService.getAccessToProject( - project.id + project.id, ); const customUser = updatedUsers.filter((u) => u.roleId === customRole.id); @@ -930,19 +930,19 @@ test("should change a users role in the project", async () => { expect(customUser[0].name).toBe(projectUser.name); }); -test("should update role for user on project", async () => { +test('should update role for user on project', async () => { const project = { - id: "update-users", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'update-users', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "update99@getunleash.io", + name: 'Some Member', + email: 'update99@getunleash.io', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -952,13 +952,13 @@ test("should update role for user on project", async () => { project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await projectService.changeRole( project.id, ownerRole.id, projectMember1.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -969,25 +969,25 @@ test("should update role for user on project", async () => { expect(ownerUsers).toHaveLength(2); }); -test("should able to assign role without existing members", async () => { +test('should able to assign role without existing members', async () => { const project = { - id: "update-users-test", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'update-users-test', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "update1999@getunleash.io", + name: 'Some Member', + email: 'update1999@getunleash.io', }); const testRole = await stores.roleStore.create({ - name: "Power user", - roleType: "custom", - description: "Grants access to modify all environments", + name: 'Power user', + roleType: 'custom', + description: 'Grants access to modify all environments', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -996,13 +996,13 @@ test("should able to assign role without existing members", async () => { project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await projectService.changeRole( project.id, testRole.id, projectMember1.id, - "test" + 'test', ); const { users } = await projectService.getAccessToProject(project.id); @@ -1013,19 +1013,19 @@ test("should able to assign role without existing members", async () => { expect(testUsers).toHaveLength(1); }); -test("should not update role for user on project when she is the owner", async () => { +test('should not update role for user on project when she is the owner', async () => { const project = { - id: "update-users-not-allowed", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'update-users-not-allowed', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); const projectMember1 = await stores.userStore.insert({ - name: "Some Member", - email: "update991@getunleash.io", + name: 'Some Member', + email: 'update991@getunleash.io', }); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -1034,7 +1034,7 @@ test("should not update role for user on project when she is the owner", async ( project.id, memberRole.id, projectMember1.id, - "test" + 'test', ); await expect(async () => { @@ -1042,36 +1042,36 @@ test("should not update role for user on project when she is the owner", async ( project.id, memberRole.id, user.id, - "test" + 'test', ); }).rejects.toThrowError( - new Error("A project must have at least one owner") + new Error('A project must have at least one owner'), ); }); -test("Should allow bulk update of group permissions", async () => { +test('Should allow bulk update of group permissions', async () => { const project = { - id: "bulk-update-project", - name: "bulk-update-project", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'bulk-update-project', + name: 'bulk-update-project', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const groupStore = stores.groupStore; const user1 = await stores.userStore.insert({ - name: "Vanessa Viewer", - email: "vanv@getunleash.io", + name: 'Vanessa Viewer', + email: 'vanv@getunleash.io', }); const group1 = await groupStore.create({ - name: "ViewersOnly", - description: "", + name: 'ViewersOnly', + description: '', }); const createFeatureRole = await accessService.createRole({ - name: "CreateRole", - description: "", + name: 'CreateRole', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -1084,21 +1084,21 @@ test("Should allow bulk update of group permissions", async () => { [createFeatureRole.id], [group1.id], [user1.id], - "some-admin-user" + 'some-admin-user', ); }); -test("Should bulk update of only users", async () => { - const project = "bulk-update-project-users"; +test('Should bulk update of only users', async () => { + const project = 'bulk-update-project-users'; const user1 = await stores.userStore.insert({ - name: "Van Viewer", - email: "vv@getunleash.io", + name: 'Van Viewer', + email: 'vv@getunleash.io', }); const createFeatureRole = await accessService.createRole({ - name: "CreateRoleForUsers", - description: "", + name: 'CreateRoleForUsers', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -1111,29 +1111,29 @@ test("Should bulk update of only users", async () => { [createFeatureRole.id], [], [user1.id], - "some-admin-user" + 'some-admin-user', ); }); -test("Should allow bulk update of only groups", async () => { +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, - defaultStickiness: "clientId", + id: 'bulk-update-project-only', + name: 'bulk-update-project-only', + mode: 'open' as const, + defaultStickiness: 'clientId', }; const groupStore = stores.groupStore; await projectService.createProject(project, user.id); const group1 = await groupStore.create({ - name: "ViewersOnly", - description: "", + name: 'ViewersOnly', + description: '', }); const createFeatureRole = await accessService.createRole({ - name: "CreateRoleForGroups", - description: "", + name: 'CreateRoleForGroups', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -1146,43 +1146,43 @@ test("Should allow bulk update of only groups", async () => { [createFeatureRole.id], [group1.id], [], - "some-admin-user" + 'some-admin-user', ); }); -test("Should allow permutations of roles, groups and users when adding a new access", async () => { +test('Should allow permutations of roles, groups and users when adding a new access', async () => { const project = { - id: "project-access-permutations", - name: "project-access-permutations", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'project-access-permutations', + name: 'project-access-permutations', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const group1 = await stores.groupStore.create({ - name: "permutation-group-1", - description: "", + name: 'permutation-group-1', + description: '', }); const group2 = await stores.groupStore.create({ - name: "permutation-group-2", - description: "", + name: 'permutation-group-2', + description: '', }); const user1 = await stores.userStore.insert({ - name: "permutation-user-1", - email: "pu1@getunleash.io", + name: 'permutation-user-1', + email: 'pu1@getunleash.io', }); const user2 = await stores.userStore.insert({ - name: "permutation-user-2", - email: "pu2@getunleash.io", + name: 'permutation-user-2', + email: 'pu2@getunleash.io', }); const role1 = await accessService.createRole({ - name: "permutation-role-1", - description: "", + name: 'permutation-role-1', + description: '', permissions: [ { id: 2, // CREATE_FEATURE @@ -1191,8 +1191,8 @@ test("Should allow permutations of roles, groups and users when adding a new acc }); const role2 = await accessService.createRole({ - name: "permutation-role-2", - description: "", + name: 'permutation-role-2', + description: '', permissions: [ { id: 7, // UPDATE_FEATURE @@ -1205,11 +1205,11 @@ test("Should allow permutations of roles, groups and users when adding a new acc [role1.id, role2.id], [group1.id, group2.id], [user1.id, user2.id], - "some-admin-user" + 'some-admin-user', ); const { users, groups } = await projectService.getAccessToProject( - project.id + project.id, ); expect(users).toHaveLength(2); @@ -1219,53 +1219,53 @@ test("Should allow permutations of roles, groups and users when adding a new acc expect(groups[0].roles).toStrictEqual([role1.id, role2.id]); }); -test("should only count active feature toggles for project", async () => { +test('should only count active feature toggles for project', async () => { const project = { - id: "only-active", - name: "New project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'only-active', + name: 'New project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: "only-active-t1", + name: 'only-active-t1', project: project.id, enabled: false, }); await stores.featureToggleStore.create(project.id, { - name: "only-active-t2", + name: 'only-active-t2', project: project.id, enabled: false, }); - await featureToggleService.archiveToggle("only-active-t2", "me"); + await featureToggleService.archiveToggle('only-active-t2', 'me'); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); expect(theProject?.featureCount).toBe(1); }); -test("should list projects with all features archived", async () => { +test('should list projects with all features archived', async () => { const project = { - id: "only-archived", - name: "Listed project", - description: "Blah", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'only-archived', + name: 'Listed project', + description: 'Blah', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { - name: "archived-toggle", + name: 'archived-toggle', project: project.id, enabled: false, }); - await featureToggleService.archiveToggle("archived-toggle", "me"); + await featureToggleService.archiveToggle('archived-toggle', 'me'); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); @@ -1274,34 +1274,34 @@ test("should list projects with all features archived", async () => { const updateEventCreatedAt = async (date: Date, featureName: string) => { return db.rawDatabase - .table("events") + .table('events') .update({ created_at: date }) .where({ feature_name: featureName }); }; const updateFeature = async (featureName: string, update: any) => { return db.rawDatabase - .table("features") + .table('features') .update(update) .where({ name: featureName }); }; -test("should calculate average time to production", async () => { +test('should calculate average time to production', async () => { const project = { - id: "average-time-to-prod", - name: "average-time-to-prod", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod', + name: 'average-time-to-prod', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const toggles = [ - { name: "average-prod-time" }, - { name: "average-prod-time-2" }, - { name: "average-prod-time-3" }, - { name: "average-prod-time-4" }, - { name: "average-prod-time-5" }, + { name: 'average-prod-time' }, + { name: 'average-prod-time-2' }, + { name: 'average-prod-time-3' }, + { name: 'average-prod-time-4' }, + { name: 'average-prod-time-5' }, ]; const featureToggles = await Promise.all( @@ -1309,9 +1309,9 @@ test("should calculate average time to production", async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user + user, ); - }) + }), ); await Promise.all( @@ -1321,22 +1321,22 @@ test("should calculate average time to production", async () => { enabled: true, project: project.id, featureName: toggle.name, - environment: "default", - createdBy: "Fredrik", - }) + environment: 'default', + createdBy: 'Fredrik', + }), ); - }) + }), ); - await updateEventCreatedAt(subDays(new Date(), 31), "average-prod-time-5"); + await updateEventCreatedAt(subDays(new Date(), 31), 'average-prod-time-5'); await Promise.all( featureToggles.map((toggle) => - updateFeature(toggle.name, { created_at: subDays(new Date(), 15) }) - ) + updateFeature(toggle.name, { created_at: subDays(new Date(), 15) }), + ), ); - await updateFeature("average-prod-time-5", { + await updateFeature('average-prod-time-5', { created_at: subDays(new Date(), 33), }); @@ -1344,110 +1344,110 @@ test("should calculate average time to production", async () => { expect(result.updates.avgTimeToProdCurrentWindow).toBe(11.4); }); -test("should calculate average time to production ignoring some items", async () => { +test('should calculate average time to production ignoring some items', async () => { const project = { - id: "average-time-to-prod-corner-cases", - name: "average-time-to-prod", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod-corner-cases', + name: 'average-time-to-prod', + mode: 'open' as const, + defaultStickiness: 'clientId', }; const makeEvent = (featureName: string) => ({ enabled: true, project: project.id, featureName, - environment: "default", - createdBy: "Fredrik", + environment: 'default', + createdBy: 'Fredrik', tags: [], }); await projectService.createProject(project, user.id); await stores.environmentStore.create({ - name: "customEnv", - type: "development", + name: 'customEnv', + type: 'development', }); - await environmentService.addEnvironmentToProject("customEnv", project.id); + await environmentService.addEnvironmentToProject('customEnv', project.id); // actual toggle we take for calculations - const toggle = { name: "main-toggle" }; + const toggle = { name: 'main-toggle' }; await featureToggleService.createFeatureToggle(project.id, toggle, user); await updateFeature(toggle.name, { created_at: subDays(new Date(), 20), }); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(toggle.name)) + new FeatureEnvironmentEvent(makeEvent(toggle.name)), ); // ignore events added after first enabled await updateEventCreatedAt(addDays(new Date(), 1), toggle.name); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(toggle.name)) + new FeatureEnvironmentEvent(makeEvent(toggle.name)), ); // ignore toggles enabled in non-prod envs - const devToggle = { name: "dev-toggle" }; + const devToggle = { name: 'dev-toggle' }; await featureToggleService.createFeatureToggle(project.id, devToggle, user); await eventService.storeEvent( new FeatureEnvironmentEvent({ ...makeEvent(devToggle.name), - environment: "customEnv", - }) + environment: 'customEnv', + }), ); // ignore toggles from other projects - const otherProjectToggle = { name: "other-project" }; + const otherProjectToggle = { name: 'other-project' }; await featureToggleService.createFeatureToggle( - "default", + 'default', otherProjectToggle, - user + user, ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)) + new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)), ); // ignore non-release toggles - const nonReleaseToggle = { name: "permission-toggle", type: "permission" }; + const nonReleaseToggle = { name: 'permission-toggle', type: 'permission' }; await featureToggleService.createFeatureToggle( project.id, nonReleaseToggle, - user + user, ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)) + new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)), ); // ignore toggles with events before toggle creation time - const previouslyDeleteToggle = { name: "previously-deleted" }; + const previouslyDeleteToggle = { name: 'previously-deleted' }; await featureToggleService.createFeatureToggle( project.id, previouslyDeleteToggle, - user + user, ); await eventService.storeEvent( - new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)) + new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)), ); await updateEventCreatedAt( subDays(new Date(), 30), - previouslyDeleteToggle.name + previouslyDeleteToggle.name, ); const result = await projectService.getStatusUpdates(project.id); expect(result.updates.avgTimeToProdCurrentWindow).toBe(20); }); -test("should get correct amount of features created in current and past window", async () => { +test('should get correct amount of features created in current and past window', async () => { const project = { - id: "features-created", - name: "features-created", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'features-created', + name: 'features-created', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const toggles = [ - { name: "features-created" }, - { name: "features-created-2" }, - { name: "features-created-3" }, - { name: "features-created-4" }, + { name: 'features-created' }, + { name: 'features-created-2' }, + { name: 'features-created-3' }, + { name: 'features-created-4' }, ]; await Promise.all( @@ -1455,9 +1455,9 @@ test("should get correct amount of features created in current and past window", return featureToggleService.createFeatureToggle( project.id, toggle, - user + user, ); - }) + }), ); await Promise.all([ @@ -1470,21 +1470,21 @@ test("should get correct amount of features created in current and past window", expect(result.updates.createdPastWindow).toBe(2); }); -test("should get correct amount of features archived in current and past window", async () => { +test('should get correct amount of features archived in current and past window', async () => { const project = { - id: "features-archived", - name: "features-archived", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'features-archived', + name: 'features-archived', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const toggles = [ - { name: "features-archived" }, - { name: "features-archived-2" }, - { name: "features-archived-3" }, - { name: "features-archived-4" }, + { name: 'features-archived' }, + { name: 'features-archived-2' }, + { name: 'features-archived-3' }, + { name: 'features-archived-4' }, ]; await Promise.all( @@ -1492,9 +1492,9 @@ test("should get correct amount of features archived in current and past window" return featureToggleService.createFeatureToggle( project.id, toggle, - user + user, ); - }) + }), ); await Promise.all([ @@ -1521,26 +1521,26 @@ test("should get correct amount of features archived in current and past window" expect(result.updates.archivedPastWindow).toBe(2); }); -test("should get correct amount of project members for current and past window", async () => { +test('should get correct amount of project members for current and past window', async () => { const project = { - id: "features-members", - name: "features-members", - mode: "open" as const, - defaultStickiness: "default", + id: 'features-members', + name: 'features-members', + mode: 'open' as const, + defaultStickiness: 'default', }; await projectService.createProject(project, user.id); const users = [ - { name: "memberOne", email: "memberOne@getunleash.io" }, - { name: "memberTwo", email: "memberTwo@getunleash.io" }, - { name: "memberThree", email: "memberThree@getunleash.io" }, - { name: "memberFour", email: "memberFour@getunleash.io" }, - { name: "memberFive", email: "memberFive@getunleash.io" }, + { name: 'memberOne', email: 'memberOne@getunleash.io' }, + { name: 'memberTwo', email: 'memberTwo@getunleash.io' }, + { name: 'memberThree', email: 'memberThree@getunleash.io' }, + { name: 'memberFour', email: 'memberFour@getunleash.io' }, + { name: 'memberFive', email: 'memberFive@getunleash.io' }, ]; const createdUsers = await Promise.all( - users.map((userObj) => stores.userStore.insert(userObj)) + users.map((userObj) => stores.userStore.insert(userObj)), ); const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -1550,9 +1550,9 @@ test("should get correct amount of project members for current and past window", project.id, memberRole.id, createdUser.id, - "test" - ) - ) + 'test', + ), + ), ); const result = await projectService.getStatusUpdates(project.id); @@ -1561,22 +1561,22 @@ test("should get correct amount of project members for current and past window", expect(result.updates.projectActivityPastWindow).toBe(0); }); -test("should return average time to production per toggle", async () => { +test('should return average time to production per toggle', async () => { const project = { - id: "average-time-to-prod-per-toggle", - name: "average-time-to-prod-per-toggle", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod-per-toggle', + name: 'average-time-to-prod-per-toggle', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project, user.id); const toggles = [ - { name: "average-prod-time-pt", subdays: 7 }, - { name: "average-prod-time-pt-2", subdays: 14 }, - { name: "average-prod-time-pt-3", subdays: 40 }, - { name: "average-prod-time-pt-4", subdays: 15 }, - { name: "average-prod-time-pt-5", subdays: 2 }, + { name: 'average-prod-time-pt', subdays: 7 }, + { name: 'average-prod-time-pt-2', subdays: 14 }, + { name: 'average-prod-time-pt-3', subdays: 40 }, + { name: 'average-prod-time-pt-4', subdays: 15 }, + { name: 'average-prod-time-pt-5', subdays: 2 }, ]; const featureToggles = await Promise.all( @@ -1584,9 +1584,9 @@ test("should return average time to production per toggle", async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user + user, ); - }) + }), ); await Promise.all( @@ -1596,19 +1596,19 @@ test("should return average time to production per toggle", async () => { enabled: true, project: project.id, featureName: toggle.name, - environment: "default", - createdBy: "Fredrik", - }) + environment: 'default', + createdBy: 'Fredrik', + }), ); - }) + }), ); await Promise.all( toggles.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }) - ) + }), + ), ); const result = await projectService.getDoraMetrics(project.id); @@ -1618,33 +1618,33 @@ test("should return average time to production per toggle", async () => { expect(result.projectAverage).toBeTruthy(); }); -test("should return average time to production per toggle for a specific project", async () => { +test('should return average time to production per toggle for a specific project', async () => { const project1 = { - id: "average-time-to-prod-per-toggle-1", - name: "Project 1", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod-per-toggle-1', + name: 'Project 1', + mode: 'open' as const, + defaultStickiness: 'clientId', }; const project2 = { - id: "average-time-to-prod-per-toggle-2", - name: "Project 2", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod-per-toggle-2', + name: 'Project 2', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project1, user.id); await projectService.createProject(project2, user.id); const togglesProject1 = [ - { name: "average-prod-time-pt-10", subdays: 7 }, - { name: "average-prod-time-pt-11", subdays: 14 }, - { name: "average-prod-time-pt-12", subdays: 40 }, + { name: 'average-prod-time-pt-10', subdays: 7 }, + { name: 'average-prod-time-pt-11', subdays: 14 }, + { name: 'average-prod-time-pt-12', subdays: 40 }, ]; const togglesProject2 = [ - { name: "average-prod-time-pt-13", subdays: 15 }, - { name: "average-prod-time-pt-14", subdays: 2 }, + { name: 'average-prod-time-pt-13', subdays: 15 }, + { name: 'average-prod-time-pt-14', subdays: 2 }, ]; const featureTogglesProject1 = await Promise.all( @@ -1652,9 +1652,9 @@ test("should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project1.id, toggle, - user + user, ); - }) + }), ); const featureTogglesProject2 = await Promise.all( @@ -1662,9 +1662,9 @@ test("should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project2.id, toggle, - user + user, ); - }) + }), ); await Promise.all( @@ -1674,11 +1674,11 @@ test("should return average time to production per toggle for a specific project enabled: true, project: project1.id, featureName: toggle.name, - environment: "default", - createdBy: "Fredrik", - }) + environment: 'default', + createdBy: 'Fredrik', + }), ); - }) + }), ); await Promise.all( @@ -1688,27 +1688,27 @@ test("should return average time to production per toggle for a specific project enabled: true, project: project2.id, featureName: toggle.name, - environment: "default", - createdBy: "Fredrik", - }) + environment: 'default', + createdBy: 'Fredrik', + }), ); - }) + }), ); await Promise.all( togglesProject1.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }) - ) + }), + ), ); await Promise.all( togglesProject2.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }) - ) + }), + ), ); const resultProject1 = await projectService.getDoraMetrics(project1.id); @@ -1718,20 +1718,20 @@ test("should return average time to production per toggle for a specific project expect(resultProject2.features).toHaveLength(2); }); -test("should return average time to production per toggle and include archived toggles", async () => { +test('should return average time to production per toggle and include archived toggles', async () => { const project1 = { - id: "average-time-to-prod-per-toggle-12", - name: "Project 1", - mode: "open" as const, - defaultStickiness: "clientId", + id: 'average-time-to-prod-per-toggle-12', + name: 'Project 1', + mode: 'open' as const, + defaultStickiness: 'clientId', }; await projectService.createProject(project1, user.id); const togglesProject1 = [ - { name: "average-prod-time-pta-10", subdays: 7 }, - { name: "average-prod-time-pta-11", subdays: 14 }, - { name: "average-prod-time-pta-12", subdays: 40 }, + { name: 'average-prod-time-pta-10', subdays: 7 }, + { name: 'average-prod-time-pta-11', subdays: 14 }, + { name: 'average-prod-time-pta-12', subdays: 40 }, ]; const featureTogglesProject1 = await Promise.all( @@ -1739,9 +1739,9 @@ test("should return average time to production per toggle and include archived t return featureToggleService.createFeatureToggle( project1.id, toggle, - user + user, ); - }) + }), ); await Promise.all( @@ -1751,42 +1751,42 @@ test("should return average time to production per toggle and include archived t enabled: true, project: project1.id, featureName: toggle.name, - environment: "default", - createdBy: "Fredrik", - }) + environment: 'default', + createdBy: 'Fredrik', + }), ); - }) + }), ); await Promise.all( togglesProject1.map((toggle) => updateFeature(toggle.name, { created_at: subDays(new Date(), toggle.subdays), - }) - ) + }), + ), ); - await featureToggleService.archiveToggle("average-prod-time-pta-12", user); + await featureToggleService.archiveToggle('average-prod-time-pta-12', user); const resultProject1 = await projectService.getDoraMetrics(project1.id); expect(resultProject1.features).toHaveLength(3); }); -describe("feature flag naming patterns", () => { +describe('feature flag naming patterns', () => { test(`should clear existing example and description if the payload doesn't contain them`, async () => { const featureNaming = { - pattern: ".+", - example: "example", - description: "description", + pattern: '.+', + example: 'example', + description: 'description', }; const project = { - id: "feature-flag-naming-patterns-cleanup", - name: "Project", - mode: "open" as const, - defaultStickiness: "clientId", - description: "description", + id: 'feature-flag-naming-patterns-cleanup', + name: 'Project', + mode: 'open' as const, + defaultStickiness: 'clientId', + description: 'description', featureNaming, }; @@ -1795,16 +1795,16 @@ describe("feature flag naming patterns", () => { await projectService.updateProjectEnterpriseSettings(project, user); expect( - (await projectService.getProject(project.id)).featureNaming + (await projectService.getProject(project.id)).featureNaming, ).toMatchObject(featureNaming); - const newPattern = "new-pattern.+"; + const newPattern = 'new-pattern.+'; await projectService.updateProjectEnterpriseSettings( { ...project, featureNaming: { pattern: newPattern }, }, - user.id + user.id, ); const updatedProject = await projectService.getProject(project.id); From 766d1e32b87941ed11636b8d8dd67f215d1bf43a Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 12:54:45 +0200 Subject: [PATCH 10/16] fix: migration --- .../last-seen/last-seen-store.ts | 29 ++++++------------- ...231003113443-last-seen-at-metrics-table.js | 2 +- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/lib/services/client-metrics/last-seen/last-seen-store.ts b/src/lib/services/client-metrics/last-seen/last-seen-store.ts index 65672f7553d5..26b3bcdd454d 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-store.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-store.ts @@ -32,15 +32,6 @@ export default class LastSeenStore implements ILastSeenStore { }); } - async exists(name: string): Promise { - const result = await this.db.raw( - 'SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present', - [name], - ); - const { present } = result.rows[0]; - return present; - } - async setLastSeen(data: LastSeenInput[]): Promise { const now = new Date(); @@ -55,17 +46,15 @@ export default class LastSeenStore implements ILastSeenStore { const batchSize = 1000; - await this.db.transaction(async (trx) => { - for (let i = 0; i < inserts.length; i += batchSize) { - const batch = inserts.slice(i, i + batchSize); - // Knex optimizes multi row insert when given an array: - // https://knexjs.org/guide/query-builder.html#insert - await trx(TABLE) - .insert(batch) - .onConflict(['feature_name', 'environment']) - .merge(); - } - }); + for (let i = 0; i < inserts.length; i += batchSize) { + const batch = inserts.slice(i, i + batchSize); + // Knex optimizes multi row insert when given an array: + // https://knexjs.org/guide/query-builder.html#insert + await this.db(TABLE) + .insert(batch) + .onConflict(['feature_name', 'environment']) + .merge(); + } } catch (err) { this.logger.error('Could not update lastSeen, error: ', err); } diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js index 7cce0d8a0100..cecddadd79a3 100644 --- a/src/migrations/20231003113443-last-seen-at-metrics-table.js +++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js @@ -7,7 +7,7 @@ exports.up = function (db, callback) { feature_name VARCHAR(255), environment VARCHAR(100), last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL, - PRIMARY KEY (feature_name, environment) + PRIMARY KEY (feature_name, environment), FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE ); From 83ece0789f8a27830bdfa09de15e41b1bcd34615 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 13:36:44 +0200 Subject: [PATCH 11/16] fix: snapshot --- src/lib/__snapshots__/create-config.test.ts.snap | 2 ++ src/lib/services/client-metrics/last-seen/last-seen-store.ts | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 7370414379c4..7780af340179 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -110,6 +110,7 @@ exports[`should create default config 1`] = ` "responseTimeWithAppNameKillSwitch": false, "strictSchemaValidation": false, "transactionalDecorator": false, + "useLastSeenRefactor": false, "variantTypeNumber": false, }, }, @@ -150,6 +151,7 @@ exports[`should create default config 1`] = ` "responseTimeWithAppNameKillSwitch": false, "strictSchemaValidation": false, "transactionalDecorator": false, + "useLastSeenRefactor": false, "variantTypeNumber": false, }, "externalResolver": { diff --git a/src/lib/services/client-metrics/last-seen/last-seen-store.ts b/src/lib/services/client-metrics/last-seen/last-seen-store.ts index 26b3bcdd454d..183cde017c92 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-store.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-store.ts @@ -6,7 +6,6 @@ import metricsHelper from '../../..//util/metrics-helper'; import { LastSeenInput } from './last-seen-service'; import { ILastSeenStore } from './types/last-seen-store-type'; -// const LAST_SEEN_AT_COLUMNS = ['feature_name', 'environment', 'last_seen_at']; const TABLE = 'last_seen_at_metrics'; export interface FeaturesTable { From 2407520fa613990e4ce8d4becf748f006c5b8cb5 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 13:50:28 +0200 Subject: [PATCH 12/16] fix: down migration --- src/migrations/20231003113443-last-seen-at-metrics-table.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/migrations/20231003113443-last-seen-at-metrics-table.js b/src/migrations/20231003113443-last-seen-at-metrics-table.js index cecddadd79a3..70a83a03c659 100644 --- a/src/migrations/20231003113443-last-seen-at-metrics-table.js +++ b/src/migrations/20231003113443-last-seen-at-metrics-table.js @@ -18,5 +18,9 @@ exports.up = function (db, callback) { }; exports.down = function (db, callback) { - callback(); + db.runSql( + `DROP TABLE last_seen_at_metrics; + `, + callback(), + ); }; From 8742f5cf0b2782aa2133663e0f89b8236d0fd55a Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 14:52:11 +0200 Subject: [PATCH 13/16] fix: tests --- src/lib/routes/admin-api/project/api-token.ts | 3 ++- src/lib/routes/client-api/metrics.test.ts | 18 ++++++++++++------ .../api/admin/project/api-token.e2e.test.ts | 14 +++++++++----- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts index 0a2cf57c55c8..2cdc1ff013ca 100644 --- a/src/lib/routes/admin-api/project/api-token.ts +++ b/src/lib/routes/admin-api/project/api-token.ts @@ -149,8 +149,9 @@ export class ProjectApiTokenController extends Controller { ): Promise { const { user } = req; const { projectId } = req.params; - await this.projectService.getProject(projectId); // Validates that the project exists + const project = await this.projectService.getProject(projectId); // Validates that the project exists + console.log('project', project); const projectTokens = await this.accessibleTokens(user, projectId); this.openApiService.respondWithValidation( 200, diff --git a/src/lib/routes/client-api/metrics.test.ts b/src/lib/routes/client-api/metrics.test.ts index 355039ab4c00..dfebb46d3eae 100644 --- a/src/lib/routes/client-api/metrics.test.ts +++ b/src/lib/routes/client-api/metrics.test.ts @@ -5,21 +5,25 @@ import { createTestConfig } from '../../../test/config/test-config'; import { clientMetricsSchema } from '../../services/client-metrics/schema'; import { createServices } from '../../services'; import { IUnleashOptions, IUnleashServices, IUnleashStores } from '../../types'; +import dbInit from '../../../test/e2e/helpers/database-init'; -async function getSetup(opts?: IUnleashOptions) { - const stores = createStores(); +let db; +async function getSetup(opts?: IUnleashOptions) { const config = createTestConfig(opts); - const services = createServices(stores, config); - const app = await getApp(config, stores, services); + db = await dbInit('metrics', config.getLogger); + + const services = createServices(db.stores, config, db.rawDatabase); + const app = await getApp(config, db.stores, services); return { request: supertest(app), - stores, + stores: db.stores, services, - destroy: () => { + destroy: async () => { services.versionService.destroy(); services.clientInstanceService.destroy(); + await db.destroy(); }, }; } @@ -207,6 +211,8 @@ test('should set lastSeen on toggle', async () => { await services.lastSeenService.store(); const toggle = await stores.featureToggleStore.get('toggleLastSeen'); + console.log(toggle); + expect(toggle.lastSeenAt).toBeTruthy(); }); diff --git a/src/test/e2e/api/admin/project/api-token.e2e.test.ts b/src/test/e2e/api/admin/project/api-token.e2e.test.ts index e85a31ba72e0..d0dbf769573f 100644 --- a/src/test/e2e/api/admin/project/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/project/api-token.e2e.test.ts @@ -10,13 +10,17 @@ let apiTokenStore: ApiTokenStore; beforeAll(async () => { db = await dbInit('projects_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); apiTokenStore = db.stores.apiTokenStore; }); From 7d14af55eaf2f44935787a0d405e7a031a36c4d6 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 6 Oct 2023 14:56:03 +0200 Subject: [PATCH 14/16] fix: add raw database --- src/test/e2e/api/admin/api-token.e2e.test.ts | 14 +++++++++----- src/test/e2e/api/admin/favorites.e2e.test.ts | 14 +++++++++----- .../e2e/api/admin/project/environments.e2e.test.ts | 14 +++++++++----- .../admin/project/project.api.tokens.e2e.test.ts | 14 +++++++++----- .../e2e/api/admin/project/projects.e2e.test.ts | 14 +++++++++----- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts index d1f0ca695b4e..34ba4fd45859 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -10,13 +10,17 @@ let app; beforeAll(async () => { db = await dbInit('token_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); }); afterAll(async () => { diff --git a/src/test/e2e/api/admin/favorites.e2e.test.ts b/src/test/e2e/api/admin/favorites.e2e.test.ts index 205b7b1819fe..84e8a5015177 100644 --- a/src/test/e2e/api/admin/favorites.e2e.test.ts +++ b/src/test/e2e/api/admin/favorites.e2e.test.ts @@ -81,13 +81,17 @@ const getProjects = async () => { beforeAll(async () => { db = await dbInit('favorites_api_serial', getLogger); - app = await setupAppWithAuth(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithAuth( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); stores = db.stores; accessService = app.services.accessService; diff --git a/src/test/e2e/api/admin/project/environments.e2e.test.ts b/src/test/e2e/api/admin/project/environments.e2e.test.ts index d951841aa0ef..fc28e8ebcfab 100644 --- a/src/test/e2e/api/admin/project/environments.e2e.test.ts +++ b/src/test/e2e/api/admin/project/environments.e2e.test.ts @@ -11,13 +11,17 @@ let db: ITestDb; beforeAll(async () => { db = await dbInit('project_environments_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); }); afterEach(async () => { diff --git a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts index a1ad6ed8d103..9bb3a7e0b256 100644 --- a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts @@ -11,13 +11,17 @@ let db: ITestDb; beforeAll(async () => { db = await dbInit('project_api_tokens_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); }); afterEach(async () => { diff --git a/src/test/e2e/api/admin/project/projects.e2e.test.ts b/src/test/e2e/api/admin/project/projects.e2e.test.ts index e5089d199719..e77f05a13514 100644 --- a/src/test/e2e/api/admin/project/projects.e2e.test.ts +++ b/src/test/e2e/api/admin/project/projects.e2e.test.ts @@ -10,13 +10,17 @@ let projectStore: ProjectStore; beforeAll(async () => { db = await dbInit('projects_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); projectStore = db.stores.projectStore; }); From 98f5629193ccac9aef20abbab1aee647b3843f2b Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Mon, 9 Oct 2023 09:05:45 +0200 Subject: [PATCH 15/16] fix: tests --- .../client-metrics/last-seen-service.test.ts | 109 ------------------ .../last-seen/last-seen-service.test.ts | 57 ++++++++- .../e2e/api/admin/api-token.auth.e2e.test.ts | 7 +- .../admin/project/project.health.e2e.test.ts | 14 ++- src/test/e2e/api/client/metricsV2.e2e.test.ts | 2 +- src/test/e2e/api/proxy/proxy.e2e.test.ts | 10 +- src/test/e2e/helpers/test-helper.ts | 3 +- 7 files changed, 77 insertions(+), 125 deletions(-) delete mode 100644 src/lib/services/client-metrics/last-seen-service.test.ts diff --git a/src/lib/services/client-metrics/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen-service.test.ts deleted file mode 100644 index 9cd3ea43b012..000000000000 --- a/src/lib/services/client-metrics/last-seen-service.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import createStores from '../../../test/fixtures/store'; -import EventEmitter from 'events'; -import getLogger from '../../../test/fixtures/no-logger'; -import { IUnleashConfig } from '../../types'; -import { LastSeenService } from './last-seen/last-seen-service'; - -function initLastSeenService(flagEnabled = true) { - const stores = createStores(); - - const eventBus = new EventEmitter(); - eventBus.emit = jest.fn(); - - const config = { - eventBus, - getLogger, - flagResolver: { - isEnabled: () => { - return flagEnabled; - }, - }, - } as unknown as IUnleashConfig; - - const lastSeenService = new LastSeenService( - { - lastSeenStore: stores.lastSeenStore, - featureToggleStore: stores.featureToggleStore, - }, - config, - ); - - return { - lastSeenService, - featureToggleStore: stores.featureToggleStore, - lastSeenStore: stores.lastSeenStore, - }; -} - -test('should not add duplicates per feature/environment', async () => { - const { lastSeenService, featureToggleStore } = initLastSeenService(false); - - lastSeenService.updateLastSeen([ - { - featureName: 'myFeature', - environment: 'development', - yes: 1, - no: 0, - appName: 'test', - timestamp: new Date(), - }, - ]); - - lastSeenService.updateLastSeen([ - { - featureName: 'myFeature', - environment: 'development', - yes: 1, - no: 0, - appName: 'test', - timestamp: new Date(), - }, - ]); - featureToggleStore.setLastSeen = jest.fn(); - await lastSeenService.store(); - - expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([ - { - environment: 'development', - featureName: 'myFeature', - }, - ]); -}); - -test('should call last seen at store with correct data', async () => { - const { lastSeenService, lastSeenStore, featureToggleStore } = - initLastSeenService(true); - - lastSeenService.updateLastSeen([ - { - featureName: 'myFeature', - environment: 'development', - yes: 1, - no: 0, - appName: 'test', - timestamp: new Date(), - }, - ]); - - lastSeenService.updateLastSeen([ - { - featureName: 'myFeature', - environment: 'development', - yes: 1, - no: 0, - appName: 'test', - timestamp: new Date(), - }, - ]); - lastSeenStore.setLastSeen = jest.fn(); - featureToggleStore.setLastSeen = jest.fn(); - await lastSeenService.store(); - - expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([ - { - environment: 'development', - featureName: 'myFeature', - }, - ]); - expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0); -}); diff --git a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts index 729d83557221..37ac58a83324 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts @@ -2,8 +2,7 @@ import createStores from '../../../../test/fixtures/store'; import EventEmitter from 'events'; import getLogger from '../../../../test/fixtures/no-logger'; import { IUnleashConfig } from '../../../types'; - -import { createFakeLastSeenService } from './createLastSeenService'; +import { LastSeenService } from '../last-seen/last-seen-service'; function initLastSeenService(flagEnabled = true) { const stores = createStores(); @@ -21,13 +20,23 @@ function initLastSeenService(flagEnabled = true) { }, } as unknown as IUnleashConfig; - const lastSeenService = createFakeLastSeenService(config); + const lastSeenService = new LastSeenService( + { + lastSeenStore: stores.lastSeenStore, + featureToggleStore: stores.featureToggleStore, + }, + config, + ); - return { lastSeenService, featureToggleStore: stores.featureToggleStore }; + return { + lastSeenService, + featureToggleStore: stores.featureToggleStore, + lastSeenStore: stores.lastSeenStore, + }; } test('should not add duplicates per feature/environment', async () => { - const { lastSeenService, featureToggleStore } = initLastSeenService(); + const { lastSeenService, featureToggleStore } = initLastSeenService(false); lastSeenService.updateLastSeen([ { @@ -60,3 +69,41 @@ test('should not add duplicates per feature/environment', async () => { }, ]); }); + +test('should call last seen at store with correct data', async () => { + const { lastSeenService, lastSeenStore, featureToggleStore } = + initLastSeenService(true); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + + lastSeenService.updateLastSeen([ + { + featureName: 'myFeature', + environment: 'development', + yes: 1, + no: 0, + appName: 'test', + timestamp: new Date(), + }, + ]); + lastSeenStore.setLastSeen = jest.fn(); + featureToggleStore.setLastSeen = jest.fn(); + await lastSeenService.store(); + + expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([ + { + environment: 'development', + featureName: 'myFeature', + }, + ]); + expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0); +}); diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts index 97b711971959..b772366afe52 100644 --- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts @@ -206,7 +206,12 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn }); }; - const { request, destroy } = await setupAppWithCustomAuth(stores, preHook); + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + {}, + db.rawDatabase, + ); await request .post('/api/admin/projects/default/api-tokens') diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts index 62de38120932..5a7a6c4818f4 100644 --- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts @@ -8,13 +8,17 @@ let user; beforeAll(async () => { db = await dbInit('project_health_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); user = await db.stores.userStore.insert({ name: 'Some Name', email: 'test@getunleash.io', diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts index f7cd95c17138..327707e2ca2c 100644 --- a/src/test/e2e/api/client/metricsV2.e2e.test.ts +++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts @@ -11,7 +11,7 @@ let defaultToken; beforeAll(async () => { db = await dbInit('metrics_two_api_client', getLogger); - app = await setupAppWithAuth(db.stores, {}); + app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); defaultToken = await app.services.apiTokenService.createApiToken({ type: ApiTokenType.CLIENT, project: 'default', diff --git a/src/test/e2e/api/proxy/proxy.e2e.test.ts b/src/test/e2e/api/proxy/proxy.e2e.test.ts index f18db74384e8..1f9c8955e332 100644 --- a/src/test/e2e/api/proxy/proxy.e2e.test.ts +++ b/src/test/e2e/api/proxy/proxy.e2e.test.ts @@ -21,9 +21,13 @@ let db: ITestDb; beforeAll(async () => { db = await dbInit('proxy', getLogger); - app = await setupAppWithAuth(db.stores, { - frontendApiOrigins: ['https://example.com'], - }); + app = await setupAppWithAuth( + db.stores, + { + frontendApiOrigins: ['https://example.com'], + }, + db.rawDatabase, + ); }); afterEach(() => { diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts index 81ee8f4141e0..a9a559c75f2b 100644 --- a/src/test/e2e/helpers/test-helper.ts +++ b/src/test/e2e/helpers/test-helper.ts @@ -263,8 +263,9 @@ export async function setupAppWithCustomAuth( preHook: Function, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types customOptions?: any, + db?: Db, ): Promise { - return createApp(stores, IAuthType.CUSTOM, preHook, customOptions); + return createApp(stores, IAuthType.CUSTOM, preHook, customOptions, db); } export async function setupAppWithBaseUrl( From a47499859fce37cc8ac52fcd97469f2fd9536f17 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Mon, 9 Oct 2023 09:19:27 +0200 Subject: [PATCH 16/16] fix: move test --- .../last-seen/tests/last-seen-mapper.test.ts | 4 ++-- .../last-seen/{ => tests}/last-seen-service.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/lib/services/client-metrics/last-seen/{ => tests}/last-seen-service.test.ts (91%) diff --git a/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts index 023d873ba2a4..07d9ec7f5d2c 100644 --- a/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts +++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-mapper.test.ts @@ -67,7 +67,7 @@ test('should produce correct output when mapped', () => { type: 'development', sortOrder: 2, variantCount: 0, - // @ts-ignore - this type is wrong. Should be string + // @ts-ignore lastSeenAt: '2023-10-04T19:03:29.682Z', }, { @@ -76,7 +76,7 @@ test('should produce correct output when mapped', () => { type: 'production', sortOrder: 3, variantCount: 0, - // @ts-ignore - this type is wrong, should be string + // @ts-ignore lastSeenAt: '2023-10-05T07:27:04.286Z', }, ], diff --git a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts similarity index 91% rename from src/lib/services/client-metrics/last-seen/last-seen-service.test.ts rename to src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts index 37ac58a83324..8859563d03cb 100644 --- a/src/lib/services/client-metrics/last-seen/last-seen-service.test.ts +++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.test.ts @@ -1,8 +1,8 @@ -import createStores from '../../../../test/fixtures/store'; +import createStores from '../../../../../test/fixtures/store'; import EventEmitter from 'events'; -import getLogger from '../../../../test/fixtures/no-logger'; -import { IUnleashConfig } from '../../../types'; -import { LastSeenService } from '../last-seen/last-seen-service'; +import getLogger from '../../../../../test/fixtures/no-logger'; +import { IUnleashConfig } from '../../../../types'; +import { LastSeenService } from '../last-seen-service'; function initLastSeenService(flagEnabled = true) { const stores = createStores();