diff --git a/.eslintrc b/.eslintrc index 754a9f38..8b96047e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,5 +40,6 @@ "avoidEscape": true } ], - }, + "@typescript-eslint/no-explicit-any": "off" + } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index d946d028..1ccd7b97 100644 --- a/.gitignore +++ b/.gitignore @@ -400,6 +400,7 @@ FodyWeavers.xsd # bundled folder dist/ dist-esm/ +out/ types/ # dotenv @@ -409,4 +410,4 @@ types/ *.tgz # examples -examples/package-lock.json \ No newline at end of file +examples/package-lock.json diff --git a/package-lock.json b/package-lock.json index ada9f0d9..9987ca56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,10 @@ }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.2", + "@types/mocha": "^10.0.4", "@types/node": "^20.5.7", + "@types/sinon": "^17.0.1", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", "chai": "^4.3.7", @@ -832,6 +835,12 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz", + "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.8.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", @@ -847,6 +856,27 @@ "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", @@ -4152,6 +4182,12 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "@types/mocha": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz", + "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==", + "dev": true + }, "@types/node": { "version": "20.8.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", @@ -4167,6 +4203,27 @@ "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, + "@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", diff --git a/package.json b/package.json index b354ca12..7f7ef05e 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,15 @@ "README.md" ], "scripts": { - "build": "npm run clean && npm run build-cjs && npm run build-esm", + "build": "npm run clean && npm run build-cjs && npm run build-esm && npm run build-test", "build-cjs": "rollup --config", - "build-esm": "tsc -p ./", - "clean": "rimraf dist dist-esm types", + "build-esm": "tsc -p ./tsconfig.json", + "build-test": "tsc -p ./tsconfig.test.json", + "clean": "rimraf dist dist-esm out types", "dev": "rollup --config --watch", "lint": "eslint src/ test/", "fix-lint": "eslint src/ test/ --fix", - "test": "mocha --timeout 10000" + "test": "mocha out/test/*.test.{js,cjs,mjs} --timeout 10000" }, "repository": { "type": "git", @@ -33,7 +34,10 @@ "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^11.1.2", + "@types/mocha": "^10.0.4", "@types/node": "^20.5.7", + "@types/sinon": "^17.0.1", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", "chai": "^4.3.7", diff --git a/src/AzureAppConfiguration.ts b/src/AzureAppConfiguration.ts index cd4cdc2a..a4af8746 100644 --- a/src/AzureAppConfiguration.ts +++ b/src/AzureAppConfiguration.ts @@ -3,4 +3,4 @@ export type AzureAppConfiguration = { // methods for advanced features, e.g. refresh() -} & ReadonlyMap; +} & ReadonlyMap; diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index a9331162..00000000 --- a/test/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/no-var-requires": "off" - } -} \ No newline at end of file diff --git a/test/clientOptions.test.js b/test/clientOptions.test.ts similarity index 86% rename from test/clientOptions.test.js rename to test/clientOptions.test.ts index ebd615b2..c5496fb9 100644 --- a/test/clientOptions.test.js +++ b/test/clientOptions.test.ts @@ -1,15 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -const { load } = require("../dist/index"); -const { createMockedConnectionString } = require("./utils/testHelper"); -const nock = require("nock"); +import { load } from "./exportedApi"; +import { createMockedConnectionString } from "./utils/testHelper"; +import * as nock from "nock"; class HttpRequestCountPolicy { + count: number; + name: string; + constructor() { this.count = 0; this.name = "HttpRequestCountPolicy"; @@ -58,7 +61,7 @@ describe("custom client options", function () { it("should override default retry options", async () => { const countPolicy = new HttpRequestCountPolicy(); - const loadWithMaxRetries = (maxRetries) => { + const loadWithMaxRetries = (maxRetries: number) => { return load(createMockedConnectionString(fakeEndpoint), { clientOptions: { additionalPolicies: [{ diff --git a/test/exportedApi.ts b/test/exportedApi.ts new file mode 100644 index 00000000..7e6feba1 --- /dev/null +++ b/test/exportedApi.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { load } from "../src"; \ No newline at end of file diff --git a/test/json.test.js b/test/json.test.ts similarity index 90% rename from test/json.test.js rename to test/json.test.ts index 24e18de5..4818e9b2 100644 --- a/test/json.test.js +++ b/test/json.test.ts @@ -1,18 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -const { load } = require("../dist/index"); -const { - mockAppConfigurationClientListConfigurationSettings, - restoreMocks, - createMockedConnectionString, - createMockedKeyVaultReference, - createMockedJsonKeyValue -} = require("./utils/testHelper"); +import { load } from "./exportedApi"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue } from "./utils/testHelper"; const jsonKeyValue = createMockedJsonKeyValue("json.settings.logging", '{"Test":{"Level":"Debug"},"Prod":{"Level":"Warning"}}'); const keyVaultKeyValue = createMockedKeyVaultReference("TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName"); diff --git a/test/keyvault.test.js b/test/keyvault.test.ts similarity index 84% rename from test/keyvault.test.js rename to test/keyvault.test.ts index 02d676d7..c5ac332e 100644 --- a/test/keyvault.test.js +++ b/test/keyvault.test.ts @@ -1,16 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -const { load } = require("../dist/index"); -const { sinon, - createMockedConnectionString, - createMockedTokenCredential, - mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } = require("./utils/testHelper"); -const { SecretClient } = require("@azure/keyvault-secrets"); +import { load } from "./exportedApi"; +import { sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper"; +import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; const mockedData = [ // key, secretUri, value @@ -72,9 +69,9 @@ describe("key vault reference", function () { // mock specific behavior per secret client const client1 = new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()); - sinon.stub(client1, "getSecret").returns({ value: "SecretValueViaClient1" }); + sinon.stub(client1, "getSecret").returns(Promise.resolve({value: "SecretValueViaClient1" } as KeyVaultSecret)); const client2 = new SecretClient("https://fake-vault-name2.vault.azure.net", createMockedTokenCredential()); - sinon.stub(client2, "getSecret").returns({ value: "SecretValueViaClient2" }); + sinon.stub(client2, "getSecret").returns(Promise.resolve({value: "SecretValueViaClient2" } as KeyVaultSecret)); const settings = await load(createMockedConnectionString(), { keyVaultOptions: { secretClients: [ diff --git a/test/load.test.js b/test/load.test.ts similarity index 91% rename from test/load.test.js rename to test/load.test.ts index 36d9d8bf..3e3d4c10 100644 --- a/test/load.test.js +++ b/test/load.test.ts @@ -1,19 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -const { load } = require("../dist/index"); -const { - mockAppConfigurationClientListConfigurationSettings, - restoreMocks, - createMockedConnectionString, - createMockedEnpoint, - createMockedTokenCredential, - createMockedKeyValue, -} = require("./utils/testHelper"); +import { load } from "./exportedApi"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEnpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper"; const mockedKVs = [{ key: "app.settings.fontColor", diff --git a/test/requestTracing.test.js b/test/requestTracing.test.ts similarity index 86% rename from test/requestTracing.test.js rename to test/requestTracing.test.ts index 533cd7a3..aaedae20 100644 --- a/test/requestTracing.test.js +++ b/test/requestTracing.test.ts @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -const { load } = require("../dist/index"); -const { createMockedConnectionString, createMockedTokenCredential } = require("./utils/testHelper"); - +import { createMockedConnectionString, createMockedTokenCredential } from "./utils/testHelper"; +import { load } from "./exportedApi"; class HttpRequestHeadersPolicy { + headers: any; + name: string; + constructor() { this.headers = {}; this.name = "HttpRequestHeadersPolicy"; @@ -22,13 +24,14 @@ class HttpRequestHeadersPolicy { describe("request tracing", function () { const fakeEndpoint = "https://127.0.0.1"; // sufficient to test the request it sends out const headerPolicy = new HttpRequestHeadersPolicy(); + const position: "perCall" | "perRetry" = "perCall"; const clientOptions = { retryOptions: { maxRetries: 0 // save time }, additionalPolicies: [{ policy: headerPolicy, - position: "perCall" + position }] }; @@ -43,7 +46,7 @@ describe("request tracing", function () { await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; - expect(headerPolicy.headers.get("User-Agent")).satisfy(ua => ua.startsWith("javascript-appconfiguration-provider")); + expect(headerPolicy.headers.get("User-Agent")).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider")); }); it("should have request type in correlation-context header", async () => { @@ -99,7 +102,7 @@ describe("request tracing", function () { delete process.env.WEBSITE_SITE_NAME; }); - it("should disable request tracing when AZURE_APP_CONFIGURATION_TRACING_DISABLED is true", async() => { + it("should disable request tracing when AZURE_APP_CONFIGURATION_TRACING_DISABLED is true", async () => { for (const indicator of ["TRUE", "true"]) { process.env.AZURE_APP_CONFIGURATION_TRACING_DISABLED = indicator; try { diff --git a/test/utils/testHelper.js b/test/utils/testHelper.ts similarity index 68% rename from test/utils/testHelper.js rename to test/utils/testHelper.ts index b58aa859..6db4bf62 100644 --- a/test/utils/testHelper.js +++ b/test/utils/testHelper.ts @@ -1,47 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -const sinon = require("sinon"); -const { AppConfigurationClient } = require("@azure/app-configuration"); -const { ClientSecretCredential } = require("@azure/identity"); -const { SecretClient } = require("@azure/keyvault-secrets"); -const uuid = require("uuid"); +import * as sinon from "sinon"; +import { AppConfigurationClient } from "@azure/app-configuration"; +import { ClientSecretCredential } from "@azure/identity"; +import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; +import * as uuid from "uuid"; -const TEST_CLIENT_ID = "62e76eb5-218e-4f90-8261-000000000000"; -const TEST_TENANT_ID = "72f988bf-86f1-41af-91ab-000000000000"; -const TEST_CLIENT_SECRET = "Q158Q~2JtUwVbuq0Mzm9ocH2umTB000000000000"; +const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000"; +const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000"; +const TEST_CLIENT_SECRET = "0000000000000000000000000000000000000000"; -function mockAppConfigurationClientListConfigurationSettings(kvList) { - function* testKvSetGnerator(kvs) { +function mockAppConfigurationClientListConfigurationSettings(kvList: any[]) { + function* testKvSetGnerator(kvs: any[]) { yield* kvs; } sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => { - const keyFilter = listOptions.keyFilter ?? "*"; - const labelFilter = listOptions.labelFilter ?? "*"; + const keyFilter = listOptions?.keyFilter ?? "*"; + const labelFilter = listOptions?.labelFilter ?? "*"; const kvs = kvList.filter(kv => { const keyMatched = keyFilter.endsWith("*") ? kv.key.startsWith(keyFilter.slice(0, keyFilter.length - 1)) : kv.key === keyFilter; const labelMatched = labelFilter.endsWith("*") ? kv.label.startsWith(labelFilter.slice(0, labelFilter.length - 1)) : (labelFilter === "\0" ? kv.label === null : kv.label === labelFilter); // '\0' in labelFilter, null in config setting. return keyMatched && labelMatched; }) - return testKvSetGnerator(kvs); + return testKvSetGnerator(kvs) as any; }); } // uriValueList: [["", "value"], ...] -function mockSecretClientGetSecret(uriValueList) { +function mockSecretClientGetSecret(uriValueList: [string, string][]) { const dict = new Map(); for (const [uri, value] of uriValueList) { dict.set(uri, value); } - sinon.stub(SecretClient.prototype, "getSecret").callsFake(function (secretName, options) { + sinon.stub(SecretClient.prototype, "getSecret").callsFake(async function (secretName, options) { const url = new URL(this.vaultUrl); url.pathname = `/secrets/${secretName}`; if (options?.version) { url.pathname += `/${options.version}`; } - return { value: dict.get(url.toString()) }; + return { + name: secretName, + value: dict.get(url.toString()) + } as KeyVaultSecret; }) } @@ -61,7 +64,7 @@ const createMockedTokenCredential = (tenantId = TEST_TENANT_ID, clientId = TEST_ return new ClientSecretCredential(tenantId, clientId, clientSecret); } -const createMockedKeyVaultReference = (key, vaultUri) => ({ +const createMockedKeyVaultReference = (key: string, vaultUri: string) => ({ // https://${vaultName}.vault.azure.net/secrets/${secretName} value: `{"uri":"${vaultUri}"}`, key, @@ -74,7 +77,7 @@ const createMockedKeyVaultReference = (key, vaultUri) => ({ isReadOnly: false, }); -const createMockedJsonKeyValue = (key, value) => ({ +const createMockedJsonKeyValue = (key: string, value: any) => ({ value: value, key: key, label: null, @@ -85,7 +88,7 @@ const createMockedJsonKeyValue = (key, value) => ({ isReadOnly: false }); -const createMockedKeyValue = (props) => (Object.assign({ +const createMockedKeyValue = (props: {[key: string]: any}) => (Object.assign({ value: "TestValue", key: "TestKey", label: null, @@ -96,7 +99,7 @@ const createMockedKeyValue = (props) => (Object.assign({ isReadOnly: false }, props)); -module.exports = { +export { sinon, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..0f96a196 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": [ + "DOM", + "WebWorker", + "ESNext" + ], + "skipDefaultLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2022", + "strictNullChecks": true, + "strictFunctionTypes": true, + "sourceMap": true, + "inlineSources": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 395c4d54..f2fc0e9b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,10 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { - "lib": [ - "DOM", - "WebWorker", - "ESNext" - ], - "skipDefaultLibCheck": true, "module": "ESNext", - "moduleResolution": "Node", - "target": "ES2022", - "strictNullChecks": true, - "strictFunctionTypes": true, - "sourceMap": true, - "inlineSources": true, "outDir": "./dist-esm" }, "include": [ "src/**/*" - ], - "exclude": [ - "node_modules", - "**/node_modules/*" ] } \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..3cbd3c0a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./out" + }, + "include": [ + "test/**/*" + ] +} \ No newline at end of file