1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-18 11:14:57 +02:00
unleash.unleash/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts
Gastón Fournier 9eb19618bf
fix: stickiness should be preserved on strategy updates (#10439)
## About the changes
When stickiness was set to empty or undefined while updating a strategy
via API the stickiness would be lost.

This adds a validation step after creating a strategy, that updating
with the same data used to create the strategy yields the same result.

The main change was lifting the default logic from the store layer to
the service layer and adapting tests accordingly
2025-07-30 15:47:51 +02:00

351 lines
11 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import type {
FeatureToggleWithEnvironment,
IFeatureOverview,
IFeatureToggleClient,
IFeatureToggleQuery,
IFeatureStrategy,
FeatureToggle,
} from '../../../types/model.js';
import NotFoundError from '../../../error/notfound-error.js';
import type { IFeatureStrategiesStore } from '../types/feature-toggle-strategies-store-type.js';
import type { IFeatureProjectUserParams } from '../feature-toggle-controller.js';
import { ALL_PROJECTS } from '../../../util/index.js';
interface ProjectEnvironment {
projectName: string;
environment: string;
}
export default class FakeFeatureStrategiesStore
implements IFeatureStrategiesStore
{
environmentAndFeature: Map<string, any[]> = new Map();
projectToEnvironment: ProjectEnvironment[] = [];
featureStrategies: IFeatureStrategy[] = [];
featureToggles: FeatureToggle[] = [];
async createStrategyFeatureEnv(
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
): Promise<IFeatureStrategy> {
const newStrat = { ...strategyConfig, id: randomUUID() };
this.featureStrategies.push(newStrat);
return Promise.resolve(newStrat);
}
async getStrategiesByContextField(
contextFieldName: string,
): Promise<IFeatureStrategy[]> {
const strategies = this.featureStrategies.filter((strategy) =>
strategy.constraints.some(
(constraint) => constraint.contextName === contextFieldName,
),
);
return Promise.resolve(strategies);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async createFeature(feature: any): Promise<void> {
this.featureToggles.push({
project: feature.project || 'default',
createdAt: new Date(),
archived: false,
...feature,
});
return Promise.resolve();
}
async deleteFeatureStrategies(): Promise<void> {
this.featureStrategies = [];
return Promise.resolve();
}
async hasStrategy(id: string): Promise<boolean> {
return this.featureStrategies.some((s) => s.id === id);
}
async get(id: string): Promise<IFeatureStrategy | undefined> {
return this.featureStrategies.find((s) => s.id === id);
}
async exists(key: string): Promise<boolean> {
return this.featureStrategies.some((s) => s.id === key);
}
async delete(key: string): Promise<void> {
this.featureStrategies.splice(
this.featureStrategies.findIndex((s) => s.id === key),
1,
);
}
async deleteAll(): Promise<void> {
this.featureStrategies = [];
}
// FIXME: implement
async updateSortOrder(id: string, sortOrder: number): Promise<void> {
const found = this.featureStrategies.find((item) => item.id === id);
if (found) {
found.sortOrder = sortOrder;
}
}
destroy(): void {
throw new Error('Method not implemented.');
}
async removeAllStrategiesForFeatureEnv(
feature_name: string,
environment: string,
): Promise<void> {
const toRemove = this.featureStrategies.filter(
(fS) =>
fS.featureName === feature_name &&
fS.environment === environment,
);
this.featureStrategies = this.featureStrategies.filter(
(f) =>
!toRemove.some(
(r) =>
r.featureName === f.featureName &&
r.environment === f.environment,
),
);
return Promise.resolve();
}
async getAll(): Promise<IFeatureStrategy[]> {
return Promise.resolve(this.featureStrategies);
}
async getStrategiesForFeatureEnv(
project_name: string,
feature_name: string,
environment: string,
): Promise<IFeatureStrategy[]> {
const rows = this.featureStrategies.filter(
(fS) =>
fS.projectId === project_name &&
fS.featureName === feature_name &&
fS.environment === environment,
);
return Promise.resolve(rows);
}
async getFeatureToggleForEnvironment(
featureName: string,
// eslint-disable-next-line
environment: string,
): Promise<FeatureToggleWithEnvironment> {
const toggle = this.featureToggles.find((f) => f.name === featureName);
if (toggle) {
return { ...toggle, environments: [] };
}
throw new NotFoundError(
`Could not find feature with name ${featureName}`,
);
}
async getFeatureToggleWithEnvs(
featureName: string,
userId?: number,
archived: boolean = false,
): Promise<FeatureToggleWithEnvironment> {
const toggle = this.featureToggles.find(
(f) => f.name === featureName && f.archived === archived,
);
if (toggle) {
return { ...toggle, environments: [] };
}
throw new NotFoundError(
`Could not find feature with name ${featureName}`,
);
}
getFeatureToggleWithVariantEnvs(
featureName: string,
userId?: number,
archived?: boolean,
): Promise<FeatureToggleWithEnvironment> {
return this.getFeatureToggleWithEnvs(featureName, userId, archived);
}
async getFeatures(
featureQuery?: IFeatureToggleQuery,
archived: boolean = false,
): Promise<IFeatureToggleClient[]> {
const rows = this.featureToggles.filter((toggle) => {
if (featureQuery?.namePrefix) {
if (featureQuery?.project) {
return (
(toggle.name.startsWith(featureQuery.namePrefix) &&
featureQuery.project.some((project) =>
project.includes(toggle.project),
)) ||
featureQuery.project.includes(ALL_PROJECTS)
);
}
return toggle.name.startsWith(featureQuery.namePrefix);
}
if (featureQuery?.project) {
return (
featureQuery.project.some((project) =>
project.includes(toggle.project),
) || featureQuery.project.includes(ALL_PROJECTS)
);
}
return toggle.archived === archived;
});
const clientRows: IFeatureToggleClient[] = rows.map((t) => ({
...t,
enabled: true,
strategies: [],
description: t.description || undefined,
type: t.type || 'Release',
stale: t.stale || false,
variants: [],
tags: [],
}));
return Promise.resolve(clientRows);
}
async getStrategyById(id: string): Promise<IFeatureStrategy> {
const strat = this.featureStrategies.find((fS) => fS.id === id);
if (strat) {
return Promise.resolve(strat);
}
return Promise.reject(
new NotFoundError(`Could not find strategy with id ${id}`),
);
}
async connectEnvironmentAndFeature(
feature_name: string,
environment: string,
enabled: boolean = false,
): Promise<void> {
if (!this.environmentAndFeature.has(environment)) {
this.environmentAndFeature.set(environment, []);
}
this.environmentAndFeature
.get(environment)!
.push({ feature: feature_name, enabled });
return Promise.resolve();
}
async removeEnvironmentForFeature(
feature_name: string,
environment: string,
): Promise<void> {
this.environmentAndFeature.set(
environment,
this.environmentAndFeature
.get(environment)!
.filter((e) => e.featureName !== feature_name),
);
return Promise.resolve();
}
async disconnectEnvironmentFromProject(
environment: string,
project: string,
): Promise<void> {
this.projectToEnvironment = this.projectToEnvironment.filter(
(f) => f.projectName !== project && f.environment !== environment,
);
return Promise.resolve();
}
async updateStrategy(
id: string,
updates: Partial<IFeatureStrategy>,
): Promise<IFeatureStrategy> {
this.featureStrategies = this.featureStrategies.map((f) => {
if (f.id === id) {
return { ...f, ...updates };
}
return f;
});
return Promise.resolve(
this.featureStrategies.find((f) => f.id === id)!,
);
}
async deleteConfigurationsForProjectAndEnvironment(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: String,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
environment: String,
): Promise<void> {
return Promise.resolve();
}
async isEnvironmentEnabled(
featureName: string,
environment: string,
): Promise<boolean> {
const enabled =
this.environmentAndFeature
.get(environment)
?.find((f) => f.featureName === featureName)?.enabled || false;
return Promise.resolve(enabled);
}
async setProjectForStrategiesBelongingToFeature(
featureName: string,
newProjectId: string,
): Promise<void> {
this.featureStrategies = this.featureStrategies.map((f) => {
if (f.featureName === featureName) {
f.projectId = newProjectId;
}
return f;
});
return Promise.resolve(undefined);
}
async setEnvironmentEnabledStatus(
environment: string,
featureName: string,
enabled: boolean,
): Promise<boolean> {
return Promise.resolve(enabled);
}
getStrategiesBySegment(): Promise<IFeatureStrategy[]> {
throw new Error('Method not implemented.');
}
getFeatureOverview(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]> {
return Promise.resolve([]);
}
getAllByFeatures(
features: string[],
environment?: string,
): Promise<IFeatureStrategy[]> {
return Promise.resolve(
this.featureStrategies.filter(
(strategy) =>
features.includes(strategy.featureName) &&
strategy.environment === environment,
),
);
}
getCustomStrategiesInUseCount(): Promise<number> {
return Promise.resolve(3);
}
getDefaultStickiness(_projectId: string): Promise<string> {
return Promise.resolve('default');
}
}