Skip to content

Commit 4139457

Browse files
authored
Merge pull request #541 from ioet/adding-support-for-ring-deployments
feat: #60 Adding support for ring deployments
2 parents f52309e + 97875e1 commit 4139457

20 files changed

+455
-14
lines changed

package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<router-outlet></router-outlet>
1+
<router-outlet></router-outlet>

src/app/modules/login/services/azure.ad.b2c.service.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { TestBed, inject } from '@angular/core/testing';
1+
import { inject, TestBed } from '@angular/core/testing';
2+
import { Account, UserAgentApplication } from 'msal';
23
import { AzureAdB2CService } from './azure.ad.b2c.service';
3-
import { UserAgentApplication, Account } from 'msal';
4+
45

56
describe('AzureAdB2CService', () => {
67
let service: AzureAdB2CService;
@@ -130,4 +131,20 @@ describe('AzureAdB2CService', () => {
130131
expect(sessionStorage.getItem).toHaveBeenCalled();
131132
expect(resp).toEqual(token);
132133
});
134+
135+
it('should get email from UserAgentApplication', () => {
136+
spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account);
137+
138+
const name = service.getName();
139+
140+
expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled();
141+
});
142+
143+
it('should group from UserAgentApplication', () => {
144+
spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account);
145+
146+
const name = service.getUserGroup();
147+
148+
expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled();
149+
});
133150
});

src/app/modules/login/services/azure.ad.b2c.service.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Injectable } from '@angular/core';
2-
import { Observable, from } from 'rxjs';
3-
import { CLIENT_ID, AUTHORITY, SCOPES } from '../../../../environments/environment';
42
import { UserAgentApplication } from 'msal';
3+
import { from, Observable } from 'rxjs';
4+
5+
import { AUTHORITY, CLIENT_ID, SCOPES } from '../../../../environments/environment';
56

