Skip to content

Commit 851d59f

Browse files
committed
add tests
1 parent 2f2710c commit 851d59f

File tree

3 files changed

+172
-2
lines changed

3 files changed

+172
-2
lines changed

src/ConfigurationClientManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ function buildConnectionString(endpoint, secret, id) {
273273
* @param {string} endpoint - The endpoint URL.
274274
* @returns {string} - The valid domain or an empty string if no valid domain is found.
275275
*/
276-
function getValidDomain(endpoint) {
276+
export function getValidDomain(endpoint) {
277277
try {
278278
const url = new URL(endpoint);
279279
const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel];
@@ -299,7 +299,7 @@ function getValidDomain(endpoint) {
299299
* @param {string} validDomain - The valid domain to check against.
300300
* @returns {boolean} - True if the host ends with the valid domain, false otherwise.
301301
*/
302-
function isValidEndpoint(host, validDomain) {
302+
export function isValidEndpoint(host, validDomain) {
303303
if (!validDomain) {
304304
return false;
305305
}

test/failover.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import * as chai from "chai";
5+
import * as chaiAsPromised from "chai-as-promised";
6+
chai.use(chaiAsPromised);
7+
const expect = chai.expect;
8+
import { load } from "./exportedApi";
9+
import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper";
10+
import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager";
11+
12+
const mockedKVs = [{
13+
key: "app.settings.fontColor",
14+
value: "red",
15+
}, {
16+
key: "app.settings.fontSize",
17+
value: "40",
18+
}].map(createMockedKeyValue);
19+
20+
const mockedFeatureFlags = [{
21+
key: "app.settings.fontColor",
22+
value: "red",
23+
}].map(createMockedKeyValue).concat([
24+
createMockedFeatureFlag("Beta", { enabled: true }),
25+
createMockedFeatureFlag("Alpha_1", { enabled: true }),
26+
createMockedFeatureFlag("Alpha_2", { enabled: false }),
27+
]);
28+
29+
describe("failover", function () {
30+
this.timeout(15000);
31+
32+
afterEach(() => {
33+
restoreMocks();
34+
});
35+
36+
it("should failover to replica and load key values from config store", async () => {
37+
const replicaDiscoveryEnabled = true;
38+
const isFailoverable = true;
39+
mockConfigurationManagerGetClients(isFailoverable);
40+
mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs);
41+
42+
const connectionString = createMockedConnectionString();
43+
const settings = await load(connectionString, {
44+
replicaDiscoveryEnabled: replicaDiscoveryEnabled
45+
});
46+
expect(settings).not.undefined;
47+
expect(settings.get("app.settings.fontColor")).eq("red");
48+
expect(settings.get("app.settings.fontSize")).eq("40");
49+
});
50+
51+
it("should failover to replica and load feature flags from config store", async () => {
52+
const replicaDiscoveryEnabled = true;
53+
const isFailoverable = true;
54+
mockConfigurationManagerGetClients(isFailoverable);
55+
mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedFeatureFlags);
56+
57+
const connectionString = createMockedConnectionString();
58+
const settings = await load(connectionString, {
59+
replicaDiscoveryEnabled: replicaDiscoveryEnabled,
60+
featureFlagOptions: {
61+
enabled: true,
62+
selectors: [{
63+
keyFilter: "*"
64+
}]
65+
}
66+
});
67+
expect(settings).not.undefined;
68+
expect(settings.get("feature_management")).not.undefined;
69+
expect(settings.get<any>("feature_management").feature_flags).not.undefined;
70+
});
71+
72+
it("should throw error when all clients failed", async () => {
73+
const isFailoverable = false;
74+
mockConfigurationManagerGetClients(isFailoverable);
75+
mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs);
76+
77+
const connectionString = createMockedConnectionString();
78+
return expect(load(connectionString)).eventually.rejectedWith("All app configuration clients failed to get settings.");
79+
});
80+
81+
it("should validate endpoint", () => {
82+
const fakeEndpoint = createMockedEndpoint("fake");
83+
const validDomain = getValidDomain(fakeEndpoint);
84+
85+
expect(isValidEndpoint("azure.azconfig.io", validDomain)).to.be.true;
86+
expect(isValidEndpoint("azure.privatelink.azconfig.io", validDomain)).to.be.true;
87+
expect(isValidEndpoint("azure-replica.azconfig.io", validDomain)).to.be.true;
88+
expect(isValidEndpoint("azure.badazconfig.io", validDomain)).to.be.false;
89+
expect(isValidEndpoint("azure.azconfigbad.io", validDomain)).to.be.false;
90+
expect(isValidEndpoint("azure.appconfig.azure.com", validDomain)).to.be.false;
91+
expect(isValidEndpoint("azure.azconfig.bad.io", validDomain)).to.be.false;
92+
93+
const fakeEndpoint2 = "https://foobar.appconfig.azure.com";
94+
const validDomain2 = getValidDomain(fakeEndpoint2);
95+
96+
expect(isValidEndpoint("azure.appconfig.azure.com", validDomain2)).to.be.true;
97+
expect(isValidEndpoint("azure.z1.appconfig.azure.com", validDomain2)).to.be.true;
98+
expect(isValidEndpoint("azure-replia.z1.appconfig.azure.com", validDomain2)).to.be.true; // Note: Typo "azure-replia"
99+
expect(isValidEndpoint("azure.privatelink.appconfig.azure.com", validDomain2)).to.be.true;
100+
expect(isValidEndpoint("azconfig.appconfig.azure.com", validDomain2)).to.be.true;
101+
expect(isValidEndpoint("azure.azconfig.io", validDomain2)).to.be.false;
102+
expect(isValidEndpoint("azure.badappconfig.azure.com", validDomain2)).to.be.false;
103+
expect(isValidEndpoint("azure.appconfigbad.azure.com", validDomain2)).to.be.false;
104+
105+
const fakeEndpoint3 = "https://foobar.azconfig-test.io";
106+
const validDomain3 = getValidDomain(fakeEndpoint3);
107+
108+
expect(isValidEndpoint("azure.azconfig-test.io", validDomain3)).to.be.false;
109+
expect(isValidEndpoint("azure.azconfig.io", validDomain3)).to.be.false;
110+
111+
const fakeEndpoint4 = "https://foobar.z1.appconfig-test.azure.com";
112+
const validDomain4 = getValidDomain(fakeEndpoint4);
113+
114+
expect(isValidEndpoint("foobar.z2.appconfig-test.azure.com", validDomain4)).to.be.false;
115+
expect(isValidEndpoint("foobar.appconfig-test.azure.com", validDomain4)).to.be.false;
116+
expect(isValidEndpoint("foobar.appconfig.azure.com", validDomain4)).to.be.false;
117+
});
118+
});

test/utils/testHelper.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,57 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList) {
131131
});
132132
}
133133

