diff --git a/package-lock.json b/package-lock.json
index cb4c21fa5..62eff1ac7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13225,6 +13225,12 @@
"path-key": "^2.0.0"
}
},
+ "npm-user-validate": {
+ "version": "1.0.0",
+ "resolved": false,
+ "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=",
+ "dev": true
+ },
"npmlog": {
"version": "4.1.2",
"resolved": false,
@@ -20756,7 +20762,7 @@
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
- "serialize-javascript": "^4.0.0",
+ "serialize-javascript": "^3.1.0",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 90c6b6463..0680b43f9 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts
index b395d3bdd..97c7f35c9 100644
--- a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts
+++ b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts
@@ -1,6 +1,7 @@
-import { TestBed, inject } from '@angular/core/testing';
+import { inject, TestBed } from '@angular/core/testing';
+import { Account, UserAgentApplication } from 'msal';
import { AzureAdB2CService } from './azure.ad.b2c.service';
-import { UserAgentApplication, Account } from 'msal';
+
describe('AzureAdB2CService', () => {
let service: AzureAdB2CService;
@@ -130,4 +131,20 @@ describe('AzureAdB2CService', () => {
expect(sessionStorage.getItem).toHaveBeenCalled();
expect(resp).toEqual(token);
});
+
+ it('should get email from UserAgentApplication', () => {
+ spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account);
+
+ const name = service.getName();
+
+ expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled();
+ });
+
+ it('should group from UserAgentApplication', () => {
+ spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account);
+
+ const name = service.getUserGroup();
+
+ expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled();
+ });
});
diff --git a/src/app/modules/login/services/azure.ad.b2c.service.ts b/src/app/modules/login/services/azure.ad.b2c.service.ts
index 2a0239fd6..b6945f9de 100644
--- a/src/app/modules/login/services/azure.ad.b2c.service.ts
+++ b/src/app/modules/login/services/azure.ad.b2c.service.ts
@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
-import { Observable, from } from 'rxjs';
-import { CLIENT_ID, AUTHORITY, SCOPES } from '../../../../environments/environment';
import { UserAgentApplication } from 'msal';
+import { from, Observable } from 'rxjs';
+
+import { AUTHORITY, CLIENT_ID, SCOPES } from '../../../../environments/environment';
@Injectable({
providedIn: 'root',
@@ -55,4 +56,12 @@ export class AzureAdB2CService {
getBearerToken(): string {
return sessionStorage.getItem('msal.idtoken');
}
+
+ getUserEmail(): string {
+ return this.msal.getAccount().idToken?.emails[0];
+ }
+
+ getUserGroup(): string {
+ return this.msal.getAccount().idToken?.extension_role;
+ }
}
diff --git a/src/app/modules/reports/pages/reports.component.ts b/src/app/modules/reports/pages/reports.component.ts
index bb276a460..dbdc3e14c 100644
--- a/src/app/modules/reports/pages/reports.component.ts
+++ b/src/app/modules/reports/pages/reports.component.ts
@@ -1,15 +1,9 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
@Component({
selector: 'app-reports',
templateUrl: './reports.component.html',
styleUrls: ['./reports.component.scss']
})
-export class ReportsComponent implements OnInit {
-
- constructor() { }
-
- ngOnInit(): void {
- }
-
+export class ReportsComponent {
}
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts b/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts
new file mode 100644
index 000000000..865a833b8
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts
@@ -0,0 +1,10 @@
+import { FeatureFilterConfiguration } from './filters/feature-filter-configuration';
+
+export interface FeatureToggleConfiguration {
+ id: string;
+ enabled: boolean;
+ description: string;
+ conditions: {
+ client_filters: FeatureFilterConfiguration[];
+ };
+}
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts
new file mode 100644
index 000000000..471d10f62
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts
@@ -0,0 +1,94 @@
+import { AppConfigurationClient } from '@azure/app-configuration';
+import { of } from 'rxjs';
+import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
+import { FeatureManagerService } from './feature-toggle-manager.service';
+import { FeatureToggleProvider } from './feature-toggle-provider.service';
+import { FeatureToggleModel } from './feature-toggle.model';
+import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
+import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model';
+
+
+describe('FeatureToggleManager', () => {
+ const featureToggleKey = 'foo';
+ const featureToggleLabel = 'dev';
+ const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
+ const aFeatureToggle = new FeatureToggleModel('any-id', true, []);
+ let service: FeatureManagerService;
+ let fakeFeatureToggleProvider;
+
+ describe('Features without filters', () => {
+ beforeEach(() => {
+
+ fakeFeatureToggleProvider = new FeatureToggleProvider(
+ new AppConfigurationClient(fakeAppConfigurationConnectionString),
+ new FeatureFilterProvider(new AzureAdB2CService())
+ );
+ spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aFeatureToggle));
+ service = new FeatureManagerService(fakeFeatureToggleProvider);
+ });
+ it('manager uses feature provider to build feature toggle model', async () => {
+ service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => {
+
+ expect(fakeFeatureToggleProvider).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel);
+ });
+ });
+
+ it('manager extracts enabled attribute from feature toggle model', async () => {
+ service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(value).toEqual(aFeatureToggle.enabled);
+ });
+ });
+ });
+
+
+ describe('Features with filters', () => {
+ const anyMatchingFilter = new TargetingFeatureFilterModel(
+ { Audience: { Groups: ['group-a'], Users: ['user-a'] } },
+ { username: 'user-b', group: 'group-a' }
+ );
+ const anyNotMatchingFilter = new TargetingFeatureFilterModel(
+ { Audience: { Groups: ['a-group'], Users: ['user-a'] } },
+ { username: 'user-b', group: 'b-group' }
+ );
+
+ let aToggleWithFilters;
+ let getFeatureToggleSpy;
+
+ beforeEach(() => {
+ aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyMatchingFilter]);
+ fakeFeatureToggleProvider = new FeatureToggleProvider(
+ new AppConfigurationClient(fakeAppConfigurationConnectionString),
+ new FeatureFilterProvider(new AzureAdB2CService())
+ );
+ getFeatureToggleSpy = spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters));
+ service = new FeatureManagerService(fakeFeatureToggleProvider);
+ });
+
+ it('manager uses feature provider to build feature toggle model', async () => {
+ service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(getFeatureToggleSpy).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel);
+ });
+ });
+
+ it('given a feature toggle with filters which match the verification, then the response is true', async () => {
+ service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(value).toEqual(true);
+ });
+ });
+
+ it('given a feature toggle with filters which do not match the verification, then the response is false', async () => {
+
+ aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter]);
+ fakeFeatureToggleProvider = new FeatureToggleProvider(
+ new AppConfigurationClient(fakeAppConfigurationConnectionString),
+ new FeatureFilterProvider(new AzureAdB2CService())
+ );
+ spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters));
+ service = new FeatureManagerService(fakeFeatureToggleProvider);
+
+ service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(value).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts
new file mode 100644
index 000000000..263b40e6c
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { FeatureToggleProvider } from './feature-toggle-provider.service';
+
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FeatureManagerService {
+
+ constructor(private featureToggleProvider: FeatureToggleProvider) { }
+
+ public isToggleEnabled(toggleName: string, toggleLabel?: string): Observable {
+ return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
+ map(featureToggle => featureToggle.enabled)
+ );
+ }
+
+ public isToggleEnabledForUser(toggleName: string, toggleLabel?: string): Observable {
+ return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
+ map(featureToggle => featureToggle.filters),
+ map(filters => filters.map(filter => filter.evaluate())),
+ map(filterEvaluations => filterEvaluations.includes(true))
+ );
+ }
+}
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts
new file mode 100644
index 000000000..5c6d9b5e4
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts
@@ -0,0 +1,80 @@
+import { AppConfigurationClient, GetConfigurationSettingResponse } from '@azure/app-configuration';
+import { of } from 'rxjs';
+import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
+import { FeatureToggleConfiguration } from './feature-toggle-configuration';
+import { FeatureToggleProvider } from './feature-toggle-provider.service';
+import { FeatureToggleModel } from './feature-toggle.model';
+import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
+import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model';
+
+
+describe('FeatureToggleProvider', () => {
+ const anyToggleResponse: FeatureToggleConfiguration = {
+ id: '1',
+ enabled: true,
+ description: 'any description',
+ conditions: {
+ client_filters: [{ name: 'any-name', parameters: {} }]
+ },
+ };
+
+ const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
+ const featureToggleKey = 'foo';
+ const featureToggleLabel = 'dev';
+ const fakeResponse: GetConfigurationSettingResponse = {
+ isReadOnly: true,
+ key: featureToggleKey,
+ _response: {
+ request: null,
+ bodyAsText: 'any-response',
+ headers: null,
+ parsedHeaders: null,
+ status: 200
+ },
+ statusCode: 200,
+ value: JSON.stringify(anyToggleResponse)
+ };
+ const anyFilter = new TargetingFeatureFilterModel(
+ { Audience: { Groups: ['a-group'], Users: ['any-user'] } },
+ { username: 'fakeuser@ioet.com', group: 'fake-group' }
+ );
+ let fakeConfigurationClient;
+ let fakeGetConfigurationSetting;
+ let fakeFeatureFilterProvider;
+ let getFilterConfigurationSpy;
+ let service;
+
+
+
+ beforeEach(() => {
+ fakeConfigurationClient = new AppConfigurationClient(fakeAppConfigurationConnectionString);
+ fakeGetConfigurationSetting = spyOn(fakeConfigurationClient, 'getConfigurationSetting').and.callFake(
+ () => of(fakeResponse).toPromise());
+
+ fakeFeatureFilterProvider = new FeatureFilterProvider(new AzureAdB2CService());
+ getFilterConfigurationSpy = spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and.
+ returnValue(anyFilter);
+ service = new FeatureToggleProvider(fakeConfigurationClient, fakeFeatureFilterProvider);
+ });
+
+ it('toggles are read using azure configuration client', async () => {
+ service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
+
+ expect(fakeGetConfigurationSetting).toHaveBeenCalledWith(
+ { key: `.appconfig.featureflag/${featureToggleKey}`, label: featureToggleLabel }
+ );
+ });
+ });
+
+ it('filters are built using the filterProvider', async () => {
+ service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(getFilterConfigurationSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('toggle model is built', async () => {
+ service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
+ expect(value).toEqual(new FeatureToggleModel(anyToggleResponse.id, anyToggleResponse.enabled, [anyFilter]));
+ });
+ });
+});
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts
new file mode 100644
index 000000000..953cbfe1b
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts
@@ -0,0 +1,40 @@
+import { Inject, Injectable, InjectionToken } from '@angular/core';
+import { AppConfigurationClient } from '@azure/app-configuration';
+import { from, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { AZURE_APP_CONFIGURATION_CONNECTION_STRING } from 'src/environments/environment';
+import { FeatureToggleConfiguration } from './feature-toggle-configuration';
+import { FeatureToggleModel } from './feature-toggle.model';
+import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
+
+
+const APP_CONFIGURATION_CLIENT = new InjectionToken('Azure configuration client', {
+ providedIn: 'root',
+ factory: () => new AppConfigurationClient(AZURE_APP_CONFIGURATION_CONNECTION_STRING)
+});
+@Injectable({
+ providedIn: 'root',
+
+})
+export class FeatureToggleProvider {
+ constructor(
+ @Inject(APP_CONFIGURATION_CLIENT)
+ private client: AppConfigurationClient,
+ private featureFilterProvider: FeatureFilterProvider
+ ) { }
+
+ public getFeatureToggle(toggleName: string, toggleLabel?: string): Observable {
+ return from(this.client.getConfigurationSetting({ key: `.appconfig.featureflag/${toggleName}`, label: toggleLabel })).pipe(
+ map(featureToggleResponse => JSON.parse(featureToggleResponse.value) as FeatureToggleConfiguration),
+ map(featureToggleConfiguration => {
+ const filters = featureToggleConfiguration.conditions.client_filters.map(filterConfiguration =>
+ this.featureFilterProvider.getFilterFromConfiguration(filterConfiguration));
+ return new FeatureToggleModel(
+ featureToggleConfiguration.id,
+ featureToggleConfiguration.enabled,
+ filters);
+ }
+ )
+ );
+ }
+}
diff --git a/src/app/modules/shared/feature-toggles/feature-toggle.model.ts b/src/app/modules/shared/feature-toggles/feature-toggle.model.ts
new file mode 100644
index 000000000..a33a74ff3
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/feature-toggle.model.ts
@@ -0,0 +1,9 @@
+import { FeatureFilterModel } from './filters/feature-filter.model';
+
+export class FeatureToggleModel {
+ constructor(
+ public readonly name: string,
+ public readonly enabled: boolean,
+ public readonly filters: FeatureFilterModel[]
+ ) { }
+}
diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts
new file mode 100644
index 000000000..1326c138f
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts
@@ -0,0 +1,4 @@
+export interface FeatureFilterConfiguration {
+ name: string;
+ parameters: any;
+}
diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts
new file mode 100644
index 000000000..fb1e21d4f
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts
@@ -0,0 +1,26 @@
+import { AzureAdB2CService } from 'src/app/modules/login/services/azure.ad.b2c.service';
+import { FeatureFilterConfiguration } from './feature-filter-configuration';
+import { FeatureFilterProvider } from './feature-filter-provider.service';
+import { FeatureFilterTypes } from './feature-filter-types';
+import { TargetingFeatureFilterModel } from './targeting/targeting-feature-filter.model';
+
+
+describe('FeatureFilterProvider', () => {
+ let fakeUserService: AzureAdB2CService;
+ let service: FeatureFilterProvider;
+ let featureFilterConfiguration: FeatureFilterConfiguration;
+
+ beforeEach(() => {
+ fakeUserService = new AzureAdB2CService();
+ spyOn(fakeUserService, 'getUserEmail').and.returnValue('any-user-email');
+ spyOn(fakeUserService, 'getUserGroup').and.returnValue('any-user-group');
+ service = new FeatureFilterProvider(fakeUserService);
+ featureFilterConfiguration = { name: FeatureFilterTypes.TARGETING, parameters: {} };
+ });
+
+ it('filter model type is created based on the filter configuration', () => {
+ const filter = service.getFilterFromConfiguration(featureFilterConfiguration);
+
+ expect(filter.constructor.name).toBe(TargetingFeatureFilterModel.name);
+ });
+});
diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts
new file mode 100644
index 000000000..9b4f57bf6
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts
@@ -0,0 +1,33 @@
+import { Injectable } from '@angular/core';
+import { AzureAdB2CService } from 'src/app/modules/login/services/azure.ad.b2c.service';
+import { FeatureFilterConfiguration } from './feature-filter-configuration';
+import { FeatureFilterTypes } from './feature-filter-types';
+import { FeatureFilterModel } from './feature-filter.model';
+import { TargetingFilterParameters } from './targeting/targeting-feature-filter-parameters';
+import { TargetingFeatureFilterModel } from './targeting/targeting-feature-filter.model';
+
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FeatureFilterProvider {
+
+ constructor(private userService: AzureAdB2CService) { }
+
+ getFilterFromConfiguration(featureFilterConfiguration: FeatureFilterConfiguration): FeatureFilterModel {
+ const featureName = featureFilterConfiguration.name;
+ switch (featureName) {
+ case FeatureFilterTypes.TARGETING: {
+ const appContext = {
+ username: this.userService.getUserEmail(),
+ group: this.userService.getUserGroup()
+ };
+ const filter = new TargetingFeatureFilterModel(featureFilterConfiguration.parameters as TargetingFilterParameters, appContext);
+ return filter;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+}
diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts
new file mode 100644
index 000000000..517c617b5
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts
@@ -0,0 +1,3 @@
+export enum FeatureFilterTypes {
+ TARGETING = 'Microsoft.Targeting'
+}
diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts
new file mode 100644
index 000000000..3a0037c89
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts
@@ -0,0 +1,14 @@
+
+export interface FeatureFilterModel {
+ name: string;
+ parameters: FeatureFilterParameters;
+ appContext: FeatureFilterAppContext;
+ evaluate(): boolean;
+
+}
+
+// tslint:disable-next-line:no-empty-interface
+export interface FeatureFilterParameters { }
+// tslint:disable-next-line:no-empty-interface
+export interface FeatureFilterAppContext { }
+
diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts
new file mode 100644
index 000000000..50f624ed1
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts
@@ -0,0 +1,9 @@
+import { FeatureFilterParameters } from '../feature-filter.model';
+
+export interface TargetingFilterParameters extends FeatureFilterParameters {
+ Audience: {
+ Users: string[];
+ Groups: string[];
+ };
+}
+
diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts
new file mode 100644
index 000000000..bba414199
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts
@@ -0,0 +1,38 @@
+import { TargetingFilterParameters } from './targeting-feature-filter-parameters';
+import { TargetingFeatureFilterModel } from './targeting-feature-filter.model';
+import { TargetingFilterAppContext } from './targeting-filter-app-context';
+
+describe('TargetingFeatureFilterModel', () => {
+
+ it('targeting feature is true when username matches', () => {
+ const aUsername = 'user-a';
+ const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: ['group-x'], Users: [aUsername] } };
+ const appContext: TargetingFilterAppContext = { group: 'group-y', username: aUsername };
+ const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext);
+
+ const filterEvaluation = targetingFeatureFilter.evaluate();
+
+ expect(filterEvaluation).toEqual(true);
+ });
+
+ it('targeting feature is true when group matches', () => {
+ const aGroup = 'group-a';
+ const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: [aGroup], Users: ['user-a'] } };
+ const appContext: TargetingFilterAppContext = { group: aGroup, username: 'user-b' };
+ const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext);
+
+ const filterEvaluation = targetingFeatureFilter.evaluate();
+
+ expect(filterEvaluation).toEqual(true);
+ });
+
+ it('targeting feature is false when neither group nor username match ', () => {
+ const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: ['group-a'], Users: ['user-a'] } };
+ const appContext: TargetingFilterAppContext = { group: 'group-b', username: 'user-b' };
+ const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext);
+
+ const filterEvaluation = targetingFeatureFilter.evaluate();
+
+ expect(filterEvaluation).toEqual(false);
+ });
+});
diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts
new file mode 100644
index 000000000..9f5ed5faf
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts
@@ -0,0 +1,19 @@
+import { FeatureFilterTypes } from '../feature-filter-types';
+import { FeatureFilterModel } from '../feature-filter.model';
+import { TargetingFilterParameters } from './targeting-feature-filter-parameters';
+import { TargetingFilterAppContext } from './targeting-filter-app-context';
+
+export class TargetingFeatureFilterModel implements FeatureFilterModel {
+
+ name = FeatureFilterTypes.TARGETING;
+ constructor(public readonly parameters: TargetingFilterParameters, public readonly appContext: TargetingFilterAppContext) {
+ }
+
+ evaluate(): boolean {
+ const userCoincidence = this.parameters.Audience.Users.includes(this.appContext.username);
+ const groupCoincidence = this.parameters.Audience.Groups.includes(this.appContext.group);
+ return userCoincidence || groupCoincidence;
+ }
+}
+
+
diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts
new file mode 100644
index 000000000..05d66efb8
--- /dev/null
+++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts
@@ -0,0 +1,9 @@
+import { FeatureFilterAppContext } from '../feature-filter.model';
+
+export interface TargetingFilterAppContext extends FeatureFilterAppContext {
+ username;
+ group;
+}
+
+
+