diff --git a/src/app/modules/login/login.component.spec.ts b/src/app/modules/login/login.component.spec.ts index ff90fa6a5..1e34c011e 100644 --- a/src/app/modules/login/login.component.spec.ts +++ b/src/app/modules/login/login.component.spec.ts @@ -2,14 +2,15 @@ import { waitForAsync, ComponentFixture, TestBed, inject } from '@angular/core/t import { RouterTestingModule } from '@angular/router/testing'; import { AzureAdB2CService } from '../../modules/login/services/azure.ad.b2c.service'; import { of } from 'rxjs'; - import { LoginComponent } from './login.component'; import { Router } from '@angular/router'; +import { FeatureToggleCookiesService } from '../shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service'; describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; let azureAdB2CService: AzureAdB2CService; + let featureToggleCookiesService: FeatureToggleCookiesService; const azureAdB2CServiceStub = { isLogin() { @@ -22,12 +23,19 @@ describe('LoginComponent', () => { } }; + const featureToggleCookiesServiceStub = { + setCookies() { + return null; + } + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ RouterTestingModule ], declarations: [ LoginComponent ], providers: [ - { providers: AzureAdB2CService, useValue: azureAdB2CServiceStub} + { providers: AzureAdB2CService, useValue: azureAdB2CServiceStub}, + { providers: FeatureToggleCookiesService, useValue: featureToggleCookiesServiceStub} ] }) .compileComponents(); @@ -38,15 +46,21 @@ describe('LoginComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); azureAdB2CService = TestBed.inject(AzureAdB2CService); - + featureToggleCookiesService = TestBed.inject(FeatureToggleCookiesService); }); - it('Service injected via inject(...) and TestBed.get(...) should be the same instance', + it('AzureAdB2CService injected via inject(...) and TestBed.get(...) should be the same instance', inject([AzureAdB2CService], (injectService: AzureAdB2CService) => { expect(injectService).toEqual(azureAdB2CService); }) ); + it('FeatureToggleCookiesService injected via inject(...) and TestBed.get(...) should be the same instance', + inject([FeatureToggleCookiesService], (injectService: FeatureToggleCookiesService) => { + expect(injectService).toEqual(featureToggleCookiesService); + }) + ); + it('should create login component', () => { expect(component).toBeTruthy(); }); @@ -55,9 +69,13 @@ describe('LoginComponent', () => { spyOn(azureAdB2CService, 'isLogin').and.returnValue(false); spyOn(azureAdB2CService, 'setCookies').and.returnValue(); spyOn(azureAdB2CService, 'signIn').and.returnValue(of(() => {})); + spyOn(featureToggleCookiesService, 'setCookies').and.returnValue(featureToggleCookiesService.setCookies()); + component.login(); + expect(azureAdB2CService.signIn).toHaveBeenCalled(); expect(azureAdB2CService.setCookies).toHaveBeenCalled(); + expect(featureToggleCookiesService.setCookies).toHaveBeenCalled(); })); it('should not sign-up or login with google if is already logged-in into the app', inject([Router], (router: Router) => { diff --git a/src/app/modules/login/login.component.ts b/src/app/modules/login/login.component.ts index c41251ab9..d136f2b90 100644 --- a/src/app/modules/login/login.component.ts +++ b/src/app/modules/login/login.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { AzureAdB2CService } from './services/azure.ad.b2c.service'; import { Router } from '@angular/router'; +import { FeatureToggleCookiesService } from '../shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service'; @Component({ selector: 'app-login', @@ -10,13 +11,18 @@ import { Router } from '@angular/router'; export class LoginComponent { - constructor(private azureAdB2CService: AzureAdB2CService, private router: Router) {} + constructor( + private azureAdB2CService: AzureAdB2CService, + private router: Router, + private featureToggleCookiesService: FeatureToggleCookiesService + ) {} login(): void { if (this.azureAdB2CService.isLogin()) { this.router.navigate(['']); } else { this.azureAdB2CService.signIn().subscribe(() => { + this.featureToggleCookiesService.setCookies(); this.azureAdB2CService.setCookies(); this.router.navigate(['']); }); 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 bc55dcbbe..3314e2edd 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 @@ -44,9 +44,12 @@ describe('AzureAdB2CService', () => { it('on logout should call msal logout and verify if user localStorage is removed', () => { spyOn(UserAgentApplication.prototype, 'logout').and.returnValue(); + spyOn(cookieService, 'deleteAll'); spyOn(localStorage, 'removeItem').withArgs('user'); + service.logout(); + expect(cookieService.deleteAll).toHaveBeenCalled(); expect(localStorage.removeItem).toHaveBeenCalledWith('user'); expect(UserAgentApplication.prototype.logout).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 96a9e6d66..c8a87ac31 100644 --- a/src/app/modules/login/services/azure.ad.b2c.service.ts +++ b/src/app/modules/login/services/azure.ad.b2c.service.ts @@ -32,8 +32,7 @@ export class AzureAdB2CService { } logout() { - this.cookieService.delete('msal.idtoken'); - this.cookieService.delete('msal.client.info'); + this.cookieService.deleteAll(); this.msal.logout(); localStorage.removeItem('user'); } diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.spec.ts new file mode 100644 index 000000000..47818843f --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.spec.ts @@ -0,0 +1,67 @@ +import { TestBed } from '@angular/core/testing'; +import { CookieService } from 'ngx-cookie-service'; +import { of } from 'rxjs'; +import { FeatureToggleGeneralService } from '../feature-toggle-general/feature-toggle-general.service'; +import { FeatureToggleModel } from '../feature-toggle.model'; +import { TargetingFeatureFilterModel } from '../filters/targeting/targeting-feature-filter.model'; +import { FeatureToggleCookiesService } from './feature-toggle-cookies.service'; + +describe('FeatureToggleCookiesService', () => { + let cookieService: CookieService; + let featureToggleGeneralService: FeatureToggleGeneralService; + let service: FeatureToggleCookiesService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [CookieService, FeatureToggleGeneralService] + }); + cookieService = TestBed.inject(CookieService); + featureToggleGeneralService = TestBed.inject(FeatureToggleGeneralService); + service = TestBed.inject(FeatureToggleCookiesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('not call CookieService.set() when call setCookies() and getActivated() return empty', () => { + const fakeAllFeatureaToggleWithFilters = []; + const spyOnSetCookie = spyOn(cookieService, 'set'); + spyOn(featureToggleGeneralService, 'getActivated').and.returnValue(of(fakeAllFeatureaToggleWithFilters)); + + service.setCookies(); + + expect(spyOnSetCookie).toHaveBeenCalledTimes(0); + }); + + it('Call 1 time CookieService.set() when call setCookies()', () => { + const anyMatchingFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['group-a'], Users: ['user-a'] } }, + { username: 'user-b', group: 'group-a' } + ); + const fakeAllFeatureaToggleWithFilters = [new FeatureToggleModel('any-other-id', true, [anyMatchingFilter])]; + const spyOnSetCookie = spyOn(cookieService, 'set'); + spyOn(featureToggleGeneralService, 'getActivated').and.returnValue(of(fakeAllFeatureaToggleWithFilters)); + + service.setCookies(); + + expect(spyOnSetCookie).toHaveBeenCalledTimes(1); + }); + + it('Call 2 times CookieService.set() when call setCookies()', () => { + const anyMatchingFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['group-a'], Users: ['user-a'] } }, + { username: 'user-b', group: 'group-a' } + ); + const fakeAllFeatureaToggleWithFilters = [ + new FeatureToggleModel('first-id', true, [anyMatchingFilter]), + new FeatureToggleModel('second-id', true, [anyMatchingFilter]) + ]; + const spyOnSetCookie = spyOn(cookieService, 'set'); + spyOn(featureToggleGeneralService, 'getActivated').and.returnValue(of(fakeAllFeatureaToggleWithFilters)); + + service.setCookies(); + + expect(spyOnSetCookie).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.ts new file mode 100644 index 000000000..3da27964d --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-cookies/feature-toggle-cookies.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { FeatureToggleGeneralService } from '../feature-toggle-general/feature-toggle-general.service'; +import { CookieService } from 'ngx-cookie-service'; + +@Injectable({ + providedIn: 'root' +}) +export class FeatureToggleCookiesService { + + constructor( + private cookieService: CookieService, + private featureToggleGeneralService: FeatureToggleGeneralService + ) { } + + setCookies(){ + this.featureToggleGeneralService.getActivated().subscribe( + (allFeaturToggle) => { + for (const featureToggle of allFeaturToggle){ + this.cookieService.set(featureToggle.name, `${featureToggle.enabled}`, 30); + } + } + ); + } +} diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.spec.ts index 4ef309d0b..8deac72e0 100644 --- a/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.spec.ts +++ b/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.spec.ts @@ -1,8 +1,10 @@ import { FeatureToggle } from './../../../../../environments/enum'; import { TestBed } from '@angular/core/testing'; -import { of, Observable } from 'rxjs'; +import { of } from 'rxjs'; import { FeatureManagerService } from '../feature-toggle-manager.service'; import { FeatureToggleGeneralService } from './feature-toggle-general.service'; +import { FeatureToggleModel } from '../feature-toggle.model'; +import { TargetingFeatureFilterModel } from '../filters/targeting/targeting-feature-filter.model'; describe('FeatureToggleGeneralService', () => { @@ -33,4 +35,28 @@ describe('FeatureToggleGeneralService', () => { }); }); }); + + it('getActivated return a FeatureToggleModel', () => { + const anyNotMatchingFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['a-group'], Users: ['user-a'] } }, + { username: 'user-b', group: 'b-group' } + ); + const fakeAllFeatureaToggleWithFilters = [new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter])]; + spyOn(featureManagerService, 'getAllFeatureToggleEnableForUser').and.returnValue(of(fakeAllFeatureaToggleWithFilters)); + + featureToggleGeneralService.getActivated().subscribe((featureToggleEnableForUser) => { + expect(featureToggleEnableForUser.length).toEqual(1); + expect(featureToggleEnableForUser).toEqual(fakeAllFeatureaToggleWithFilters); + }); + }); + + it('getActivated return empty', () => { + const fakeAllFeatureaToggleWithFilters = []; + spyOn(featureManagerService, 'getAllFeatureToggleEnableForUser').and.returnValue(of(fakeAllFeatureaToggleWithFilters)); + + featureToggleGeneralService.getActivated().subscribe((featureToggleEnableForUser) => { + expect(featureToggleEnableForUser.length).toEqual(0); + expect(featureToggleEnableForUser).toEqual(fakeAllFeatureaToggleWithFilters); + }); + }); }); diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.ts index ed2c22e7e..f73507733 100644 --- a/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.ts +++ b/src/app/modules/shared/feature-toggles/feature-toggle-general/feature-toggle-general.service.ts @@ -3,6 +3,7 @@ import { FeatureToggle } from './../../../../../environments/enum'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { FeatureManagerService } from '../feature-toggle-manager.service'; +import { FeatureToggleModel } from '../feature-toggle.model'; @Injectable({ providedIn: 'root', @@ -13,4 +14,8 @@ export class FeatureToggleGeneralService { isActivated(featureToggle: FeatureToggle): Observable { return this.featureManagerService.isToggleEnabledForUser(featureToggle); } + + getActivated(): Observable{ + return this.featureManagerService.getAllFeatureToggleEnableForUser(); + } } 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 index 6a47ca604..8730168ab 100644 --- 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 @@ -91,5 +91,48 @@ describe('FeatureToggleManager', () => { expect(value).toEqual(false); }); }); + + it('Get empty when getAllFeatureToggle() return empty', () => { + const fakeAllFeatureaToggleWithFilters = []; + spyOn(fakeFeatureToggleProvider, 'getAllFeatureToggle').and.returnValue(Promise.resolve(fakeAllFeatureaToggleWithFilters)); + + const response = service.getAllFeatureToggleEnableForUser(); + + response.subscribe((result) => { + expect(result.length).toEqual(0); + expect(result).toEqual([]); + }); + expect().nothing(); + }); + + it('Get empty when getAllFeatureToggle() return FeatureToggle without fakeuser', () => { + const fakeAllFeatureaToggleWithFilters = [new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter])]; + spyOn(fakeFeatureToggleProvider, 'getAllFeatureToggle').and.returnValue(Promise.resolve(fakeAllFeatureaToggleWithFilters)); + + const response = service.getAllFeatureToggleEnableForUser(); + + response.subscribe((result) => { + expect(result.length).toEqual(0); + expect(result).toEqual([]); + }); + expect().nothing(); + }); + + it('Get FeatureToggleModel[] when getAllFeatureToggle() return FeatureToggle with fakeuser', () => { + const fakeFeatureToggleModel: FeatureToggleModel = new FeatureToggleModel('good-other-id', false, [anyMatchingFilter]); + const fakeAllFeatureaToggleWithFilters = [ + new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter]), + fakeFeatureToggleModel + ]; + spyOn(fakeFeatureToggleProvider, 'getAllFeatureToggle').and.returnValue(Promise.resolve(fakeAllFeatureaToggleWithFilters)); + + const response = service.getAllFeatureToggleEnableForUser(); + + response.subscribe((result) => { + expect(result.length).toEqual(1); + expect(result[0]).toEqual(fakeFeatureToggleModel); + }); + expect().nothing(); + }); }); }); 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 index df1ab25da..68f4770fb 100644 --- a/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts +++ b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import { Observable, zip } from 'rxjs'; +import { from, Observable, zip } from 'rxjs'; import { map } from 'rxjs/operators'; import { FeatureToggleProvider } from './feature-toggle-provider.service'; - +import { FeatureToggleModel } from './feature-toggle.model'; @Injectable({ providedIn: 'root', @@ -12,9 +12,9 @@ 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) - ); + return this.featureToggleProvider + .getFeatureToggle(toggleName, toggleLabel) + .pipe(map((featureToggle) => featureToggle.enabled)); } public isToggleEnabledForUser(toggleName: string, toggleLabel?: string): Observable { @@ -35,4 +35,14 @@ export class FeatureManagerService { return result$; } + + public getAllFeatureToggleEnableForUser(): Observable { + return from(this.featureToggleProvider.getAllFeatureToggle()).pipe( + map((allFeatureToggle) => + allFeatureToggle.filter((featureToggle) => + featureToggle.filters.map((filter) => filter.evaluate()).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 index 5c6d9b5e4..0e177cf72 100644 --- 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 @@ -1,4 +1,4 @@ -import { AppConfigurationClient, GetConfigurationSettingResponse } from '@azure/app-configuration'; +import { AppConfigurationClient, ConfigurationSetting, GetConfigurationSettingResponse } from '@azure/app-configuration'; import { of } from 'rxjs'; import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; import { FeatureToggleConfiguration } from './feature-toggle-configuration'; @@ -14,7 +14,7 @@ describe('FeatureToggleProvider', () => { enabled: true, description: 'any description', conditions: { - client_filters: [{ name: 'any-name', parameters: {} }] + client_filters: [{ name: 'any-name', parameters: {} }], }, }; @@ -29,52 +29,128 @@ describe('FeatureToggleProvider', () => { bodyAsText: 'any-response', headers: null, parsedHeaders: null, - status: 200 + status: 200, }, statusCode: 200, - value: JSON.stringify(anyToggleResponse) + value: JSON.stringify(anyToggleResponse), }; - const anyFilter = new TargetingFeatureFilterModel( + const fakeFeatureFilterModelUserName = 'fakeuser@ioet.com'; + const fakeFeatureFilterModelGroup = 'fake-group'; + const fakeFeatureFilterModel = new TargetingFeatureFilterModel ( { Audience: { Groups: ['a-group'], Users: ['any-user'] } }, - { username: 'fakeuser@ioet.com', group: 'fake-group' } + { username: fakeFeatureFilterModelUserName, group: fakeFeatureFilterModelGroup } ); - let fakeConfigurationClient; + let fakeGetConfigurationSetting; - let fakeFeatureFilterProvider; - let getFilterConfigurationSpy; - let service; + let fakeAppConfigurationClient: any; + let fakeFeatureFilterProvider: FeatureFilterProvider; + let service: FeatureToggleProvider; 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); + fakeAppConfigurationClient = new AppConfigurationClient(fakeAppConfigurationConnectionString); + const fakeAzureAdB2CService = { + getUserEmail(){ + return fakeFeatureFilterModelUserName; + }, + getUserGroup(){ + return fakeFeatureFilterModelGroup; + } + }; + fakeFeatureFilterProvider = new FeatureFilterProvider(fakeAzureAdB2CService as AzureAdB2CService); + service = new FeatureToggleProvider(fakeAppConfigurationClient, fakeFeatureFilterProvider); + + fakeGetConfigurationSetting = spyOn(fakeAppConfigurationClient, 'getConfigurationSetting').and.callFake(() => + of(fakeResponse).toPromise() + ); }); it('toggles are read using azure configuration client', async () => { service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { - - expect(fakeGetConfigurationSetting).toHaveBeenCalledWith( - { key: `.appconfig.featureflag/${featureToggleKey}`, label: featureToggleLabel } - ); + expect(fakeGetConfigurationSetting).toHaveBeenCalledWith({ + key: `.appconfig.featureflag/${featureToggleKey}`, + label: featureToggleLabel, + }); }); }); it('filters are built using the filterProvider', async () => { + const getFilterConfigurationSpy = spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and.returnValue( + fakeFeatureFilterModel + ); + service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { expect(getFilterConfigurationSpy).toHaveBeenCalled(); }); }); it('toggle model is built', async () => { + spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and.returnValue( + fakeFeatureFilterModel + ); + service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { - expect(value).toEqual(new FeatureToggleModel(anyToggleResponse.id, anyToggleResponse.enabled, [anyFilter])); + expect(value).toEqual(new FeatureToggleModel(anyToggleResponse.id, anyToggleResponse.enabled, [fakeFeatureFilterModel])); + }); + }); + + it('Call listConfigurationSettings when we called getAllFeatureToggle()', () => { + const fakeKeyFilter = { keyFilter: '.appconfig.featureflag/*' }; + spyOn(fakeAppConfigurationClient, 'listConfigurationSettings'); + + service.getAllFeatureToggle(); + + expect(fakeAppConfigurationClient.listConfigurationSettings).toHaveBeenCalledWith(fakeKeyFilter); + }); + + it('Call listConfigurationSettings with bad keyFilter when we called getAllFeatureToggle()', () => { + const badFakeKeyFilter = { keyFilter: 'abc' }; + spyOn(fakeAppConfigurationClient, 'listConfigurationSettings').withArgs(badFakeKeyFilter); + + const response = service.getAllFeatureToggle(); + + response.then((arrayFeatureToggle) => { + expect(arrayFeatureToggle).toEqual([]); }); }); + + it('Get empty array when we called getAllFeatureToggle() and listConfigurationSettings returns an empty object', () => { + spyOn(fakeAppConfigurationClient, 'listConfigurationSettings').and.returnValue([]); + + const response = service.getAllFeatureToggle(); + + response.then((arrayFeatureToggle) => { + expect(arrayFeatureToggle).toEqual([]); + }); + }); + + it('Get empty Promise when we called getAllFeatureToggle() an raise error', () => { + spyOn(fakeAppConfigurationClient, 'listConfigurationSettings').and.returnValue(3); + + const response: Promise = service.getAllFeatureToggle(); + response.then((arrayFeatureToggle) => { + expect(arrayFeatureToggle).toEqual([]); + }); + }); + + it('Get array when we called getAllFeatureToggle() and listConfigurationSettings returns an object', () => { + const fakeConfigurationSetting: ConfigurationSetting = { + key: '.appconfig.featureflag/test', + isReadOnly: false, + value: + '{"id":"test","description":"Exponential growth in Time clock and Time entries, in the UI","enabled":true,"conditions":{"client_filters":[{"name":"Microsoft.Targeting","parameters":{"Audience":{"Users":["any-user"],"Groups":["a-group"]}}}]}}', + }; + const fakeFeatureToggleModel = new FeatureToggleModel('test', true, [fakeFeatureFilterModel]); + spyOn(fakeAppConfigurationClient, 'listConfigurationSettings').and.returnValue([fakeConfigurationSetting]); + spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and.returnValue(fakeFeatureFilterModel); + + const response = service.getAllFeatureToggle(); + + response.then((result) => { + expect(result.length).toEqual(1); + expect(result).toEqual([fakeFeatureToggleModel]); + }); + + }); }); 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 index 953cbfe1b..fb1401cd1 100644 --- a/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts +++ b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts @@ -7,34 +7,53 @@ 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) + 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( + 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); + }) + ); + } + + public async getAllFeatureToggle(): Promise { + const listFeatureToggleModel: FeatureToggleModel[] = []; + const allFeatureToggle = this.client.listConfigurationSettings({ keyFilter: '.appconfig.featureflag/*' }); + try { + for await (const featureToggle of allFeatureToggle) { + const featureToggleConfiguration = JSON.parse(featureToggle.value) as FeatureToggleConfiguration; + const filters = featureToggleConfiguration.conditions.client_filters.map((filterConfiguration) => { + return this.featureFilterProvider.getFilterFromConfiguration(filterConfiguration); + }); + const featureToggleModel = new FeatureToggleModel( featureToggleConfiguration.id, featureToggleConfiguration.enabled, - filters); + filters + ); + listFeatureToggleModel.push(featureToggleModel); } - ) - ); + return listFeatureToggleModel; + } catch (errorResponseAzure) { + return listFeatureToggleModel; + } } } diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts index 9410b6a31..4987e73c9 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts @@ -1,5 +1,5 @@ -import { Subscription, of, Observable } from 'rxjs'; -import { LoadActiveEntry, EntryActionTypes, UpdateEntry } from './../../store/entry.actions'; +import { Subscription, of } from 'rxjs'; +import { LoadActiveEntry, EntryActionTypes } from './../../store/entry.actions'; import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; @@ -15,7 +15,7 @@ import { formatDate } from '@angular/common'; import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker'; import * as moment from 'moment'; import { DATE_FORMAT_YEAR } from 'src/environments/environment'; -import { FeatureManagerService } from './../../../shared/feature-toggles/feature-toggle-manager.service'; +import { CookieService } from 'ngx-cookie-service'; import { FeatureToggleGeneralService } from './../../../shared/feature-toggles/feature-toggle-general/feature-toggle-general.service'; import { FeatureToggle } from 'src/environments/enum'; @@ -25,6 +25,7 @@ describe('EntryFieldsComponent', () => { let component: EntryFieldsComponent; let fixture: ComponentFixture; let store: MockStore; + let cookieService: CookieService; let mockTechnologySelector; let mockProjectsSelector; let entryForm; @@ -122,6 +123,7 @@ describe('EntryFieldsComponent', () => { mockTechnologySelector = store.overrideSelector(allTechnologies, state.technologies); mockProjectsSelector = store.overrideSelector(getCustomerProjects, state.projects); featureToggleGeneralService = TestBed.inject(FeatureToggleGeneralService); + cookieService = TestBed.inject(CookieService); })); beforeEach(() => { @@ -453,6 +455,84 @@ describe('EntryFieldsComponent', () => { }); }); + it('Set true in isCookieFeatureToggleActive when feature-toggle "feature-toggle-in-cookies" is enable for user', () => { + const expectedValue = true; + spyOn(featureToggleGeneralService, 'isActivated').and.returnValue(of(true)); + + component.ngOnInit(); + + expect(component.isCookieFeatureToggleActive).toEqual(expectedValue); + }); + + it('Set false in isCookieFeatureToggleActive when feature-toggle "feature-toggle-in-cookies" is not enable for user', () => { + const expectedValue = false; + spyOn(featureToggleGeneralService, 'isActivated').and.returnValue(of(false)); + + component.ngOnInit(); + + expect(component.isCookieFeatureToggleActive).toEqual(expectedValue); + }); + + + it('Call cookieService.get() when isCookieFeatureToggleActive is True', () => { + const expectedValue = true; + component.isCookieFeatureToggleActive = expectedValue; + spyOn(cookieService, 'get').and.returnValue(`${expectedValue}`); + + component.ngOnInit(); + + expect(cookieService.get).toHaveBeenCalledWith(FeatureToggle.UPDATE_ENTRIES); + }); + + it('Call featureToggleGeneralService.isActivated() when isCookieFeatureToggleActive is False', () => { + const expectedValue = false; + spyOn(featureToggleGeneralService, 'isActivated').and.returnValue(of(expectedValue)); + + component.ngOnInit(); + + expect(featureToggleGeneralService.isActivated).toHaveBeenCalledWith(FeatureToggle.UPDATE_ENTRIES); + }); + + it('Set True in isFeatureToggleActive when cookieService.get() return "true" and isCookieFeatureToggleActive is true', () => { + const expectedValue = true; + component.isCookieFeatureToggleActive = expectedValue; + spyOn(cookieService, 'get').and.returnValue(`${expectedValue}`); + + component.ngOnInit(); + + expect(component.isFeatureToggleActive).toEqual(expectedValue); + }); + + it('Set True in isFeatureToggleActive when cookieService.get() return "false" and isCookieFeatureToggleActive is true', () => { + const expectedValue = false; + component.isCookieFeatureToggleActive = !expectedValue; + spyOn(cookieService, 'get').and.returnValue(`${expectedValue}`); + + component.ngOnInit(); + + expect(component.isFeatureToggleActive).toEqual(expectedValue); + }); + + it('Set True in isFeatureToggleActive when featureToggleGeneralService.isActivated() return true', () => { + const expectedValue = true; + spyOn(featureToggleGeneralService, 'isActivated').and.callFake( + (featureToggle) => featureToggle === FeatureToggle.COOKIES ? of(false) : of(true) ); + + component.ngOnInit(); + + expect(featureToggleGeneralService.isActivated).toHaveBeenCalledWith(FeatureToggle.UPDATE_ENTRIES); + expect(component.isFeatureToggleActive).toEqual(expectedValue); + }); + + it('Set False in isFeatureToggleActive when featureToggleGeneralService.isActivated() return false and isCookieFeatureToggleActive is false', () => { + const expectedValue = false; + spyOn(featureToggleGeneralService, 'isActivated').and.returnValue(of(expectedValue)); + + component.ngOnInit(); + + expect(component.isFeatureToggleActive).toEqual(expectedValue); + }); + it('when FT "update-entries" disable for the user,the UpdateCurrentOrLastEntry function is called to update the entries', () => { spyOn(featureToggleGeneralService, 'isActivated').and.returnValue(of(false)); diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts index f3133463a..9e533d05d 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts @@ -20,6 +20,7 @@ import { DATE_FORMAT } from 'src/environments/environment'; import { Subscription, Observable } from 'rxjs'; import { FeatureManagerService } from './../../../shared/feature-toggles/feature-toggle-manager.service'; import { FeatureToggle } from './../../../../../environments/enum'; +import { CookieService } from 'ngx-cookie-service'; type Merged = TechnologyState & ProjectState & ActivityState; @@ -39,7 +40,7 @@ export class EntryFieldsComponent implements OnInit, OnDestroy { loadActivitiesSubscription: Subscription; loadActiveEntrySubscription: Subscription; actionSetDateSubscription: Subscription; - isEnableToggleSubscription: Subscription; + isCookieFeatureToggleActive: boolean; isFeatureToggleActive: boolean; constructor( @@ -48,6 +49,7 @@ export class EntryFieldsComponent implements OnInit, OnDestroy { private actionsSubject$: ActionsSubject, private toastrService: ToastrService, private featureToggleGeneralService: FeatureToggleGeneralService, + private cookiesService: CookieService ) { this.entryForm = this.formBuilder.group({ description: '', @@ -68,10 +70,18 @@ export class EntryFieldsComponent implements OnInit, OnDestroy { this.store.dispatch(new LoadActiveEntry()); }); - this.isEnableToggleSubscription = this.featureToggleGeneralService.isActivated(FeatureToggle.UPDATE_ENTRIES).subscribe((flag) => { - this.isFeatureToggleActive = flag; + this.featureToggleGeneralService.isActivated(FeatureToggle.COOKIES).subscribe((flag) => { + this.isCookieFeatureToggleActive = flag; }); + if (this.isCookieFeatureToggleActive){ + this.isFeatureToggleActive = this.cookiesService.get(FeatureToggle.UPDATE_ENTRIES) === 'true' ? true : false; + }else{ + this.featureToggleGeneralService.isActivated(FeatureToggle.UPDATE_ENTRIES).subscribe((flag) => { + this.isFeatureToggleActive = flag; + }); + } + this.loadActiveEntrySubscription = this.actionsSubject$ .pipe( filter( diff --git a/src/environments/enum.ts b/src/environments/enum.ts index 0dac4770b..232f6ff7e 100644 --- a/src/environments/enum.ts +++ b/src/environments/enum.ts @@ -1,4 +1,5 @@ export enum FeatureToggle { SWITCH_GROUP = 'switch-group', - UPDATE_ENTRIES = 'update-entries' + UPDATE_ENTRIES = 'update-entries', + COOKIES = 'feature-toggle-in-cookies' }