67
@Injectable({
78
providedIn: 'root',
@@ -55,4 +56,12 @@ export class AzureAdB2CService {
5556
getBearerToken(): string {
5657
return sessionStorage.getItem('msal.idtoken');
5758
}
59+
60+
getUserEmail(): string {
61+
return this.msal.getAccount().idToken?.emails[0];
62+
}
63+
64+
getUserGroup(): string {
65+
return this.msal.getAccount().idToken?.extension_role;
66+
}
5867
}
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component } from '@angular/core';
22

33
@Component({
44
selector: 'app-reports',
55
templateUrl: './reports.component.html',
66
styleUrls: ['./reports.component.scss']
77
})
8-
export class ReportsComponent implements OnInit {
9-
10-
constructor() { }
11-
12-
ngOnInit(): void {
13-
}
14-
8+
export class ReportsComponent {
159
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { FeatureFilterConfiguration } from './filters/feature-filter-configuration';
2+
3+
export interface FeatureToggleConfiguration {
4+
id: string;
5+
enabled: boolean;
6+
description: string;
7+
conditions: {
8+
client_filters: FeatureFilterConfiguration[];
9+
};
10+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { AppConfigurationClient } from '@azure/app-configuration';
2+
import { of } from 'rxjs';
3+
import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
4+
import { FeatureManagerService } from './feature-toggle-manager.service';
5+
import { FeatureToggleProvider } from './feature-toggle-provider.service';
6+
import { FeatureToggleModel } from './feature-toggle.model';
7+
import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
8+
import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model';
9+
10+
11+
describe('FeatureToggleManager', () => {
12+
const featureToggleKey = 'foo';
13+
const featureToggleLabel = 'dev';
14+
const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
15+
const aFeatureToggle = new FeatureToggleModel('any-id', true, []);
16+
let service: FeatureManagerService;
17+
let fakeFeatureToggleProvider;
18+
19+
describe('Features without filters', () => {
20+
beforeEach(() => {
21+
22+
fakeFeatureToggleProvider = new FeatureToggleProvider(
23+
new AppConfigurationClient(fakeAppConfigurationConnectionString),
24+
new FeatureFilterProvider(new AzureAdB2CService())
25+
);
26+
spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aFeatureToggle));
27+
service = new FeatureManagerService(fakeFeatureToggleProvider);
28+
});
29+
it('manager uses feature provider to build feature toggle model', async () => {
30+
service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => {
31+
32+
expect(fakeFeatureToggleProvider).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel);
33+
});
34+
});
35+
36+
it('manager extracts enabled attribute from feature toggle model', async () => {
37+
service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => {
38+
expect(value).toEqual(aFeatureToggle.enabled);
39+
});
40+
});
41+
});
42+
43+
44+
describe('Features with filters', () => {
45+
const anyMatchingFilter = new TargetingFeatureFilterModel(
46+
{ Audience: { Groups: ['group-a'], Users: ['user-a'] } },
47+
{ username: 'user-b', group: 'group-a' }
48+
);
49+
const anyNotMatchingFilter = new TargetingFeatureFilterModel(
50+
{ Audience: { Groups: ['a-group'], Users: ['user-a'] } },
51+
{ username: 'user-b', group: 'b-group' }
52+
);
53+
54+
let aToggleWithFilters;
55+
let getFeatureToggleSpy;
56+
57+
beforeEach(() => {
58+
aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyMatchingFilter]);
59+
fakeFeatureToggleProvider = new FeatureToggleProvider(
60+
new AppConfigurationClient(fakeAppConfigurationConnectionString),
61+
new FeatureFilterProvider(new AzureAdB2CService())
62+
);
63+
getFeatureToggleSpy = spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters));
64+
service = new FeatureManagerService(fakeFeatureToggleProvider);
65+
});
66+
67+
it('manager uses feature provider to build feature toggle model', async () => {
68+
service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
69+
expect(getFeatureToggleSpy).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel);
70+
});
71+
});
72+
73+
it('given a feature toggle with filters which match the verification, then the response is true', async () => {
74+
service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
75+
expect(value).toEqual(true);
76+
});
77+
});
78+
79+
it('given a feature toggle with filters which do not match the verification, then the response is false', async () => {
80+
81+
aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter]);
82+
fakeFeatureToggleProvider = new FeatureToggleProvider(
83+
new AppConfigurationClient(fakeAppConfigurationConnectionString),
84+
new FeatureFilterProvider(new AzureAdB2CService())
85+
);
86+
spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters));
87+
service = new FeatureManagerService(fakeFeatureToggleProvider);
88+
89+
service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => {
90+
expect(value).toEqual(false);
91+
});
92+
});
93+
});
94+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Injectable } from '@angular/core';
2+
import { Observable } from 'rxjs';
3+
import { map } from 'rxjs/operators';
4+
import { FeatureToggleProvider } from './feature-toggle-provider.service';
5+
6+
7+
@Injectable({
8+
providedIn: 'root',
9+
})
10+
export class FeatureManagerService {
11+
12+
constructor(private featureToggleProvider: FeatureToggleProvider) { }
13+
14+
public isToggleEnabled(toggleName: string, toggleLabel?: string): Observable<boolean> {
15+
return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
16+
map(featureToggle => featureToggle.enabled)
17+
);
18+
}
19+
20+
public isToggleEnabledForUser(toggleName: string, toggleLabel?: string): Observable<boolean> {
21+
return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe(
22+
map(featureToggle => featureToggle.filters),
23+
map(filters => filters.map(filter => filter.evaluate())),
24+
map(filterEvaluations => filterEvaluations.includes(true))
25+
);
26+
}
27+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { AppConfigurationClient, GetConfigurationSettingResponse } from '@azure/app-configuration';
2+
import { of } from 'rxjs';
3+
import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
4+
import { FeatureToggleConfiguration } from './feature-toggle-configuration';
5+
import { FeatureToggleProvider } from './feature-toggle-provider.service';
6+
import { FeatureToggleModel } from './feature-toggle.model';
7+
import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
8+
import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model';
9+
10+
11+
describe('FeatureToggleProvider', () => {
12+
const anyToggleResponse: FeatureToggleConfiguration = {
13+
id: '1',
14+
enabled: true,
15+
description: 'any description',
16+
conditions: {
17+
client_filters: [{ name: 'any-name', parameters: {} }]
18+
},
19+
};
20+
21+
const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
22+
const featureToggleKey = 'foo';
23+
const featureToggleLabel = 'dev';
24+
const fakeResponse: GetConfigurationSettingResponse = {
25+
isReadOnly: true,
26+
key: featureToggleKey,
27+
_response: {
28+
request: null,
29+
bodyAsText: 'any-response',
30+
headers: null,
31+
parsedHeaders: null,
32+
status: 200
33+
},
34+
statusCode: 200,
35+
value: JSON.stringify(anyToggleResponse)
36+
};
37+
const anyFilter = new TargetingFeatureFilterModel(
38+
{ Audience: { Groups: ['a-group'], Users: ['any-user'] } },
39+
{ username: '[email protected]', group: 'fake-group' }
40+
);
41+
let fakeConfigurationClient;
42+
let fakeGetConfigurationSetting;
43+
let fakeFeatureFilterProvider;
44+
let getFilterConfigurationSpy;
45+
let service;
46+
47+
48+
49+
beforeEach(() => {
50+
fakeConfigurationClient = new AppConfigurationClient(fakeAppConfigurationConnectionString);
51+
fakeGetConfigurationSetting = spyOn(fakeConfigurationClient, 'getConfigurationSetting').and.callFake(
52+
() => of(fakeResponse).toPromise());
53+
54+
fakeFeatureFilterProvider = new FeatureFilterProvider(new AzureAdB2CService());
55+
getFilterConfigurationSpy = spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and.
56+
returnValue(anyFilter);
57+
service = new FeatureToggleProvider(fakeConfigurationClient, fakeFeatureFilterProvider);
58+
});
59+
60+
it('toggles are read using azure configuration client', async () => {
61+
service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
62+
63+
expect(fakeGetConfigurationSetting).toHaveBeenCalledWith(
64+
{ key: `.appconfig.featureflag/${featureToggleKey}`, label: featureToggleLabel }
65+
);
66+
});
67+
});
68+
69+
it('filters are built using the filterProvider', async () => {
70+
service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
71+
expect(getFilterConfigurationSpy).toHaveBeenCalled();
72+
});
73+
});
74+
75+
it('toggle model is built', async () => {
76+
service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => {
77+
expect(value).toEqual(new FeatureToggleModel(anyToggleResponse.id, anyToggleResponse.enabled, [anyFilter]));
78+
});
79+
});
80+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Inject, Injectable, InjectionToken } from '@angular/core';
2+
import { AppConfigurationClient } from '@azure/app-configuration';
3+
import { from, Observable } from 'rxjs';
4+
import { map } from 'rxjs/operators';
5+
import { AZURE_APP_CONFIGURATION_CONNECTION_STRING } from 'src/environments/environment';
6+
import { FeatureToggleConfiguration } from './feature-toggle-configuration';
7+
import { FeatureToggleModel } from './feature-toggle.model';
8+
import { FeatureFilterProvider } from './filters/feature-filter-provider.service';
9+
10+
11+
const APP_CONFIGURATION_CLIENT = new InjectionToken<AppConfigurationClient>('Azure configuration client', {
12+
providedIn: 'root',
13+
factory: () => new AppConfigurationClient(AZURE_APP_CONFIGURATION_CONNECTION_STRING)
14+
});
15+
@Injectable({
16+
providedIn: 'root',
17+
18+
})
19+
export class FeatureToggleProvider {
20+
constructor(
21+
@Inject(APP_CONFIGURATION_CLIENT)
22+
private client: AppConfigurationClient,
23+
private featureFilterProvider: FeatureFilterProvider
24+
) { }
25+
26+
public getFeatureToggle(toggleName: string, toggleLabel?: string): Observable<FeatureToggleModel> {
27+
return from(this.client.getConfigurationSetting({ key: `.appconfig.featureflag/${toggleName}`, label: toggleLabel })).pipe(
28+
map(featureToggleResponse => JSON.parse(featureToggleResponse.value) as FeatureToggleConfiguration),
29+
map(featureToggleConfiguration => {
30+
const filters = featureToggleConfiguration.conditions.client_filters.map(filterConfiguration =>
31+
this.featureFilterProvider.getFilterFromConfiguration(filterConfiguration));
32+
return new FeatureToggleModel(
33+
featureToggleConfiguration.id,
34+
featureToggleConfiguration.enabled,
35+
filters);
36+
}
37+
)
38+
);
39+
}
40+
}

0 commit comments

Comments
 (0)