134+
function mockAppConfigurationClientListConfigurationSettingsWithFailure(...pages: ConfigurationSetting[][]) {
135+
const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
136+
137+
// Configure the stub to throw an error on the first call and return mockedKVs on the second call
138+
stub.onFirstCall().throws(new RestError("Internal Server Error", { statusCode: 500 }));
139+
stub.callsFake((listOptions) => {
140+
let kvs = _filterKVs(pages.flat(), listOptions);
141+
const mockIterator: AsyncIterableIterator<any> & { byPage(): AsyncIterableIterator<any> } = {
142+
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
143+
kvs = _filterKVs(pages.flat(), listOptions);
144+
return this;
145+
},
146+
next() {
147+
const value = kvs.shift();
148+
return Promise.resolve({ done: !value, value });
149+
},
150+
byPage(): AsyncIterableIterator<any> {
151+
let remainingPages;
152+
const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list
153+
return {
154+
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
155+
remainingPages = [...pages];
156+
return this;
157+
},
158+
next() {
159+
const pageItems = remainingPages.shift();
160+
const pageEtag = pageEtags?.shift();
161+
if (pageItems === undefined) {
162+
return Promise.resolve({ done: true, value: undefined });
163+
} else {
164+
const items = _filterKVs(pageItems ?? [], listOptions);
165+
const etag = _sha256(JSON.stringify(items));
166+
const statusCode = pageEtag === etag ? 304 : 200;
167+
return Promise.resolve({
168+
done: false,
169+
value: {
170+
items,
171+
etag,
172+
_response: { status: statusCode }
173+
}
174+
});
175+
}
176+
}
177+
};
178+
}
179+
};
180+
181+
return mockIterator as any;
182+
});
183+
}
184+
134185
// uriValueList: [["<secretUri>", "value"], ...]
135186
function mockSecretClientGetSecret(uriValueList: [string, string][]) {
136187
const dict = new Map();
@@ -220,6 +271,7 @@ export {
220271
sinon,
221272
mockAppConfigurationClientListConfigurationSettings,
222273
mockAppConfigurationClientGetConfigurationSetting,
274+
mockAppConfigurationClientListConfigurationSettingsWithFailure,
223275
mockConfigurationManagerGetClients,
224276
mockSecretClientGetSecret,
225277
restoreMocks,

0 commit comments

Comments
 (0)