diff --git a/src/appConfigurationImpl.ts b/src/appConfigurationImpl.ts index 4b22e6b6..7f8fbdf0 100644 --- a/src/appConfigurationImpl.ts +++ b/src/appConfigurationImpl.ts @@ -56,7 +56,8 @@ import { AIConfigurationTracingOptions } from "./requestTracing/aiConfigurationT import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./configurationClientManager.js"; import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js"; -import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js"; +import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js"; +import { ErrorMessages } from "./common/errorMessages.js"; const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds @@ -151,10 +152,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } else { for (const setting of watchedSettings) { if (setting.key.includes("*") || setting.key.includes(",")) { - throw new ArgumentError("The characters '*' and ',' are not supported in key of watched settings."); + throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_KEY); } if (setting.label?.includes("*") || setting.label?.includes(",")) { - throw new ArgumentError("The characters '*' and ',' are not supported in label of watched settings."); + throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_LABEL); } this.#sentinels.push(setting); } @@ -163,7 +164,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // custom refresh interval if (refreshIntervalInMs !== undefined) { if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) { - throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`); + throw new RangeError(ErrorMessages.INVALID_REFRESH_INTERVAL); } this.#kvRefreshInterval = refreshIntervalInMs; } @@ -182,7 +183,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // custom refresh interval if (refreshIntervalInMs !== undefined) { if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) { - throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`); + throw new RangeError(ErrorMessages.INVALID_FEATURE_FLAG_REFRESH_INTERVAL); } this.#ffRefreshInterval = refreshIntervalInMs; } @@ -195,7 +196,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const { secretRefreshIntervalInMs } = options.keyVaultOptions; if (secretRefreshIntervalInMs !== undefined) { if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) { - throw new RangeError(`The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`); + throw new RangeError(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL); } this.#secretRefreshEnabled = true; this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs); @@ -272,7 +273,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { new Promise((_, reject) => { timeoutId = setTimeout(() => { abortController.abort(); // abort the initialization promise - reject(new Error("Load operation timed out.")); + reject(new Error(ErrorMessages.LOAD_OPERATION_TIMEOUT)); }, startupTimeout); }) @@ -287,7 +288,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { await new Promise(resolve => setTimeout(resolve, MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed)); } } - throw new Error("Failed to load.", { cause: error }); + throw new Error(ErrorMessages.LOAD_OPERATION_FAILED, { cause: error }); } finally { clearTimeout(timeoutId); // cancel the timeout promise } @@ -341,7 +342,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { */ async refresh(): Promise { if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) { - throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets."); + throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED); } if (this.#refreshInProgress) { @@ -360,7 +361,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { */ onRefresh(listener: () => any, thisArg?: any): Disposable { if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) { - throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets."); + throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED); } const boundedListener = listener.bind(thisArg); @@ -840,7 +841,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } this.#clientManager.refreshClients(); - throw new Error("All fallback clients failed to get configuration settings."); + throw new Error(ErrorMessages.ALL_FALLBACK_CLIENTS_FAILED); } async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise { @@ -916,7 +917,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { async #parseFeatureFlag(setting: ConfigurationSetting): Promise { const rawFlag = setting.value; if (rawFlag === undefined) { - throw new ArgumentError("The value of configuration setting cannot be undefined."); + throw new ArgumentError(ErrorMessages.CONFIGURATION_SETTING_VALUE_UNDEFINED); } const featureFlag = JSON.parse(rawFlag); @@ -983,17 +984,17 @@ function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector const selector = { ...selectorCandidate }; if (selector.snapshotName) { if (selector.keyFilter || selector.labelFilter || selector.tagFilters) { - throw new ArgumentError("Key, label or tag filters should not be specified while selecting a snapshot."); + throw new ArgumentError(ErrorMessages.INVALID_SNAPSHOT_SELECTOR); } } else { if (!selector.keyFilter) { - throw new ArgumentError("Key filter cannot be null or empty."); + throw new ArgumentError(ErrorMessages.INVALID_KEY_FILTER); } if (!selector.labelFilter) { selector.labelFilter = LabelFilter.Null; } if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) { - throw new ArgumentError("The characters '*' and ',' are not supported in label filters."); + throw new ArgumentError(ErrorMessages.INVALID_LABEL_FILTER); } if (selector.tagFilters) { validateTagFilters(selector.tagFilters); @@ -1045,7 +1046,7 @@ function validateTagFilters(tagFilters: string[]): void { for (const tagFilter of tagFilters) { const res = tagFilter.split("="); if (res[0] === "" || res.length !== 2) { - throw new Error(`Invalid tag filter: ${tagFilter}. Tag filter must follow the format "tagName=tagValue".`); + throw new Error(`Invalid tag filter: ${tagFilter}. ${ErrorMessages.INVALID_TAG_FILTER}.`); } } } diff --git a/src/common/errorMessages.ts b/src/common/errorMessages.ts new file mode 100644 index 00000000..e1f9f658 --- /dev/null +++ b/src/common/errorMessages.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { MIN_REFRESH_INTERVAL_IN_MS } from "../refresh/refreshOptions.js"; +import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "../keyvault/keyVaultOptions.js"; + +export const enum ErrorMessages { + INVALID_WATCHED_SETTINGS_KEY = "The characters '*' and ',' are not supported in key of watched settings.", + INVALID_WATCHED_SETTINGS_LABEL = "The characters '*' and ',' are not supported in label of watched settings.", + INVALID_REFRESH_INTERVAL = `The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`, + INVALID_FEATURE_FLAG_REFRESH_INTERVAL = `The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`, + INVALID_SECRET_REFRESH_INTERVAL = `The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`, + LOAD_OPERATION_TIMEOUT = "The load operation timed out.", + LOAD_OPERATION_FAILED = "The load operation failed.", + REFRESH_NOT_ENABLED = "Refresh is not enabled for key-values, feature flags or Key Vault secrets.", + ALL_FALLBACK_CLIENTS_FAILED = "All fallback clients failed to get configuration settings.", + CONFIGURATION_SETTING_VALUE_UNDEFINED = "The value of configuration setting cannot be undefined.", + INVALID_SNAPSHOT_SELECTOR = "Key, label or tag filters should not be specified while selecting a snapshot.", + INVALID_KEY_FILTER = "Key filter cannot be null or empty.", + INVALID_LABEL_FILTER = "The characters '*' and ',' are not supported in label filters.", + INVALID_TAG_FILTER = "Tag filter must follow the format 'tagName=tagValue'", + CONNECTION_STRING_OR_ENDPOINT_MISSED = "A connection string or an endpoint with credential must be specified to create a client.", +} + +export const enum KeyVaultReferenceErrorMessages { + KEY_VAULT_OPTIONS_UNDEFINED = "Failed to process the Key Vault reference because Key Vault options are not configured.", + KEY_VAULT_REFERENCE_UNRESOLVABLE = "Failed to resolve the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret." +} diff --git a/src/common/error.ts b/src/common/errors.ts similarity index 100% rename from src/common/error.ts rename to src/common/errors.ts diff --git a/src/configurationClientManager.ts b/src/configurationClientManager.ts index f74303df..7249eda7 100644 --- a/src/configurationClientManager.ts +++ b/src/configurationClientManager.ts @@ -8,7 +8,8 @@ import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js"; import { isBrowser, isWebWorker } from "./requestTracing/utils.js"; import * as RequestTracing from "./requestTracing/constants.js"; import { shuffleList, instanceOfTokenCredential } from "./common/utils.js"; -import { ArgumentError } from "./common/error.js"; +import { ArgumentError } from "./common/errors.js"; +import { ErrorMessages } from "./common/errorMessages.js"; // Configuration client retry options const CLIENT_MAX_RETRIES = 2; @@ -80,7 +81,7 @@ export class ConfigurationClientManager { this.#credential = credential; staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions); } else { - throw new ArgumentError("A connection string or an endpoint with credential must be specified to create a client."); + throw new ArgumentError(ErrorMessages.CONNECTION_STRING_OR_ENDPOINT_MISSED); } this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)]; diff --git a/src/keyvault/keyVaultKeyValueAdapter.ts b/src/keyvault/keyVaultKeyValueAdapter.ts index cd41de1d..e16fbd25 100644 --- a/src/keyvault/keyVaultKeyValueAdapter.ts +++ b/src/keyvault/keyVaultKeyValueAdapter.ts @@ -6,7 +6,8 @@ import { IKeyValueAdapter } from "../keyValueAdapter.js"; import { AzureKeyVaultSecretProvider } from "./keyVaultSecretProvider.js"; import { KeyVaultOptions } from "./keyVaultOptions.js"; import { RefreshTimer } from "../refresh/refreshTimer.js"; -import { ArgumentError, KeyVaultReferenceError } from "../common/error.js"; +import { ArgumentError, KeyVaultReferenceError } from "../common/errors.js"; +import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js"; import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets"; import { isRestError } from "@azure/core-rest-pipeline"; import { AuthenticationError } from "@azure/identity"; @@ -26,7 +27,7 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter { async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> { if (!this.#keyVaultOptions) { - throw new ArgumentError("Failed to process the Key Vault reference because Key Vault options are not configured."); + throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED); } let secretIdentifier: KeyVaultSecretIdentifier; try { diff --git a/src/keyvault/keyVaultSecretProvider.ts b/src/keyvault/keyVaultSecretProvider.ts index d9c49e5d..47b21812 100644 --- a/src/keyvault/keyVaultSecretProvider.ts +++ b/src/keyvault/keyVaultSecretProvider.ts @@ -3,8 +3,9 @@ import { KeyVaultOptions } from "./keyVaultOptions.js"; import { RefreshTimer } from "../refresh/refreshTimer.js"; -import { ArgumentError } from "../common/error.js"; +import { ArgumentError } from "../common/errors.js"; import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets"; +import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js"; export class AzureKeyVaultSecretProvider { #keyVaultOptions: KeyVaultOptions | undefined; @@ -51,7 +52,7 @@ export class AzureKeyVaultSecretProvider { async #getSecretValueFromKeyVault(secretIdentifier: KeyVaultSecretIdentifier): Promise { if (!this.#keyVaultOptions) { - throw new ArgumentError("Failed to get secret value. The keyVaultOptions is not configured."); + throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED); } const { name: secretName, vaultUrl, sourceId, version } = secretIdentifier; // precedence: secret clients > custom secret resolver @@ -64,7 +65,7 @@ export class AzureKeyVaultSecretProvider { return await this.#keyVaultOptions.secretResolver(new URL(sourceId)); } // When code reaches here, it means that the key vault reference cannot be resolved in all possible ways. - throw new ArgumentError("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret."); + throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE); } #getSecretClient(vaultUrl: URL): SecretClient | undefined { diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index a48d633e..73537fe5 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -8,6 +8,7 @@ const expect = chai.expect; import { load } from "./exportedApi.js"; import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference, sleepInMs } from "./utils/testHelper.js"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; +import { ErrorMessages, KeyVaultReferenceErrorMessages } from "../src/common/errorMessages.js"; const mockedData = [ // key, secretUri, value @@ -43,8 +44,8 @@ describe("key vault reference", function () { try { await load(createMockedConnectionString()); } catch (error) { - expect(error.message).eq("Failed to load."); - expect(error.cause.message).eq("Failed to process the Key Vault reference because Key Vault options are not configured."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); + expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED); return; } // we should never reach here, load should throw an error @@ -106,8 +107,8 @@ describe("key vault reference", function () { } }); } catch (error) { - expect(error.message).eq("Failed to load."); - expect(error.cause.message).eq("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); + expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE); return; } // we should never reach here, load should throw an error @@ -167,7 +168,7 @@ describe("key vault secret refresh", function () { secretRefreshIntervalInMs: 59999 // less than 60_000 milliseconds } }); - return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith("The Key Vault secret refresh interval cannot be less than 60000 milliseconds."); + return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL); }); it("should reload key vault secret when there is no change to key-values", async () => { diff --git a/test/load.test.ts b/test/load.test.ts index 244782e9..79e29377 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -7,6 +7,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi.js"; import { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js"; +import { ErrorMessages } from "../src/common/errorMessages.js"; const mockedKVs = [{ key: "app.settings.fontColor", @@ -164,7 +165,7 @@ describe("load", function () { snapshotName: "Test", labelFilter: "\0" }] - })).eventually.rejectedWith("Key, label or tag filters should not be specified while selecting a snapshot."); + })).eventually.rejectedWith(ErrorMessages.INVALID_SNAPSHOT_SELECTOR); }); it("should not include feature flags directly in the settings", async () => { @@ -359,7 +360,7 @@ describe("load", function () { labelFilter: "*" }] }); - return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters."); + return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER); }); it("should not support , in label filters", async () => { @@ -370,7 +371,7 @@ describe("load", function () { labelFilter: "labelA,labelB" }] }); - return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters."); + return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER); }); it("should throw exception when there is any invalid tag filter", async () => { @@ -381,7 +382,7 @@ describe("load", function () { tagFilters: ["emptyTag"] }] }); - return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith("Tag filter must follow the format \"tagName=tagValue\""); + return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_TAG_FILTER); }); it("should throw exception when too many tag filters are provided", async () => { @@ -404,7 +405,7 @@ describe("load", function () { }] }); } catch (error) { - expect(error.message).eq("Failed to load."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); expect(error.cause.message).eq("Invalid request parameter 'tags'. Maximum number of tag filters is 5."); return; } diff --git a/test/startup.test.ts b/test/startup.test.ts index 51b46a3a..7b56243c 100644 --- a/test/startup.test.ts +++ b/test/startup.test.ts @@ -7,6 +7,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; import { MAX_TIME_OUT, createMockedConnectionString, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; +import { ErrorMessages } from "../src/common/errorMessages.js"; describe("startup", function () { this.timeout(MAX_TIME_OUT); @@ -50,8 +51,8 @@ describe("startup", function () { } }); } catch (error) { - expect(error.message).eq("Failed to load."); - expect(error.cause.message).eq("Load operation timed out."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); + expect(error.cause.message).eq(ErrorMessages.LOAD_OPERATION_TIMEOUT); expect(attempt).eq(1); return; } @@ -76,7 +77,7 @@ describe("startup", function () { } }); } catch (error) { - expect(error.message).eq("Failed to load."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); expect(error.cause.message).eq("Non-retriable Test Error"); expect(attempt).eq(1); return; @@ -102,7 +103,7 @@ describe("startup", function () { } }); } catch (error) { - expect(error.message).eq("Failed to load."); + expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED); expect(error.cause.message).eq("Non-retriable Test Error"); expect(attempt).eq(1); return;