Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: #60 Adding support for ring deployments
  • Loading branch information
Juan Gabriel Guzman committed Oct 23, 2020
commit 1d2e800c48e26b8a6e4a12900a25a697ef38e58a
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<router-outlet></router-outlet>
<router-outlet></router-outlet>
21 changes: 19 additions & 2 deletions src/app/modules/login/services/azure.ad.b2c.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
});
});
13 changes: 11 additions & 2 deletions src/app/modules/login/services/azure.ad.b2c.service.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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;
}
}
10 changes: 2 additions & 8 deletions src/app/modules/reports/pages/reports.component.ts
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FeatureFilterConfiguration } from './filters/feature-filter-configuration';

export interface FeatureToggleConfiguration {
id: string;
enabled: boolean;
description: string;
conditions: {
client_filters: FeatureFilterConfiguration[];
};
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
map(featureToggle => featureToggle.enabled)
);
}

public isToggleEnabledForUser(toggleName: string, toggleLabel?: string): Observable<boolean> {
return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
map(featureToggle => featureToggle.filters),
map(filters => filters.map(filter => filter.evaluate())),
map(filterEvaluations => filterEvaluations.includes(true))
);
}
}
Original file line number Diff line number Diff line change
@@ -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: '[email protected]', 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]));
});
});
});
Original file line number Diff line number Diff line change
@@ -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<AppConfigurationClient>('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<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);
}
)
);
}
}
Original file line number Diff line number Diff line change
@@ -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[]
) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface FeatureFilterConfiguration {
name: string;
parameters: any;
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading