From 6bc3eb7ffa22a893ceae0537c61140556d321639 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 15 Mar 2020 08:26:21 -0700 Subject: [PATCH 01/18] stage --- src/commands/init.test.ts | 187 +++++++++++++++++- src/commands/init.ts | 135 ++++++++++--- .../servicePrincipalService.test.ts | 31 +-- .../servicePrincipalService.ts | 20 +- .../subscriptionService.test.ts | 23 ++- .../{setup => azure}/subscriptionService.ts | 15 +- src/lib/i18n.json | 13 ++ src/lib/promptBuilder.ts | 115 +++++++++++ src/lib/setup/prompt.test.ts | 14 +- src/lib/setup/prompt.ts | 101 ++++------ 10 files changed, 504 insertions(+), 150 deletions(-) rename src/lib/{setup => azure}/servicePrincipalService.test.ts (57%) rename src/lib/{setup => azure}/servicePrincipalService.ts (78%) rename src/lib/{setup => azure}/subscriptionService.test.ts (78%) rename src/lib/{setup => azure}/subscriptionService.ts (75%) create mode 100644 src/lib/i18n.json create mode 100644 src/lib/promptBuilder.ts diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index f63c6dc94..b67209c6a 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -7,14 +7,20 @@ import path from "path"; import uuid from "uuid"; import { saveConfiguration } from "../config"; import * as config from "../config"; +import * as servicePrincipalService from "../lib/azure/servicePrincipalService"; +import * as subscriptionService from "../lib/azure/subscriptionService"; import { createTempDir } from "../lib/ioUtil"; import { disableVerboseLogging, enableVerboseLogging } from "../logger"; import { ConfigYaml } from "../types"; import { execute, getConfig, + getSubscriptionId, handleInteractiveMode, + handleIntrospectionInteractive, + isIntrospectionAzureDefined, prompt, + promptCreateSP, validatePersonalAccessToken } from "./init"; import * as init from "./init"; @@ -30,6 +36,9 @@ afterAll(() => { }); const mockFileName = "src/commands/mocks/spk-config.yaml"; +const principalId = uuid(); +const principalPassword = uuid(); +const principalTenantId = uuid(); describe("Test execute function", () => { it("negative test: missing file value", async () => { @@ -177,7 +186,8 @@ const testHandleInteractiveModeFunc = async ( Promise.resolve({ azdo_org_name: "org_name", azdo_pat: "pat", - azdo_project_name: "project" + azdo_project_name: "project", + toSetupIntrospectionConfig: true }) ); jest @@ -185,6 +195,7 @@ const testHandleInteractiveModeFunc = async ( .mockReturnValueOnce(Promise.resolve(verified)); const tmpFile = path.join(createTempDir(), "config.yaml"); jest.spyOn(config, "defaultConfigFile").mockReturnValueOnce(tmpFile); + jest.spyOn(init, "handleIntrospectionInteractive").mockResolvedValueOnce(); await handleInteractiveMode(); const content = fs.readFileSync(tmpFile, "utf8"); const data = yaml.safeLoad(content) as ConfigYaml; @@ -209,7 +220,8 @@ describe("test prompt function", () => { const answers = { azdo_org_name: "org", azdo_pat: "pat", - azdo_project_name: "project" + azdo_project_name: "project", + toSetupIntrospectionConfig: false }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); const ans = await prompt({}); @@ -217,3 +229,174 @@ describe("test prompt function", () => { done(); }); }); + +const testPromptCreateSP = async (answer: boolean): Promise => { + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + create_service_principal: answer + }); + const ans = await promptCreateSP(); + expect(ans).toBe(answer); +}; + +describe("test promptCreateSP function", () => { + it("positive test: true", async () => { + testPromptCreateSP(true); + }); + it("positive test: false", async () => { + testPromptCreateSP(false); + }); + it("negative test: exception thrown", async () => { + jest.spyOn(inquirer, "prompt").mockRejectedValueOnce(Error("fake")); + await expect(promptCreateSP()).rejects.toThrow(); + }); +}); + +describe("test isIntrospectionAzureDefined function", () => { + it("positive test: true", () => { + const ans = isIntrospectionAzureDefined({ + introspection: { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }) + } + } + }); + expect(ans).toBe(true); + }); + it("positive test: false", () => { + const ans = isIntrospectionAzureDefined({ + introspection: {} + }); + expect(ans).toBe(false); + const ans1 = isIntrospectionAzureDefined({}); + expect(ans1).toBe(false); + }); +}); + +describe("test getSubscriptionId function", () => { + it("positive test, single value", async () => { + jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ + { + id: "test", + name: "test" + } + ]); + const config: ConfigYaml = { + introspection: { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }), + service_principal_id: principalId, + service_principal_secret: principalPassword, + tenant_id: principalTenantId + } + } + }; + await getSubscriptionId(config); + expect(config.introspection?.azure?.subscription_id).toBe("test"); + }); + it("positive test, multiple values", async () => { + jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ + { + id: "test", + name: "test" + }, + { + id: "test1", + name: "test1" + } + ]); + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + az_subscription: "test1" + }); + const config: ConfigYaml = { + introspection: { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }), + service_principal_id: principalId, + service_principal_secret: principalPassword, + tenant_id: principalTenantId + } + } + }; + await getSubscriptionId(config); + expect(config.introspection?.azure?.subscription_id).toBe("test1"); + }); + it("negative test, no subscription found", async () => { + jest + .spyOn(subscriptionService, "getSubscriptions") + .mockResolvedValueOnce([]); + const config: ConfigYaml = { + introspection: { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }), + service_principal_id: principalId, + service_principal_secret: principalPassword, + tenant_id: principalTenantId + } + } + }; + await expect(getSubscriptionId(config)).rejects.toThrow(); + }); +}); + +const testHandleIntrospectionInteractive = async ( + withIntrosepection = false, + promptCreateSP = true +): Promise => { + const config: ConfigYaml = {}; + if (!withIntrosepection) { + config["introspection"] = { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }) + } + }; + } + jest.spyOn(init, "promptCreateSP").mockResolvedValueOnce(promptCreateSP); + if (promptCreateSP) { + jest + .spyOn(servicePrincipalService, "createWithAzCLI") + .mockResolvedValueOnce({ + id: "id", + password: "password", + tenantId: "tenantId" + }); + } else { + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + az_sp_id: "id", + az_sp_password: "password", + az_sp_tenant: "tenantId" + }); + } + jest.spyOn(init, "getSubscriptionId").mockImplementationOnce( + async (curConfig: ConfigYaml): Promise => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const azure = curConfig.introspection!.azure!; + azure.subscription_id = "subscriptionId"; + } + ); + await handleIntrospectionInteractive(config); + expect(config.introspection?.azure?.subscription_id).toBe("subscriptionId"); + expect(config.introspection?.azure?.service_principal_id).toBe("id"); + expect(config.introspection?.azure?.service_principal_secret).toBe( + "password" + ); + expect(config.introspection?.azure?.tenant_id).toBe("tenantId"); +}; + +describe("test handleIntrospectionInteractive function", () => { + it("positive test", async () => { + await testHandleIntrospectionInteractive(false, true); + await testHandleIntrospectionInteractive(true, false); + await testHandleIntrospectionInteractive(false, true); + await testHandleIntrospectionInteractive(false, false); + }); +}); diff --git a/src/commands/init.ts b/src/commands/init.ts index 99662ea0f..8ec6abe55 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,14 +11,22 @@ import { loadConfiguration, saveConfiguration } from "../config"; +import { createWithAzCLI } from "../lib/azure/servicePrincipalService"; +import { getSubscriptions } from "../lib/azure/subscriptionService"; import { build as buildCmd, exit as exitCmd } from "../lib/commandBuilder"; -import { deepClone } from "../lib/util"; import { - hasValue, - validateAccessToken, - validateOrgName, - validateProjectName -} from "../lib/validator"; + askToCreateServicePrincipal, + askToSetupIntrospectionConfig, + azureAccessToken, + azureOrgName, + azureProjectName, + chooseSubscriptionId, + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId +} from "../lib/promptBuilder"; +import { deepClone } from "../lib/util"; +import { hasValue } from "../lib/validator"; import { logger } from "../logger"; import { ConfigYaml } from "../types"; import decorator from "./init.decorator.json"; @@ -32,6 +40,7 @@ interface Answer { azdo_org_name: string; azdo_project_name: string; azdo_pat: string; + toSetupIntrospectionConfig: boolean; } /** @@ -53,34 +62,17 @@ export const handleFileConfig = (file: string): void => { */ export const prompt = async (curConfig: ConfigYaml): Promise => { const questions = [ - { - default: curConfig.azure_devops?.org || undefined, - message: "Enter organization name\n", - name: "azdo_org_name", - type: "input", - validate: validateOrgName - }, - { - default: curConfig.azure_devops?.project || undefined, - message: "Enter project name\n", - name: "azdo_project_name", - type: "input", - validate: validateProjectName - }, - { - default: curConfig.azure_devops?.access_token || undefined, - mask: "*", - message: "Enter your AzDO personal access token\n", - name: "azdo_pat", - type: "password", - validate: validateAccessToken - } + azureOrgName(curConfig.azure_devops?.org), + azureProjectName(curConfig.azure_devops?.project), + azureAccessToken(curConfig.azure_devops?.access_token), + askToSetupIntrospectionConfig(false) ]; const answers = await inquirer.prompt(questions); return { azdo_org_name: answers.azdo_org_name as string, azdo_pat: answers.azdo_pat as string, - azdo_project_name: answers.azdo_project_name as string + azdo_project_name: answers.azdo_project_name as string, + toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -93,7 +85,7 @@ export const getConfig = (): ConfigYaml => { loadConfiguration(); return Config(); } catch (_) { - // current config is not found. + logger.info("current config is not found."); return { azure_devops: { access_token: "", @@ -130,6 +122,84 @@ export const validatePersonalAccessToken = async ( } }; +export const promptCreateSP = async (): Promise => { + const questions = [askToCreateServicePrincipal(false)]; + const answers = await inquirer.prompt(questions); + return !!answers.create_service_principal; +}; + +export const isIntrospectionAzureDefined = (curConfig: ConfigYaml): boolean => { + if (curConfig.introspection === undefined) { + return false; + } + const intro = curConfig.introspection!; + return intro.azure !== undefined; +}; + +export const getSubscriptionId = async ( + curConfig: ConfigYaml +): Promise => { + const azure = curConfig.introspection!.azure!; + + const subscriptions = await getSubscriptions( + azure.service_principal_id!, + azure.service_principal_secret!, + azure.tenant_id! + ); + if (subscriptions.length === 0) { + throw Error("no subscriptions found"); + } + if (subscriptions.length === 1) { + azure.subscription_id = subscriptions[0].id; + } else { + const ans = await inquirer.prompt([ + chooseSubscriptionId(subscriptions.map(s => s.name)) + ]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + azure.subscription_id = subscriptions.find( + s => s.name === (ans.az_subscription as string) + )!.id; + } +}; + +export const handleIntrospectionInteractive = async ( + curConfig: ConfigYaml +): Promise => { + if (!isIntrospectionAzureDefined(curConfig)) { + curConfig.introspection = { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }) + } + }; + // key is needed to create the azure object in + // introspction object however it causes + // problem when we do `yaml.safeDump` because + // key is a function. + delete curConfig.introspection.azure?.key; + } + const azure = curConfig.introspection!.azure!; + + if (await promptCreateSP()) { + const sp = await createWithAzCLI(); + azure.service_principal_id = sp.id; + azure.service_principal_secret = sp.password; + azure.tenant_id = sp.tenantId; + } else { + const answers = await inquirer.prompt([ + servicePrincipalId(azure.service_principal_id), + servicePrincipalPassword(azure.service_principal_secret), + servicePrincipalTenantId(azure.tenant_id) + ]); + azure.service_principal_id = answers.az_sp_id; + azure.service_principal_secret = answers.az_sp_password; + azure.tenant_id = answers.az_sp_tenant; + } + + await getSubscriptionId(curConfig); +}; + /** * Handles the interactive mode of the command. */ @@ -139,6 +209,11 @@ export const handleInteractiveMode = async (): Promise => { curConfig.azure_devops!.org = answer.azdo_org_name; curConfig.azure_devops!.project = answer.azdo_project_name; curConfig.azure_devops!.access_token = answer.azdo_pat; + + if (answer.toSetupIntrospectionConfig) { + await handleIntrospectionInteractive(curConfig); + } + const data = yaml.safeDump(curConfig); fs.writeFileSync(defaultConfigFile(), data); logger.info("Successfully constructed SPK configuration file."); diff --git a/src/lib/setup/servicePrincipalService.test.ts b/src/lib/azure/servicePrincipalService.test.ts similarity index 57% rename from src/lib/setup/servicePrincipalService.test.ts rename to src/lib/azure/servicePrincipalService.test.ts index 715606d3e..426d41c72 100644 --- a/src/lib/setup/servicePrincipalService.test.ts +++ b/src/lib/azure/servicePrincipalService.test.ts @@ -1,5 +1,4 @@ import * as shell from "../shell"; -import { RequestContext, WORKSPACE } from "./constants"; import { azCLILogin, createWithAzCLI } from "./servicePrincipalService"; import * as servicePrincipalService from "./servicePrincipalService"; @@ -29,30 +28,14 @@ describe("test createWithAzCLI function", () => { jest .spyOn(shell, "exec") .mockReturnValueOnce(Promise.resolve(JSON.stringify(result))); - const rc: RequestContext = { - accessToken: "pat", - orgName: "orgName", - projectName: "project", - workspace: WORKSPACE - }; - await createWithAzCLI(rc); - expect(rc.createServicePrincipal).toBeTruthy(); - expect(rc.servicePrincipalPassword).toBe(result.password); - expect(rc.servicePrincipalTenantId).toBe(result.tenant); + const sp = await createWithAzCLI(); + expect(sp.id).toBe(result.appId); + expect(sp.password).toBe(result.password); + expect(sp.tenantId).toBe(result.tenant); }); it("negative test", async () => { - jest - .spyOn(servicePrincipalService, "azCLILogin") - .mockReturnValueOnce(Promise.resolve()); - jest - .spyOn(shell, "exec") - .mockReturnValueOnce(Promise.reject(Error("fake"))); - const rc: RequestContext = { - accessToken: "pat", - orgName: "orgName", - projectName: "project", - workspace: WORKSPACE - }; - await expect(createWithAzCLI(rc)).rejects.toThrow(); + jest.spyOn(servicePrincipalService, "azCLILogin").mockResolvedValueOnce(); + jest.spyOn(shell, "exec").mockRejectedValueOnce(Error("fake")); + await expect(createWithAzCLI()).rejects.toThrow(); }); }); diff --git a/src/lib/setup/servicePrincipalService.ts b/src/lib/azure/servicePrincipalService.ts similarity index 78% rename from src/lib/setup/servicePrincipalService.ts rename to src/lib/azure/servicePrincipalService.ts index aff28cef9..d2287aba6 100644 --- a/src/lib/setup/servicePrincipalService.ts +++ b/src/lib/azure/servicePrincipalService.ts @@ -1,6 +1,11 @@ import { logger } from "../../logger"; import { exec } from "../shell"; -import { RequestContext } from "./constants"; + +export interface ServicePrincipal { + id: string; + password: string; + tenantId: string; +} /** * Login to az command line tool. This is done by @@ -24,20 +29,19 @@ export const azCLILogin = async (): Promise => { * this service principal should have contributor privileges. * Request context will have the service principal information * when service principal is successfully created. - * - * @param rc Request Context */ -export const createWithAzCLI = async (rc: RequestContext): Promise => { +export const createWithAzCLI = async (): Promise => { await azCLILogin(); try { logger.info("attempting to create service principal with az command line"); const result = await exec("az", ["ad", "sp", "create-for-rbac"]); const oResult = JSON.parse(result); - rc.createServicePrincipal = true; - rc.servicePrincipalId = oResult.appId; - rc.servicePrincipalPassword = oResult.password; - rc.servicePrincipalTenantId = oResult.tenant; logger.info("Successfully created service principal with az command line"); + return { + id: oResult.appId, + password: oResult.password, + tenantId: oResult.tenant + }; } catch (err) { logger.error("Unable to create service principal with az command line"); logger.error(err); diff --git a/src/lib/setup/subscriptionService.test.ts b/src/lib/azure/subscriptionService.test.ts similarity index 78% rename from src/lib/setup/subscriptionService.test.ts rename to src/lib/azure/subscriptionService.test.ts index 6ca97ef9c..6f02a11bc 100644 --- a/src/lib/setup/subscriptionService.test.ts +++ b/src/lib/azure/subscriptionService.test.ts @@ -1,6 +1,11 @@ +import uuid = require("uuid"); import * as restAuth from "@azure/ms-rest-nodeauth"; import { getSubscriptions } from "./subscriptionService"; +const principalId = uuid(); +const principalPassword = uuid(); +const principalTenantId = uuid(); + jest.mock("@azure/arm-subscriptions", () => { class MockClient { constructor() { @@ -32,12 +37,11 @@ describe("test getSubscriptions function", () => { .mockImplementationOnce(async () => { return {}; }); - const result = await getSubscriptions({ - accessToken: "pat", - orgName: "org", - projectName: "project", - workspace: "test" - }); + const result = await getSubscriptions( + principalId, + principalPassword, + principalTenantId + ); expect(result).toStrictEqual([ { id: "1234567890-abcdef", @@ -52,12 +56,7 @@ describe("test getSubscriptions function", () => { throw Error("fake"); }); await expect( - getSubscriptions({ - accessToken: "pat", - orgName: "org", - projectName: "project", - workspace: "test" - }) + getSubscriptions(principalId, principalPassword, principalTenantId) ).rejects.toThrow(); }); }); diff --git a/src/lib/setup/subscriptionService.ts b/src/lib/azure/subscriptionService.ts similarity index 75% rename from src/lib/setup/subscriptionService.ts rename to src/lib/azure/subscriptionService.ts index f71e56ef7..12d4dd96b 100644 --- a/src/lib/setup/subscriptionService.ts +++ b/src/lib/azure/subscriptionService.ts @@ -5,7 +5,6 @@ import { loginWithServicePrincipalSecret } from "@azure/ms-rest-nodeauth"; import { logger } from "../../logger"; -import { RequestContext } from "./constants"; export interface SubscriptionItem { id: string; @@ -15,17 +14,21 @@ export interface SubscriptionItem { /** * Returns a list of subscriptions based on the service principal credentials. * - * @param rc Request Context + * @param servicePrincipalId Service Principal Id + * @param servicePrincipalPassword Service Principal Password + * @param servicePrincipalTenantId Service Principal TenantId */ export const getSubscriptions = ( - rc: RequestContext + servicePrincipalId: string, + servicePrincipalPassword: string, + servicePrincipalTenantId: string ): Promise => { logger.info("attempting to get subscription list"); return new Promise((resolve, reject) => { loginWithServicePrincipalSecret( - rc.servicePrincipalId!, - rc.servicePrincipalPassword!, - rc.servicePrincipalTenantId! + servicePrincipalId!, + servicePrincipalPassword!, + servicePrincipalTenantId! ) .then(async (creds: ApplicationTokenCredentials) => { const client = new SubscriptionClient(creds); diff --git a/src/lib/i18n.json b/src/lib/i18n.json new file mode 100644 index 000000000..677e14869 --- /dev/null +++ b/src/lib/i18n.json @@ -0,0 +1,13 @@ +{ + "prompt": { + "orgName": "Enter organization name", + "projectName": "Enter project name", + "personalAccessToken": "Enter your AzDO personal access token", + "setupIntrospectionConfig": "Do you want to setup introspection configuration?", + "createServicePrincipal": "Do you want to create a service principal?", + "servicePrincipalId": "Enter Service Principal Id", + "servicePrincipalPassword": "Enter Service Principal Password", + "servicePrincipalTenantId": "Enter Service Principal Tenant Id", + "selectSubscriptionId": "Select one of the subscription" + } +} diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts new file mode 100644 index 000000000..38bf2b4ee --- /dev/null +++ b/src/lib/promptBuilder.ts @@ -0,0 +1,115 @@ +import { QuestionCollection } from "inquirer"; +import i18n from "./i18n.json"; +import { + validateAccessToken, + validateOrgName, + validateProjectName, + validateServicePrincipalId, + validateServicePrincipalPassword, + validateServicePrincipalTenantId +} from "../lib/validator"; + +export const azureOrgName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.orgName}\n`, + name: "azdo_org_name", + type: "input", + validate: validateOrgName + }; +}; + +export const azureProjectName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.projectName}\n`, + name: "azdo_project_name", + type: "input", + validate: validateProjectName + }; +}; + +export const azureAccessToken = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + mask: "*", + message: `${i18n.prompt.personalAccessToken}\n`, + name: "azdo_pat", + type: "password", + validate: validateAccessToken + }; +}; + +export const askToSetupIntrospectionConfig = ( + defaultValue = false +): QuestionCollection => { + return { + default: defaultValue, + message: i18n.prompt.setupIntrospectionConfig, + name: "toSetupIntrospectionConfig", + type: "confirm" + }; +}; + +export const askToCreateServicePrincipal = ( + defaultValue = false +): QuestionCollection => { + return { + default: defaultValue, + message: i18n.prompt.createServicePrincipal, + name: "create_service_principal", + type: "confirm" + }; +}; + +export const servicePrincipalId = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.servicePrincipalId}\n`, + name: "az_sp_id", + type: "input", + validate: validateServicePrincipalId + }; +}; + +export const servicePrincipalPassword = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + mask: "*", + message: `${i18n.prompt.servicePrincipalPassword}\n`, + name: "az_sp_password", + type: "password", + validate: validateServicePrincipalPassword + }; +}; + +export const servicePrincipalTenantId = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.servicePrincipalTenantId}\n`, + name: "az_sp_tenant", + type: "input", + validate: validateServicePrincipalTenantId + }; +}; + +export const chooseSubscriptionId = (names: string[]): QuestionCollection => { + return { + choices: names, + message: `${i18n.prompt.selectSubscriptionId}\n`, + name: "az_subscription", + type: "list" + }; +}; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 8ffba9579..0518ca6da 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -7,8 +7,8 @@ import uuid from "uuid/v4"; import { createTempDir } from "../../lib/ioUtil"; import { DEFAULT_PROJECT_NAME, RequestContext, WORKSPACE } from "./constants"; import { getAnswerFromFile, prompt, promptForSubscriptionId } from "./prompt"; -import * as servicePrincipalService from "./servicePrincipalService"; -import * as subscriptionService from "./subscriptionService"; +import * as servicePrincipalService from "../azure/servicePrincipalService"; +import * as subscriptionService from "../azure/subscriptionService"; describe("test prompt function", () => { it("positive test: No App Creation", async () => { @@ -41,7 +41,11 @@ describe("test prompt function", () => { }); jest .spyOn(servicePrincipalService, "createWithAzCLI") - .mockReturnValueOnce(Promise.resolve()); + .mockResolvedValueOnce({ + id: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", + password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", + tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" + }); jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ { id: "72f988bf-86f1-41af-91ab-2d7cd011db48", @@ -52,8 +56,12 @@ describe("test prompt function", () => { const ans = await prompt(); expect(ans).toStrictEqual({ accessToken: "pat", + createServicePrincipal: true, orgName: "org", projectName: "project", + servicePrincipalId: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", + servicePrincipalPassword: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", + servicePrincipalTenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47", subscriptionId: "72f988bf-86f1-41af-91ab-2d7cd011db48", toCreateAppRepo: true, toCreateSP: true, diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index 067313d5d..83e4167fd 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -1,6 +1,15 @@ -/* eslint-disable @typescript-eslint/camelcase */ import fs from "fs"; import inquirer from "inquirer"; +import { + askToCreateServicePrincipal, + azureAccessToken, + azureOrgName, + azureProjectName, + chooseSubscriptionId, + servicePrincipalId, + servicePrincipalPassword, + servicePrincipalTenantId +} from "../promptBuilder"; import { validateAccessToken, validateOrgName, @@ -11,28 +20,26 @@ import { validateSubscriptionId } from "../validator"; import { DEFAULT_PROJECT_NAME, RequestContext, WORKSPACE } from "./constants"; -import { createWithAzCLI } from "./servicePrincipalService"; -import { getSubscriptions } from "./subscriptionService"; +import { createWithAzCLI } from "../azure/servicePrincipalService"; +import { getSubscriptions } from "../azure/subscriptionService"; export const promptForSubscriptionId = async ( rc: RequestContext ): Promise => { - const subscriptions = await getSubscriptions(rc); + const subscriptions = await getSubscriptions( + rc.servicePrincipalId as string, + rc.servicePrincipalPassword as string, + rc.servicePrincipalTenantId as string + ); if (subscriptions.length === 0) { throw Error("no subscriptions found"); } if (subscriptions.length === 1) { rc.subscriptionId = subscriptions[0].id; } else { - const questions = [ - { - choices: subscriptions.map(s => s.name), - message: "Select one of the subscription\n", - name: "az_subscription", - type: "list" - } - ]; - const ans = await inquirer.prompt(questions); + const ans = await inquirer.prompt([ + chooseSubscriptionId(subscriptions.map(s => s.name)) + ]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion rc.subscriptionId = subscriptions.find( s => s.name === (ans.az_subscription as string) @@ -51,30 +58,14 @@ export const promptForServicePrincipal = async ( rc: RequestContext ): Promise => { const questions = [ - { - message: "Enter Service Principal Id\n", - name: "az_sp_id", - type: "input", - validate: validateServicePrincipalId - }, - { - mask: "*", - message: "Enter Service Principal Password\n", - name: "az_sp_password", - type: "password", - validate: validateServicePrincipalPassword - }, - { - message: "Enter Service Principal Tenant Id\n", - name: "az_sp_tenant", - type: "input", - validate: validateServicePrincipalTenantId - } + servicePrincipalId(), + servicePrincipalPassword(), + servicePrincipalTenantId() ]; const answers = await inquirer.prompt(questions); - rc.servicePrincipalId = answers.az_sp_id as string; - rc.servicePrincipalPassword = answers.az_sp_password as string; - rc.servicePrincipalTenantId = answers.az_sp_tenant as string; + rc.servicePrincipalId = answers.az_sp_id; + rc.servicePrincipalPassword = answers.az_sp_password; + rc.servicePrincipalTenantId = answers.az_sp_tenant; }; /** @@ -86,18 +77,15 @@ export const promptForServicePrincipal = async ( export const promptForServicePrincipalCreation = async ( rc: RequestContext ): Promise => { - const questions = [ - { - default: true, - message: `Do you want to create a service principal?`, - name: "create_service_principal", - type: "confirm" - } - ]; + const questions = [askToCreateServicePrincipal(true)]; const answers = await inquirer.prompt(questions); if (answers.create_service_principal) { rc.toCreateSP = true; - await createWithAzCLI(rc); + const sp = await createWithAzCLI(); + rc.createServicePrincipal = true; + rc.servicePrincipalId = sp.id; + rc.servicePrincipalPassword = sp.password; + rc.servicePrincipalTenantId = sp.tenantId; } else { rc.toCreateSP = false; await promptForServicePrincipal(rc); @@ -112,26 +100,9 @@ export const promptForServicePrincipalCreation = async ( */ export const prompt = async (): Promise => { const questions = [ - { - message: "Enter organization name\n", - name: "azdo_org_name", - type: "input", - validate: validateOrgName - }, - { - default: DEFAULT_PROJECT_NAME, - message: "Enter name of project to be created\n", - name: "azdo_project_name", - type: "input", - validate: validateProjectName - }, - { - mask: "*", - message: "Enter your Azure DevOps personal access token\n", - name: "azdo_pat", - type: "password", - validate: validateAccessToken - }, + azureOrgName(), + azureProjectName(), + azureAccessToken(), { default: true, message: `Do you like create a sample application repository?`, @@ -214,7 +185,7 @@ const parseInformationFromFile = (file: string): { [key: string]: string } => { */ export const getAnswerFromFile = (file: string): RequestContext => { const map = parseInformationFromFile(file); - map.azdo_project_name = map.azdo_project_name || DEFAULT_PROJECT_NAME; + map["azdo_project_name"] = map["azdo_project_name"] || DEFAULT_PROJECT_NAME; const vOrgName = validateOrgName(map.azdo_org_name); if (typeof vOrgName === "string") { From 957f1a0d5c32782dfd766d432a81768a14355bb7 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 15 Mar 2020 17:41:09 -0700 Subject: [PATCH 02/18] code clean up --- src/commands/init.ts | 36 ++++++++++++++---------------------- src/lib/promptBuilder.ts | 12 ++++++++++++ src/lib/setup/prompt.ts | 28 +++++++--------------------- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 8ec6abe55..7f19a9234 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -14,17 +14,7 @@ import { import { createWithAzCLI } from "../lib/azure/servicePrincipalService"; import { getSubscriptions } from "../lib/azure/subscriptionService"; import { build as buildCmd, exit as exitCmd } from "../lib/commandBuilder"; -import { - askToCreateServicePrincipal, - askToSetupIntrospectionConfig, - azureAccessToken, - azureOrgName, - azureProjectName, - chooseSubscriptionId, - servicePrincipalId, - servicePrincipalPassword, - servicePrincipalTenantId -} from "../lib/promptBuilder"; +import * as promptBuilder from "../lib/promptBuilder"; import { deepClone } from "../lib/util"; import { hasValue } from "../lib/validator"; import { logger } from "../logger"; @@ -62,10 +52,10 @@ export const handleFileConfig = (file: string): void => { */ export const prompt = async (curConfig: ConfigYaml): Promise => { const questions = [ - azureOrgName(curConfig.azure_devops?.org), - azureProjectName(curConfig.azure_devops?.project), - azureAccessToken(curConfig.azure_devops?.access_token), - askToSetupIntrospectionConfig(false) + promptBuilder.azureOrgName(curConfig.azure_devops?.org), + promptBuilder.azureProjectName(curConfig.azure_devops?.project), + promptBuilder.azureAccessToken(curConfig.azure_devops?.access_token), + promptBuilder.askToSetupIntrospectionConfig(false) ]; const answers = await inquirer.prompt(questions); return { @@ -123,7 +113,7 @@ export const validatePersonalAccessToken = async ( }; export const promptCreateSP = async (): Promise => { - const questions = [askToCreateServicePrincipal(false)]; + const questions = [promptBuilder.askToCreateServicePrincipal(false)]; const answers = await inquirer.prompt(questions); return !!answers.create_service_principal; }; @@ -153,7 +143,7 @@ export const getSubscriptionId = async ( azure.subscription_id = subscriptions[0].id; } else { const ans = await inquirer.prompt([ - chooseSubscriptionId(subscriptions.map(s => s.name)) + promptBuilder.chooseSubscriptionId(subscriptions.map(s => s.name)) ]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion azure.subscription_id = subscriptions.find( @@ -187,11 +177,13 @@ export const handleIntrospectionInteractive = async ( azure.service_principal_secret = sp.password; azure.tenant_id = sp.tenantId; } else { - const answers = await inquirer.prompt([ - servicePrincipalId(azure.service_principal_id), - servicePrincipalPassword(azure.service_principal_secret), - servicePrincipalTenantId(azure.tenant_id) - ]); + const answers = await inquirer.prompt( + promptBuilder.servicePrincipal( + azure.service_principal_id, + azure.service_principal_secret, + azure.tenant_id + ) + ); azure.service_principal_id = answers.az_sp_id; azure.service_principal_secret = answers.az_sp_password; azure.tenant_id = answers.az_sp_tenant; diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 38bf2b4ee..70e8d7bb6 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -105,6 +105,18 @@ export const servicePrincipalTenantId = ( }; }; +export const servicePrincipal = ( + id?: string | undefined, + pwd?: string | undefined, + tenantId?: string | undefined +): QuestionCollection[] => { + return [ + servicePrincipalId(id), + servicePrincipalPassword(pwd), + servicePrincipalTenantId(tenantId) + ]; +}; + export const chooseSubscriptionId = (names: string[]): QuestionCollection => { return { choices: names, diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index 83e4167fd..2de0a426b 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -1,15 +1,6 @@ import fs from "fs"; import inquirer from "inquirer"; -import { - askToCreateServicePrincipal, - azureAccessToken, - azureOrgName, - azureProjectName, - chooseSubscriptionId, - servicePrincipalId, - servicePrincipalPassword, - servicePrincipalTenantId -} from "../promptBuilder"; +import * as promptBuilder from "../promptBuilder"; import { validateAccessToken, validateOrgName, @@ -38,7 +29,7 @@ export const promptForSubscriptionId = async ( rc.subscriptionId = subscriptions[0].id; } else { const ans = await inquirer.prompt([ - chooseSubscriptionId(subscriptions.map(s => s.name)) + promptBuilder.chooseSubscriptionId(subscriptions.map(s => s.name)) ]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion rc.subscriptionId = subscriptions.find( @@ -57,12 +48,7 @@ export const promptForSubscriptionId = async ( export const promptForServicePrincipal = async ( rc: RequestContext ): Promise => { - const questions = [ - servicePrincipalId(), - servicePrincipalPassword(), - servicePrincipalTenantId() - ]; - const answers = await inquirer.prompt(questions); + const answers = await inquirer.prompt(promptBuilder.servicePrincipal()); rc.servicePrincipalId = answers.az_sp_id; rc.servicePrincipalPassword = answers.az_sp_password; rc.servicePrincipalTenantId = answers.az_sp_tenant; @@ -77,7 +63,7 @@ export const promptForServicePrincipal = async ( export const promptForServicePrincipalCreation = async ( rc: RequestContext ): Promise => { - const questions = [askToCreateServicePrincipal(true)]; + const questions = [promptBuilder.askToCreateServicePrincipal(true)]; const answers = await inquirer.prompt(questions); if (answers.create_service_principal) { rc.toCreateSP = true; @@ -100,9 +86,9 @@ export const promptForServicePrincipalCreation = async ( */ export const prompt = async (): Promise => { const questions = [ - azureOrgName(), - azureProjectName(), - azureAccessToken(), + promptBuilder.azureOrgName(), + promptBuilder.azureProjectName(), + promptBuilder.azureAccessToken(), { default: true, message: `Do you like create a sample application repository?`, From 4eea6126fe1eeefa919a83ab5f02407d09857e43 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Mon, 16 Mar 2020 09:38:43 -0700 Subject: [PATCH 03/18] added storage account and storage table prompt --- dennis.yaml | 12 ++++++++++ src/commands/init.test.ts | 4 ++++ src/commands/init.ts | 20 ++++++++++++----- src/lib/i18n.json | 10 +++++---- src/lib/promptBuilder.ts | 45 +++++++++++++++++++++++++------------ src/lib/validator.test.ts | 47 +++++++++++++++++++++++++++++++++++++-- src/lib/validator.ts | 40 +++++++++++++++++++++++++++++++-- 7 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 dennis.yaml diff --git a/dennis.yaml b/dennis.yaml new file mode 100644 index 000000000..77366ea3b --- /dev/null +++ b/dennis.yaml @@ -0,0 +1,12 @@ +azure_devops: + access_token: doegx7fx26zj5gwgzlbz4rphh5h5dzwqelxaucibyvgxp45vhlna + org: veseah + project: SPK12345 +introspection: + azure: + account_name: storage12345 + service_principal_id: 53441b1b-132e-4dba-992f-9009d86ddfa3 + service_principal_secret: e79ff863-5b90-4340-b9e9-f7cede7a03bd + tenant_id: 72f988bf-86f1-41af-91ab-2d7cd011db47 + subscription_id: dd831253-787f-4dc8-8eb0-ac9d052177d9 + table_name: table12345 diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index b67209c6a..dd2b9f640 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -360,6 +360,10 @@ const testHandleIntrospectionInteractive = async ( } }; } + jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + azdo_storage_account_name: "storagetest", + azdo_storage_table_name: "storagetabletest" + }); jest.spyOn(init, "promptCreateSP").mockResolvedValueOnce(promptCreateSP); if (promptCreateSP) { jest diff --git a/src/commands/init.ts b/src/commands/init.ts index 7f19a9234..2665446a7 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -163,14 +163,16 @@ export const handleIntrospectionInteractive = async ( }) } }; - // key is needed to create the azure object in - // introspction object however it causes - // problem when we do `yaml.safeDump` because - // key is a function. - delete curConfig.introspection.azure?.key; } const azure = curConfig.introspection!.azure!; + const ans = await inquirer.prompt([ + promptBuilder.azureStorageAccountName(azure.account_name), + promptBuilder.azureStorageTableName(azure.table_name) + ]); + azure.account_name = ans.azdo_storage_account_name; + azure.table_name = ans.azdo_storage_table_name; + if (await promptCreateSP()) { const sp = await createWithAzCLI(); azure.service_principal_id = sp.id; @@ -206,7 +208,15 @@ export const handleInteractiveMode = async (): Promise => { await handleIntrospectionInteractive(curConfig); } + if (curConfig.introspection && curConfig.introspection.azure) { + // key is needed to create the azure object in + // introspction object however it causes + // problem when we do `yaml.safeDump` because + // key is a function. + delete curConfig.introspection.azure.key; + } const data = yaml.safeDump(curConfig); + fs.writeFileSync(defaultConfigFile(), data); logger.info("Successfully constructed SPK configuration file."); const ok = await validatePersonalAccessToken(curConfig.azure_devops); diff --git a/src/lib/i18n.json b/src/lib/i18n.json index 677e14869..91a3b81fa 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -1,13 +1,15 @@ { "prompt": { + "createServicePrincipal": "Do you want to create a service principal?", "orgName": "Enter organization name", - "projectName": "Enter project name", "personalAccessToken": "Enter your AzDO personal access token", - "setupIntrospectionConfig": "Do you want to setup introspection configuration?", - "createServicePrincipal": "Do you want to create a service principal?", + "projectName": "Enter project name", + "selectSubscriptionId": "Select one of the subscription", "servicePrincipalId": "Enter Service Principal Id", "servicePrincipalPassword": "Enter Service Principal Password", "servicePrincipalTenantId": "Enter Service Principal Tenant Id", - "selectSubscriptionId": "Select one of the subscription" + "setupIntrospectionConfig": "Do you want to setup introspection configuration?", + "storageAccountName": "Enter storage account name", + "storageTableName": "Enter storage table name" } } diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 70e8d7bb6..4b6089fca 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -1,13 +1,6 @@ import { QuestionCollection } from "inquirer"; import i18n from "./i18n.json"; -import { - validateAccessToken, - validateOrgName, - validateProjectName, - validateServicePrincipalId, - validateServicePrincipalPassword, - validateServicePrincipalTenantId -} from "../lib/validator"; +import * as validator from "../lib/validator"; export const azureOrgName = ( defaultValue?: string | undefined @@ -17,7 +10,7 @@ export const azureOrgName = ( message: `${i18n.prompt.orgName}\n`, name: "azdo_org_name", type: "input", - validate: validateOrgName + validate: validator.validateOrgName }; }; @@ -29,7 +22,7 @@ export const azureProjectName = ( message: `${i18n.prompt.projectName}\n`, name: "azdo_project_name", type: "input", - validate: validateProjectName + validate: validator.validateProjectName }; }; @@ -42,7 +35,7 @@ export const azureAccessToken = ( message: `${i18n.prompt.personalAccessToken}\n`, name: "azdo_pat", type: "password", - validate: validateAccessToken + validate: validator.validateAccessToken }; }; @@ -76,7 +69,7 @@ export const servicePrincipalId = ( message: `${i18n.prompt.servicePrincipalId}\n`, name: "az_sp_id", type: "input", - validate: validateServicePrincipalId + validate: validator.validateServicePrincipalId }; }; @@ -89,7 +82,7 @@ export const servicePrincipalPassword = ( message: `${i18n.prompt.servicePrincipalPassword}\n`, name: "az_sp_password", type: "password", - validate: validateServicePrincipalPassword + validate: validator.validateServicePrincipalPassword }; }; @@ -101,7 +94,7 @@ export const servicePrincipalTenantId = ( message: `${i18n.prompt.servicePrincipalTenantId}\n`, name: "az_sp_tenant", type: "input", - validate: validateServicePrincipalTenantId + validate: validator.validateServicePrincipalTenantId }; }; @@ -125,3 +118,27 @@ export const chooseSubscriptionId = (names: string[]): QuestionCollection => { type: "list" }; }; + +export const azureStorageAccountName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.storageAccountName}\n`, + name: "azdo_storage_account_name", + type: "input", + validate: validator.validateStorageAccountName + }; +}; + +export const azureStorageTableName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.storageTableName}\n`, + name: "azdo_storage_table_name", + type: "input", + validate: validator.validateStorageTableName + }; +}; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index b7a74b0da..0baa95f43 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -16,6 +16,8 @@ import { validateServicePrincipalId, validateServicePrincipalPassword, validateServicePrincipalTenantId, + validateStorageAccountName, + validateStorageTableName, validateSubscriptionId } from "./validator"; @@ -228,10 +230,51 @@ describe("test validateServicePrincipal functions", () => { describe("test validateSubscriptionId function", () => { it("sanity test", () => { - expect(validateSubscriptionId("")).toBe("Must enter a Subscription Id."); + expect(validateSubscriptionId("")).toBe( + "Must enter a subscription identifier." + ); expect(validateSubscriptionId("xyz")).toBe( - "The value for Subscription Id is invalid." + "The value for subscription identifier is invalid." ); expect(validateSubscriptionId("abc123-456")).toBeTruthy(); }); }); + +describe("test validateStorageAccountName test", () => { + it("sanity test", () => { + expect(validateStorageAccountName("")).toBe( + "Must enter a storage account name." + ); + expect(validateStorageAccountName("XYZ123")).toBe( + "The value for storage account name is invalid. Lowercase letters and numbers are allowed." + ); + expect(validateStorageAccountName("ab")).toBe( + "The value for storage account name is invalid. It has to be between 3 and 24 characters long" + ); + expect(validateStorageAccountName("12345678a".repeat(3))).toBe( + "The value for storage account name is invalid. It has to be between 3 and 24 characters long" + ); + expect(validateStorageAccountName("abc123456")).toBeTruthy(); + }); +}); + +describe("test validateStorageTableName test", () => { + it("sanity test", () => { + expect(validateStorageTableName("")).toBe( + "Must enter a storage table name." + ); + expect(validateStorageTableName("XYZ123*")).toBe( + "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet." + ); + expect(validateStorageTableName("1XYZ123")).toBe( + "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet." + ); + expect(validateStorageTableName("ab")).toBe( + "The value for storage table name is invalid. It has to be between 3 and 63 characters long" + ); + expect(validateStorageTableName("a123456789".repeat(7))).toBe( + "The value for storage table name is invalid. It has to be between 3 and 63 characters long" + ); + expect(validateStorageTableName("abc123456")).toBeTruthy(); + }); +}); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 07b789333..1ed14cd06 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -234,10 +234,46 @@ export const validateServicePrincipalTenantId = ( */ export const validateSubscriptionId = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a Subscription Id."; + return "Must enter a subscription identifier."; } if (!isDashHex(value)) { - return "The value for Subscription Id is invalid."; + return "The value for subscription identifier is invalid."; + } + return true; +}; + +/** + * Returns true if storage account name is valid. + * + * @param value storage account name . + */ +export const validateStorageAccountName = (value: string): string | boolean => { + if (!hasValue(value)) { + return "Must enter a storage account name."; + } + if (!value.match(/^[a-z0-9]+$/)) { + return "The value for storage account name is invalid. Lowercase letters and numbers are allowed."; + } + if (value.length < 3 || value.length > 24) { + return "The value for storage account name is invalid. It has to be between 3 and 24 characters long"; + } + return true; +}; + +/** + * Returns true if storage table name is valid. + * + * @param value storage table name . + */ +export const validateStorageTableName = (value: string): string | boolean => { + if (!hasValue(value)) { + return "Must enter a storage table name."; + } + if (!value.match(/^[A-Za-z][A-Za-z0-9]*$/)) { + return "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet."; + } + if (value.length < 3 || value.length > 63) { + return "The value for storage table name is invalid. It has to be between 3 and 63 characters long"; } return true; }; From ad9a6c5c1b5b66ad52993df239fcca39c50c26ed Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Mon, 16 Mar 2020 10:25:40 -0700 Subject: [PATCH 04/18] Delete dennis.yaml --- dennis.yaml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 dennis.yaml diff --git a/dennis.yaml b/dennis.yaml deleted file mode 100644 index 77366ea3b..000000000 --- a/dennis.yaml +++ /dev/null @@ -1,12 +0,0 @@ -azure_devops: - access_token: doegx7fx26zj5gwgzlbz4rphh5h5dzwqelxaucibyvgxp45vhlna - org: veseah - project: SPK12345 -introspection: - azure: - account_name: storage12345 - service_principal_id: 53441b1b-132e-4dba-992f-9009d86ddfa3 - service_principal_secret: e79ff863-5b90-4340-b9e9-f7cede7a03bd - tenant_id: 72f988bf-86f1-41af-91ab-2d7cd011db47 - subscription_id: dd831253-787f-4dc8-8eb0-ac9d052177d9 - table_name: table12345 From f0470c85da5db6f1ea583568e02188f4c0bdd8af Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Mon, 16 Mar 2020 13:52:24 -0700 Subject: [PATCH 05/18] stage --- src/commands/init.test.ts | 175 +++----------------------------------- src/commands/init.ts | 75 +++++----------- src/lib/i18n.json | 3 +- src/lib/promptBuilder.ts | 13 +++ src/lib/validator.test.ts | 12 +++ src/lib/validator.ts | 15 ++++ 6 files changed, 76 insertions(+), 217 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index dd2b9f640..c8c954ef8 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -7,20 +7,15 @@ import path from "path"; import uuid from "uuid"; import { saveConfiguration } from "../config"; import * as config from "../config"; -import * as servicePrincipalService from "../lib/azure/servicePrincipalService"; -import * as subscriptionService from "../lib/azure/subscriptionService"; import { createTempDir } from "../lib/ioUtil"; import { disableVerboseLogging, enableVerboseLogging } from "../logger"; import { ConfigYaml } from "../types"; import { execute, getConfig, - getSubscriptionId, handleInteractiveMode, handleIntrospectionInteractive, - isIntrospectionAzureDefined, prompt, - promptCreateSP, validatePersonalAccessToken } from "./init"; import * as init from "./init"; @@ -36,9 +31,6 @@ afterAll(() => { }); const mockFileName = "src/commands/mocks/spk-config.yaml"; -const principalId = uuid(); -const principalPassword = uuid(); -const principalTenantId = uuid(); describe("Test execute function", () => { it("negative test: missing file value", async () => { @@ -180,6 +172,13 @@ const testHandleInteractiveModeFunc = async ( access_token: "", org: "", project: "" + }, + introspection: { + azure: { + key: new Promise(resolve => { + resolve(undefined); + }) + } } }); jest.spyOn(init, "prompt").mockReturnValueOnce( @@ -221,7 +220,7 @@ describe("test prompt function", () => { azdo_org_name: "org", azdo_pat: "pat", azdo_project_name: "project", - toSetupIntrospectionConfig: false + toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); const ans = await prompt({}); @@ -230,128 +229,11 @@ describe("test prompt function", () => { }); }); -const testPromptCreateSP = async (answer: boolean): Promise => { - jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - create_service_principal: answer - }); - const ans = await promptCreateSP(); - expect(ans).toBe(answer); -}; - -describe("test promptCreateSP function", () => { - it("positive test: true", async () => { - testPromptCreateSP(true); - }); - it("positive test: false", async () => { - testPromptCreateSP(false); - }); - it("negative test: exception thrown", async () => { - jest.spyOn(inquirer, "prompt").mockRejectedValueOnce(Error("fake")); - await expect(promptCreateSP()).rejects.toThrow(); - }); -}); - -describe("test isIntrospectionAzureDefined function", () => { - it("positive test: true", () => { - const ans = isIntrospectionAzureDefined({ - introspection: { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }) - } - } - }); - expect(ans).toBe(true); - }); - it("positive test: false", () => { - const ans = isIntrospectionAzureDefined({ - introspection: {} - }); - expect(ans).toBe(false); - const ans1 = isIntrospectionAzureDefined({}); - expect(ans1).toBe(false); - }); -}); - -describe("test getSubscriptionId function", () => { - it("positive test, single value", async () => { - jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ - { - id: "test", - name: "test" - } - ]); - const config: ConfigYaml = { - introspection: { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }), - service_principal_id: principalId, - service_principal_secret: principalPassword, - tenant_id: principalTenantId - } - } - }; - await getSubscriptionId(config); - expect(config.introspection?.azure?.subscription_id).toBe("test"); - }); - it("positive test, multiple values", async () => { - jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ - { - id: "test", - name: "test" - }, - { - id: "test1", - name: "test1" - } - ]); - jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_subscription: "test1" - }); - const config: ConfigYaml = { - introspection: { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }), - service_principal_id: principalId, - service_principal_secret: principalPassword, - tenant_id: principalTenantId - } - } - }; - await getSubscriptionId(config); - expect(config.introspection?.azure?.subscription_id).toBe("test1"); - }); - it("negative test, no subscription found", async () => { - jest - .spyOn(subscriptionService, "getSubscriptions") - .mockResolvedValueOnce([]); - const config: ConfigYaml = { - introspection: { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }), - service_principal_id: principalId, - service_principal_secret: principalPassword, - tenant_id: principalTenantId - } - } - }; - await expect(getSubscriptionId(config)).rejects.toThrow(); - }); -}); - const testHandleIntrospectionInteractive = async ( - withIntrosepection = false, - promptCreateSP = true + withIntrospection = false ): Promise => { const config: ConfigYaml = {}; - if (!withIntrosepection) { + if (!withIntrospection) { config["introspection"] = { azure: { key: new Promise(resolve => { @@ -364,43 +246,14 @@ const testHandleIntrospectionInteractive = async ( azdo_storage_account_name: "storagetest", azdo_storage_table_name: "storagetabletest" }); - jest.spyOn(init, "promptCreateSP").mockResolvedValueOnce(promptCreateSP); - if (promptCreateSP) { - jest - .spyOn(servicePrincipalService, "createWithAzCLI") - .mockResolvedValueOnce({ - id: "id", - password: "password", - tenantId: "tenantId" - }); - } else { - jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_sp_id: "id", - az_sp_password: "password", - az_sp_tenant: "tenantId" - }); - } - jest.spyOn(init, "getSubscriptionId").mockImplementationOnce( - async (curConfig: ConfigYaml): Promise => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const azure = curConfig.introspection!.azure!; - azure.subscription_id = "subscriptionId"; - } - ); await handleIntrospectionInteractive(config); - expect(config.introspection?.azure?.subscription_id).toBe("subscriptionId"); - expect(config.introspection?.azure?.service_principal_id).toBe("id"); - expect(config.introspection?.azure?.service_principal_secret).toBe( - "password" - ); - expect(config.introspection?.azure?.tenant_id).toBe("tenantId"); + expect(config.introspection?.azure?.account_name).toBe("storagetest"); + expect(config.introspection?.azure?.table_name).toBe("storagetabletest"); }; describe("test handleIntrospectionInteractive function", () => { it("positive test", async () => { - await testHandleIntrospectionInteractive(false, true); - await testHandleIntrospectionInteractive(true, false); - await testHandleIntrospectionInteractive(false, true); - await testHandleIntrospectionInteractive(false, false); + await testHandleIntrospectionInteractive(false); + await testHandleIntrospectionInteractive(true); }); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 2665446a7..f60ec0f9a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,8 +11,6 @@ import { loadConfiguration, saveConfiguration } from "../config"; -import { createWithAzCLI } from "../lib/azure/servicePrincipalService"; -import { getSubscriptions } from "../lib/azure/subscriptionService"; import { build as buildCmd, exit as exitCmd } from "../lib/commandBuilder"; import * as promptBuilder from "../lib/promptBuilder"; import { deepClone } from "../lib/util"; @@ -112,12 +110,6 @@ export const validatePersonalAccessToken = async ( } }; -export const promptCreateSP = async (): Promise => { - const questions = [promptBuilder.askToCreateServicePrincipal(false)]; - const answers = await inquirer.prompt(questions); - return !!answers.create_service_principal; -}; - export const isIntrospectionAzureDefined = (curConfig: ConfigYaml): boolean => { if (curConfig.introspection === undefined) { return false; @@ -126,32 +118,6 @@ export const isIntrospectionAzureDefined = (curConfig: ConfigYaml): boolean => { return intro.azure !== undefined; }; -export const getSubscriptionId = async ( - curConfig: ConfigYaml -): Promise => { - const azure = curConfig.introspection!.azure!; - - const subscriptions = await getSubscriptions( - azure.service_principal_id!, - azure.service_principal_secret!, - azure.tenant_id! - ); - if (subscriptions.length === 0) { - throw Error("no subscriptions found"); - } - if (subscriptions.length === 1) { - azure.subscription_id = subscriptions[0].id; - } else { - const ans = await inquirer.prompt([ - promptBuilder.chooseSubscriptionId(subscriptions.map(s => s.name)) - ]); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - azure.subscription_id = subscriptions.find( - s => s.name === (ans.az_subscription as string) - )!.id; - } -}; - export const handleIntrospectionInteractive = async ( curConfig: ConfigYaml ): Promise => { @@ -164,41 +130,40 @@ export const handleIntrospectionInteractive = async ( } }; } - const azure = curConfig.introspection!.azure!; + const azure = curConfig.introspection!.azure!; + console.log(JSON.stringify(azure)); const ans = await inquirer.prompt([ promptBuilder.azureStorageAccountName(azure.account_name), - promptBuilder.azureStorageTableName(azure.table_name) + promptBuilder.azureStorageTableName(azure.table_name), + promptBuilder.azureStorageKey(undefined) ]); azure.account_name = ans.azdo_storage_account_name; azure.table_name = ans.azdo_storage_table_name; +}; - if (await promptCreateSP()) { - const sp = await createWithAzCLI(); - azure.service_principal_id = sp.id; - azure.service_principal_secret = sp.password; - azure.tenant_id = sp.tenantId; - } else { - const answers = await inquirer.prompt( - promptBuilder.servicePrincipal( - azure.service_principal_id, - azure.service_principal_secret, - azure.tenant_id - ) - ); - azure.service_principal_id = answers.az_sp_id; - azure.service_principal_secret = answers.az_sp_password; - azure.tenant_id = answers.az_sp_tenant; +export const cloneConfig = (): ConfigYaml => { + const config = getConfig(); + const introspection = config.introspection; + const azure = introspection?.azure; + const storageKey = azure ? azure.key : undefined; + + const curConfig = deepClone(config); + if (storageKey) { + const introspectionCloned = curConfig.introspection; + const azureCloned = introspectionCloned?.azure; + if (azureCloned) { + azureCloned.key = storageKey; + } } - - await getSubscriptionId(curConfig); + return curConfig; }; /** * Handles the interactive mode of the command. */ export const handleInteractiveMode = async (): Promise => { - const curConfig = deepClone(getConfig()); + const curConfig = cloneConfig(); const answer = await prompt(curConfig); curConfig.azure_devops!.org = answer.azdo_org_name; curConfig.azure_devops!.project = answer.azdo_project_name; diff --git a/src/lib/i18n.json b/src/lib/i18n.json index 91a3b81fa..be0634372 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -10,6 +10,7 @@ "servicePrincipalTenantId": "Enter Service Principal Tenant Id", "setupIntrospectionConfig": "Do you want to setup introspection configuration?", "storageAccountName": "Enter storage account name", - "storageTableName": "Enter storage table name" + "storageTableName": "Enter storage table name", + "storageKey": "Enter storage key" } } diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 4b6089fca..885093e0a 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -142,3 +142,16 @@ export const azureStorageTableName = ( validate: validator.validateStorageTableName }; }; + +export const azureStorageKey = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + mask: "*", + message: `${i18n.prompt.storageKey}\n`, + name: "azdo_storage_key", + type: "password", + validate: validator.validatePassword + }; +}; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index 0baa95f43..0b3cacacd 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -11,6 +11,7 @@ import { validateAccessToken, validateForNonEmptyValue, validateOrgName, + validatePassword, validatePrereqs, validateProjectName, validateServicePrincipalId, @@ -278,3 +279,14 @@ describe("test validateStorageTableName test", () => { expect(validateStorageTableName("abc123456")).toBeTruthy(); }); }); + +describe("test validatePassword test", () => { + it("sanity test", () => { + expect(validatePassword("")).toBe("Must enter a value."); + expect(validatePassword("1234567")).toBe( + "Must be more than 8 characters long." + ); + expect(validatePassword("abcd1234")).toBeTruthy(); + expect(validatePassword("abcdefg123456678")).toBeTruthy(); + }); +}); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 1ed14cd06..223d999d5 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -116,6 +116,21 @@ export const isDashHex = (value: string): boolean => { return !!value.match(/^[a-f0-9-]+$/); }; +/** + * Returns true if password is proper. Typical password validation + * + * @param value password + */ +export const validatePassword = (value: string): string | boolean => { + if (!hasValue(value)) { + return "Must enter a value."; + } + if (value.length < 8) { + return "Must be more than 8 characters long."; + } + return true; +}; + /** * Returns true if project name is proper. * From ddc7968ecb7d1c5b47ebc76c4bf70391186e54b3 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Mon, 16 Mar 2020 14:09:02 -0700 Subject: [PATCH 06/18] Update init.ts --- src/commands/init.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/init.ts b/src/commands/init.ts index f60ec0f9a..cb149172d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -142,6 +142,9 @@ export const handleIntrospectionInteractive = async ( azure.table_name = ans.azdo_storage_table_name; }; +/** + * Clone configuration and keep the storage key function + */ export const cloneConfig = (): ConfigYaml => { const config = getConfig(); const introspection = config.introspection; From b523f5df64a7f381b98d39c198bf525274ce20c1 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Mon, 16 Mar 2020 18:23:49 -0700 Subject: [PATCH 07/18] added more prompts --- src/commands/init.test.ts | 6 +++++- src/commands/init.ts | 5 ++++- src/lib/i18n.json | 3 ++- src/lib/promptBuilder.ts | 20 ++++++++++++++++---- src/lib/validator.test.ts | 15 +++++++++++++++ src/lib/validator.ts | 19 ++++++++++++++++++- 6 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index c8c954ef8..8dfbb696a 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -244,11 +244,15 @@ const testHandleIntrospectionInteractive = async ( } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ azdo_storage_account_name: "storagetest", - azdo_storage_table_name: "storagetabletest" + azdo_storage_table_name: "storagetabletest", + azdo_storage_partition_key: "test1234key", + azdo_storage_repo_access_key: "repokey" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); expect(config.introspection?.azure?.table_name).toBe("storagetabletest"); + expect(config.introspection?.azure?.partition_key).toBe("test1234key"); + expect(config.introspection?.azure?.source_repo_access_token).toBe("repokey"); }; describe("test handleIntrospectionInteractive function", () => { diff --git a/src/commands/init.ts b/src/commands/init.ts index cb149172d..5b163f236 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -136,10 +136,13 @@ export const handleIntrospectionInteractive = async ( const ans = await inquirer.prompt([ promptBuilder.azureStorageAccountName(azure.account_name), promptBuilder.azureStorageTableName(azure.table_name), - promptBuilder.azureStorageKey(undefined) + promptBuilder.azureStoragePartitionKey(azure.partition_key), + promptBuilder.azureStorageRepoAccessKey(azure.source_repo_access_token) ]); azure.account_name = ans.azdo_storage_account_name; azure.table_name = ans.azdo_storage_table_name; + azure.partition_key = ans.azdo_storage_partition_key; + azure.source_repo_access_token = ans.azdo_storage_repo_access_key; }; /** diff --git a/src/lib/i18n.json b/src/lib/i18n.json index be0634372..bb5b97385 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -11,6 +11,7 @@ "setupIntrospectionConfig": "Do you want to setup introspection configuration?", "storageAccountName": "Enter storage account name", "storageTableName": "Enter storage table name", - "storageKey": "Enter storage key" + "storagePartitionKey": "Enter storage partition key", + "storageRepoAccessKey": "Enter storage repo access key" } } diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 885093e0a..3d13509f8 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -143,15 +143,27 @@ export const azureStorageTableName = ( }; }; -export const azureStorageKey = ( +export const azureStoragePartitionKey = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.storagePartitionKey}\n`, + name: "azdo_storage_partition_key", + type: "input", + validate: validator.validateStoragePartitionKey + }; +}; + +export const azureStorageRepoAccessKey = ( defaultValue?: string | undefined ): QuestionCollection => { return { default: defaultValue, mask: "*", - message: `${i18n.prompt.storageKey}\n`, - name: "azdo_storage_key", + message: `${i18n.prompt.storageRepoAccessKey}\n`, + name: "azdo_storage_repo_access_key", type: "password", - validate: validator.validatePassword + validate: validator.validateStoragePartitionKey }; }; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index 0b3cacacd..d848aaaac 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -18,6 +18,7 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateStorageAccountName, + validateStoragePartitionKey, validateStorageTableName, validateSubscriptionId } from "./validator"; @@ -290,3 +291,17 @@ describe("test validatePassword test", () => { expect(validatePassword("abcdefg123456678")).toBeTruthy(); }); }); + +describe("test validateStoragePartitionKey test", () => { + it("sanity test", () => { + expect(validateStoragePartitionKey("")).toBe( + "Must enter a storage partition key." + ); + ["abc\\", "abc/", "abc?", "abc#"].forEach(s => { + expect(validateStoragePartitionKey(s)).toBe( + "The value for storage partition key is invalid. /, \\, # and ? characters are not allowed." + ); + }); + expect(validateStoragePartitionKey("abcdefg123456678")).toBeTruthy(); + }); +}); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 223d999d5..51d8f1ffb 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -278,7 +278,7 @@ export const validateStorageAccountName = (value: string): string | boolean => { /** * Returns true if storage table name is valid. * - * @param value storage table name . + * @param value storage table name. */ export const validateStorageTableName = (value: string): string | boolean => { if (!hasValue(value)) { @@ -292,3 +292,20 @@ export const validateStorageTableName = (value: string): string | boolean => { } return true; }; + +/** + * Returns true if storage partition key is valid. + * + * @param value storage partition key. + */ +export const validateStoragePartitionKey = ( + value: string +): string | boolean => { + if (!hasValue(value)) { + return "Must enter a storage partition key."; + } + if (value.match(/[/\\#?]/)) { + return "The value for storage partition key is invalid. /, \\, # and ? characters are not allowed."; + } + return true; +}; From beb6d8bb69baed68f45fbacfaca52cd23d360476 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 17 Mar 2020 21:36:25 -0700 Subject: [PATCH 08/18] fix lint --- src/commands/deployment/dashboard.test.ts | 16 ++-- src/commands/deployment/validate.test.ts | 24 ++--- src/commands/init.test.ts | 36 ++++---- src/commands/init.ts | 10 +- src/commands/setup.test.ts | 12 +-- src/commands/setup.ts | 16 ++-- src/lib/pipelines/serviceEndpoint.test.ts | 106 +++++++++++----------- src/lib/pipelines/variableGroup.test.ts | 48 +++++----- src/lib/setup/prompt.test.ts | 36 ++++---- 9 files changed, 152 insertions(+), 152 deletions(-) diff --git a/src/commands/deployment/dashboard.test.ts b/src/commands/deployment/dashboard.test.ts index 5bfb6c5c0..7a8d64c8e 100644 --- a/src/commands/deployment/dashboard.test.ts +++ b/src/commands/deployment/dashboard.test.ts @@ -30,18 +30,18 @@ afterAll(() => { const mockConfig = (): void => { (Config as jest.Mock).mockReturnValueOnce({ - azure_devops: { - access_token: uuid(), + "azure_devops": { + "access_token": uuid(), org: uuid(), project: uuid() }, introspection: { azure: { - account_name: uuid(), + "account_name": uuid(), key: uuid(), - partition_key: uuid(), - source_repo_access_token: "test_token", - table_name: uuid() + "partition_key": uuid(), + "source_repo_access_token": "test_token", + "table_name": uuid() } } }); @@ -208,8 +208,8 @@ describe("Fallback to azure devops access token", () => { describe("Extract manifest repository information", () => { test("Manifest repository information is successfully extracted", () => { (Config as jest.Mock).mockReturnValue({ - azure_devops: { - manifest_repository: + "azure_devops": { + "manifest_repository": "https://dev.azure.com/bhnook/fabrikam/_git/materialized" } }); diff --git a/src/commands/deployment/validate.test.ts b/src/commands/deployment/validate.test.ts index 9059041d0..5e93c9c24 100644 --- a/src/commands/deployment/validate.test.ts +++ b/src/commands/deployment/validate.test.ts @@ -134,16 +134,16 @@ afterAll(() => { describe("Validate deployment configuration", () => { test("valid deployment configuration", async () => { const config: ConfigYaml = { - azure_devops: { + "azure_devops": { org: uuid(), project: uuid() }, introspection: { azure: { - account_name: uuid(), + "account_name": uuid(), key: Promise.resolve(uuid()), - partition_key: uuid(), - table_name: uuid() + "partition_key": uuid(), + "table_name": uuid() } } }; @@ -210,7 +210,7 @@ describe("test runSelfTest function", () => { introspection: { azure: { key: Promise.resolve(uuid()), - table_name: undefined + "table_name": undefined } } }; @@ -229,7 +229,7 @@ describe("test runSelfTest function", () => { introspection: { azure: { key: Promise.resolve(uuid()), - table_name: undefined + "table_name": undefined } } }; @@ -244,7 +244,7 @@ describe("test runSelfTest function", () => { introspection: { azure: { key: Promise.resolve(uuid()), - table_name: undefined + "table_name": undefined } } }; @@ -277,7 +277,7 @@ describe("Validate missing deployment.storage configuration", () => { const config: ConfigYaml = { introspection: { azure: { - account_name: undefined, + "account_name": undefined, key: Promise.resolve(uuid()) } } @@ -292,7 +292,7 @@ describe("Validate missing deployment.storage configuration", () => { introspection: { azure: { key: Promise.resolve(uuid()), - table_name: undefined + "table_name": undefined } } }; @@ -306,7 +306,7 @@ describe("Validate missing deployment.storage configuration", () => { introspection: { azure: { key: Promise.resolve(uuid()), - partition_key: undefined + "partition_key": undefined } } }; @@ -343,7 +343,7 @@ describe("Validate missing deployment.pipeline configuration", () => { describe("Validate missing deployment.pipeline configuration", () => { test("missing deployment.pipeline.org configuration", async () => { const config: ConfigYaml = { - azure_devops: { + "azure_devops": { org: undefined }, introspection: { @@ -359,7 +359,7 @@ describe("Validate missing deployment.pipeline configuration", () => { describe("Validate missing deployment.pipeline configuration", () => { test("missing deployment.pipeline.project configuration", async () => { const config: ConfigYaml = { - azure_devops: { + "azure_devops": { org: "org", project: undefined }, diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 67ac5b5b8..bc23b1c6f 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -107,8 +107,8 @@ describe("Test execute function", () => { describe("test getConfig function", () => { it("with configuration file", () => { const mockedValues = { - azure_devops: { - access_token: "access_token", + "azure_devops": { + "access_token": "access_token", org: "org", project: "project" } @@ -125,8 +125,8 @@ describe("test getConfig function", () => { }); const cfg = getConfig(); expect(cfg).toStrictEqual({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } @@ -142,7 +142,7 @@ describe("test validatePersonalAccessToken function", () => { }) ); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -154,7 +154,7 @@ describe("test validatePersonalAccessToken function", () => { .spyOn(axios, "get") .mockReturnValueOnce(Promise.reject(new Error("fake"))); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -167,8 +167,8 @@ const testHandleInteractiveModeFunc = async ( verified: boolean ): Promise => { jest.spyOn(init, "getConfig").mockReturnValueOnce({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" }, @@ -181,9 +181,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - azdo_org_name: "org_name", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org_name", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }); jest @@ -216,9 +216,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -242,10 +242,10 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - azdo_storage_account_name: "storagetest", - azdo_storage_table_name: "storagetabletest", - azdo_storage_partition_key: "test1234key", - azdo_storage_repo_access_key: "repokey" + "azdo_storage_account_name": "storagetest", + "azdo_storage_table_name": "storagetabletest", + "azdo_storage_partition_key": "test1234key", + "azdo_storage_repo_access_key": "repokey" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); diff --git a/src/commands/init.ts b/src/commands/init.ts index 644514817..e4bab0807 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -55,9 +55,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - azdo_org_name: answers.azdo_org_name as string, - azdo_pat: answers.azdo_pat as string, - azdo_project_name: answers.azdo_project_name as string, + "azdo_org_name": answers.azdo_org_name as string, + "azdo_pat": answers.azdo_pat as string, + "azdo_project_name": answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -73,8 +73,8 @@ export const getConfig = (): ConfigYaml => { } catch (_) { logger.info("current config is not found."); return { - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index af21c810f..36167f3e0 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -37,7 +37,7 @@ describe("test createSPKConfig function", () => { createSPKConfig(mockRequestContext); const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ - access_token: "pat", + "access_token": "pat", org: "orgname", project: "project" }); @@ -56,16 +56,16 @@ describe("test createSPKConfig function", () => { const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ - access_token: "pat", + "access_token": "pat", org: "orgname", project: "project" }); expect(data.introspection).toStrictEqual({ azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId + "service_principal_id": rc.servicePrincipalId, + "service_principal_secret": rc.servicePrincipalPassword, + "subscription_id": rc.subscriptionId, + "tenant_id": rc.servicePrincipalTenantId } }); }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 20ccdf497..ae742f57d 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -40,23 +40,23 @@ interface APIError { export const createSPKConfig = (rc: RequestContext): void => { const data = rc.toCreateAppRepo ? { - azure_devops: { - access_token: rc.accessToken, + "azure_devops": { + "access_token": rc.accessToken, org: rc.orgName, project: rc.projectName }, introspection: { azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId + "service_principal_id": rc.servicePrincipalId, + "service_principal_secret": rc.servicePrincipalPassword, + "subscription_id": rc.subscriptionId, + "tenant_id": rc.servicePrincipalTenantId } } } : { - azure_devops: { - access_token: rc.accessToken, + "azure_devops": { + "access_token": rc.accessToken, org: rc.orgName, project: rc.projectName } diff --git a/src/lib/pipelines/serviceEndpoint.test.ts b/src/lib/pipelines/serviceEndpoint.test.ts index 58d3824f7..374d530cc 100644 --- a/src/lib/pipelines/serviceEndpoint.test.ts +++ b/src/lib/pipelines/serviceEndpoint.test.ts @@ -26,22 +26,22 @@ const servicePrincipalSecret: string = uuid(); const tenantId: string = uuid(); const mockedConfig = { - azure_devops: { + "azure_devops": { orrg: uuid() } }; const mockedYaml = { description: "mydesc", - key_vault_provider: { + "key_vault_provider": { name: "vault", - service_endpoint: { + "service_endpoint": { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId } }, name: "myvg" @@ -112,11 +112,11 @@ const mockedInvalidServiceEndpointResponse = { const createServiceEndpointInput: ServiceEndpointData = { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId }; beforeAll(() => { @@ -131,14 +131,14 @@ describe("Validate service endpoint parameters creation", () => { test("valid service endpoint params", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId } } }); @@ -167,13 +167,13 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without the name", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "key_vault_provider": { + "service_endpoint": { + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId } } }); @@ -192,13 +192,13 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without service principal id", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: serviceEndpointName, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId } } }); @@ -217,13 +217,13 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without service principal secret", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - subscription_id: subscriptionId, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "subscription_id": subscriptionId, + "subscription_name": subscriptionName, + "tenant_id": tenantId } } }); @@ -242,13 +242,13 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without subscription id", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_name: subscriptionName, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_name": subscriptionName, + "tenant_id": tenantId } } }); @@ -267,13 +267,13 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without subscription name", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: serviceEndpointName, - service_principal_id: servicePrincipalId, - service_principal_secret: servicePrincipalSecret, - subscription_id: subscriptionId, - tenant_id: tenantId + "service_principal_id": servicePrincipalId, + "service_principal_secret": servicePrincipalSecret, + "subscription_id": subscriptionId, + "tenant_id": tenantId } } }); @@ -292,8 +292,8 @@ describe("Validate service endpoint parameters creation", () => { test("should fail creating service endpoint params without entire section", () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: {} + "key_vault_provider": { + "service_endpoint": {} } }); const input = readYaml(""); diff --git a/src/lib/pipelines/variableGroup.test.ts b/src/lib/pipelines/variableGroup.test.ts index e58f7c654..22d21ad85 100644 --- a/src/lib/pipelines/variableGroup.test.ts +++ b/src/lib/pipelines/variableGroup.test.ts @@ -118,14 +118,14 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should fail when key vault name is not set for variable group", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: "sename", - service_principal_id: "id", - service_principal_secret: "secret", - subscription_id: "id", - subscription_name: "subname", - tenant_id: "tid" + "service_principal_id": "id", + "service_principal_secret": "secret", + "subscription_id": "id", + "subscription_name": "subname", + "tenant_id": "tid" } }, name: "myvg", @@ -152,9 +152,9 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should fail when service endpoint data is not set for variable group", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "myvg desc", - key_vault_provider: { + "key_vault_provider": { name: "mykv", - service_endpoint: {} + "service_endpoint": {} }, name: "myvg", variables: [ @@ -179,15 +179,15 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should pass when variable group data is valid", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "myvg desc", - key_vault_provider: { + "key_vault_provider": { name: "mykv", - service_endpoint: { + "service_endpoint": { name: "epname", - service_principal_id: "pricid", - service_principal_secret: "princsecret", - subscription_id: "subid", - subscription_name: "subname", - tenant_id: "tenid" + "service_principal_id": "pricid", + "service_principal_secret": "princsecret", + "subscription_id": "subid", + "subscription_name": "subname", + "tenant_id": "tenid" } }, name: "myvg", @@ -254,15 +254,15 @@ describe("doAddVariableGroup", () => { test("should pass when variable group with key vault data is set", async () => { (readYaml as jest.Mock).mockReturnValue({ description: uuid(), - key_vault_data: { + "key_vault_data": { name: "mykv", - service_endpoint: { + "service_endpoint": { name: "epname", - service_principal_id: "pricid", - service_principal_secret: "princsecret", - subscription_id: "subid", - subscription_name: "subname", - tenant_id: "tenid" + "service_principal_id": "pricid", + "service_principal_secret": "princsecret", + "subscription_id": "subid", + "subscription_name": "subname", + "tenant_id": "tenid" } }, name: uuid(), @@ -311,7 +311,7 @@ describe("authorizeAccessToAllPipelines", () => { }); test("should pass when valid variable group is passed", async () => { jest.spyOn(config, "Config").mockReturnValueOnce({ - azure_devops: { + "azure_devops": { project: "test" } }); diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 3450402ee..a1589c47b 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -12,10 +12,10 @@ import * as subscriptionService from "../azure/subscriptionService"; describe("test prompt function", () => { it("positive test: No App Creation", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: false + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": false }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); const ans = await prompt(); @@ -29,14 +29,14 @@ describe("test prompt function", () => { }); it("positive test: create SP", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: true + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - create_service_principal: true + "create_service_principal": true }); jest .spyOn(servicePrincipalService, "createWithAzCLI") @@ -69,19 +69,19 @@ describe("test prompt function", () => { }); it("positive test: no create SP", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: true + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - create_service_principal: false + "create_service_principal": false }); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_sp_id: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", - az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", - az_sp_tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47" + "az_sp_id": "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", + "az_sp_password": "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", + "az_sp_tenant": "72f988bf-86f1-41af-91ab-2d7cd011db47" }); jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ { @@ -273,7 +273,7 @@ describe("test promptForSubscriptionId function", () => { } ]); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_subscription: "subscription2" + "az_subscription": "subscription2" }); const mockRc: RequestContext = { accessToken: "pat", From ea10db9a06342a4b63354b7b9aafe1d074f49cc3 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 17 Mar 2020 21:57:00 -0700 Subject: [PATCH 09/18] fix lint --- src/commands/setup.test.ts | 12 +++---- src/commands/setup.ts | 16 ++++----- src/lib/pipelines/variableGroup.test.ts | 48 ++++++++++++------------- src/lib/setup/prompt.test.ts | 42 +++++++++++----------- 4 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index ffe77f17f..12dbd7177 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -37,7 +37,7 @@ describe("test createSPKConfig function", () => { createSPKConfig(mockRequestContext); const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ - access_token: "pat", + "access_token": "pat", org: "orgname", project: "project" }); @@ -56,16 +56,16 @@ describe("test createSPKConfig function", () => { const data = readYaml(tmpFile); expect(data.azure_devops).toStrictEqual({ - access_token: "pat", + "access_token": "pat", org: "orgname", project: "project" }); expect(data.introspection).toStrictEqual({ azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId + "service_principal_id": rc.servicePrincipalId, + "service_principal_secret": rc.servicePrincipalPassword, + "subscription_id": rc.subscriptionId, + "tenant_id": rc.servicePrincipalTenantId } }); }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index a5bebaaf8..a93bec0d6 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -45,23 +45,23 @@ interface APIError { export const createSPKConfig = (rc: RequestContext): void => { const data = rc.toCreateAppRepo ? { - azure_devops: { - access_token: rc.accessToken, + "azure_devops": { + "access_token": rc.accessToken, org: rc.orgName, project: rc.projectName }, introspection: { azure: { - service_principal_id: rc.servicePrincipalId, - service_principal_secret: rc.servicePrincipalPassword, - subscription_id: rc.subscriptionId, - tenant_id: rc.servicePrincipalTenantId + "service_principal_id": rc.servicePrincipalId, + "service_principal_secret": rc.servicePrincipalPassword, + "subscription_id": rc.subscriptionId, + "tenant_id": rc.servicePrincipalTenantId } } } : { - azure_devops: { - access_token: rc.accessToken, + "azure_devops": { + "access_token": rc.accessToken, org: rc.orgName, project: rc.projectName } diff --git a/src/lib/pipelines/variableGroup.test.ts b/src/lib/pipelines/variableGroup.test.ts index 22801277d..af97c5b02 100644 --- a/src/lib/pipelines/variableGroup.test.ts +++ b/src/lib/pipelines/variableGroup.test.ts @@ -119,14 +119,14 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should fail when key vault name is not set for variable group", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "mydesc", - key_vault_provider: { - service_endpoint: { + "key_vault_provider": { + "service_endpoint": { name: "sename", - service_principal_id: "id", - service_principal_secret: "secret", - subscription_id: "id", - subscription_name: "subname", - tenant_id: "tid" + "service_principal_id": "id", + "service_principal_secret": "secret", + "subscription_id": "id", + "subscription_name": "subname", + "tenant_id": "tid" } }, name: "myvg", @@ -153,9 +153,9 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should fail when service endpoint data is not set for variable group", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "myvg desc", - key_vault_provider: { + "key_vault_provider": { name: "mykv", - service_endpoint: {} + "service_endpoint": {} }, name: "myvg", variables: [ @@ -180,15 +180,15 @@ describe("addVariableGroupWithKeyVaultMap", () => { test("should pass when variable group data is valid", async () => { (readYaml as jest.Mock).mockReturnValue({ description: "myvg desc", - key_vault_provider: { + "key_vault_provider": { name: "mykv", - service_endpoint: { + "service_endpoint": { name: "epname", - service_principal_id: "pricid", - service_principal_secret: "princsecret", - subscription_id: "subid", - subscription_name: "subname", - tenant_id: "tenid" + "service_principal_id": "pricid", + "service_principal_secret": "princsecret", + "subscription_id": "subid", + "subscription_name": "subname", + "tenant_id": "tenid" } }, name: "myvg", @@ -255,15 +255,15 @@ describe("doAddVariableGroup", () => { test("should pass when variable group with key vault data is set", async () => { (readYaml as jest.Mock).mockReturnValue({ description: uuid(), - key_vault_data: { + "key_vault_data": { name: "mykv", - service_endpoint: { + "service_endpoint": { name: "epname", - service_principal_id: "pricid", - service_principal_secret: "princsecret", - subscription_id: "subid", - subscription_name: "subname", - tenant_id: "tenid" + "service_principal_id": "pricid", + "service_principal_secret": "princsecret", + "subscription_id": "subid", + "subscription_name": "subname", + "tenant_id": "tenid" } }, name: uuid(), @@ -312,7 +312,7 @@ describe("authorizeAccessToAllPipelines", () => { }); test("should pass when valid variable group is passed", async () => { jest.spyOn(config, "Config").mockReturnValueOnce({ - azure_devops: { + "azure_devops": { project: "test" } }); diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index 80266d7b7..1348d3e33 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -17,10 +17,10 @@ import * as subscriptionService from "../azure/subscriptionService"; describe("test prompt function", () => { it("positive test: No App Creation", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: false + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": false }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); const ans = await prompt(); @@ -34,17 +34,17 @@ describe("test prompt function", () => { }); it("positive test: create SP", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: true + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - create_service_principal: true + "create_service_principal": true }); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - acr_name: "testACR" + "acr_name": "testACR" }); jest @@ -79,22 +79,22 @@ describe("test prompt function", () => { }); it("positive test: no create SP", async () => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", - create_app_repo: true + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", + "create_app_repo": true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - create_service_principal: false + "create_service_principal": false }); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_sp_id: "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", - az_sp_password: "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", - az_sp_tenant: "72f988bf-86f1-41af-91ab-2d7cd011db47" + "az_sp_id": "b510c1ff-358c-4ed4-96c8-eb23f42bb65b", + "az_sp_password": "a510c1ff-358c-4ed4-96c8-eb23f42bbc5b", + "az_sp_tenant": "72f988bf-86f1-41af-91ab-2d7cd011db47" }); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - acr_name: "testACR" + "acr_name": "testACR" }); jest.spyOn(subscriptionService, "getSubscriptions").mockResolvedValueOnce([ { @@ -287,7 +287,7 @@ describe("test promptForSubscriptionId function", () => { } ]); jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - az_subscription: "subscription2" + "az_subscription": "subscription2" }); const mockRc: RequestContext = { accessToken: "pat", @@ -309,7 +309,7 @@ describe("test promptForACRName function", () => { workspace: WORKSPACE }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - acr_name: "testACR" + "acr_name": "testACR" }); await promptForACRName(mockRc); expect(mockRc.acrName).toBe("testACR"); From 0f33b165c435448d49c22a4cdce70eed276e7079 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 18 Mar 2020 10:11:39 -0700 Subject: [PATCH 10/18] wrapping up --- src/commands/init.test.ts | 48 +++++++++++++++++++++++---------------- src/commands/init.ts | 42 ++++++++++++++-------------------- src/lib/i18n.json | 3 ++- src/lib/promptBuilder.ts | 12 ++++++++++ src/lib/validator.test.ts | 25 ++++++++++++++++++++ src/lib/validator.ts | 26 +++++++++++++++++++++ 6 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index bc23b1c6f..1d109cf6e 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -107,8 +107,8 @@ describe("Test execute function", () => { describe("test getConfig function", () => { it("with configuration file", () => { const mockedValues = { - "azure_devops": { - "access_token": "access_token", + azure_devops: { + access_token: "access_token", org: "org", project: "project" } @@ -125,8 +125,8 @@ describe("test getConfig function", () => { }); const cfg = getConfig(); expect(cfg).toStrictEqual({ - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" } @@ -142,7 +142,7 @@ describe("test validatePersonalAccessToken function", () => { }) ); const result = await validatePersonalAccessToken({ - "access_token": "token", + access_token: "token", org: "org", project: "project" }); @@ -154,7 +154,7 @@ describe("test validatePersonalAccessToken function", () => { .spyOn(axios, "get") .mockReturnValueOnce(Promise.reject(new Error("fake"))); const result = await validatePersonalAccessToken({ - "access_token": "token", + access_token: "token", org: "org", project: "project" }); @@ -167,8 +167,8 @@ const testHandleInteractiveModeFunc = async ( verified: boolean ): Promise => { jest.spyOn(init, "getConfig").mockReturnValueOnce({ - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" }, @@ -181,9 +181,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - "azdo_org_name": "org_name", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org_name", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }); jest @@ -216,9 +216,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - "azdo_org_name": "org", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -229,7 +229,8 @@ describe("test prompt function", () => { }); const testHandleIntrospectionInteractive = async ( - withIntrospection = false + withIntrospection = false, + withKeyVault = false ): Promise => { const config: ConfigYaml = {}; if (!withIntrospection) { @@ -242,21 +243,30 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - "azdo_storage_account_name": "storagetest", - "azdo_storage_table_name": "storagetabletest", - "azdo_storage_partition_key": "test1234key", - "azdo_storage_repo_access_key": "repokey" + azdo_storage_account_name: "storagetest", + azdo_storage_table_name: "storagetabletest", + azdo_storage_partition_key: "test1234key", + azdo_storage_repo_access_key: "repokey", + azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); expect(config.introspection?.azure?.table_name).toBe("storagetabletest"); expect(config.introspection?.azure?.partition_key).toBe("test1234key"); expect(config.introspection?.azure?.source_repo_access_token).toBe("repokey"); + + if (withKeyVault) { + expect(config.key_vault_name).toBe("keyvault"); + } else { + expect(config.key_vault_name).toBeUndefined(); + } }; describe("test handleIntrospectionInteractive function", () => { it("positive test", async () => { await testHandleIntrospectionInteractive(false); await testHandleIntrospectionInteractive(true); + await testHandleIntrospectionInteractive(false, true); + await testHandleIntrospectionInteractive(true, true); }); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index e4bab0807..03f00ddba 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -55,9 +55,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - "azdo_org_name": answers.azdo_org_name as string, - "azdo_pat": answers.azdo_pat as string, - "azdo_project_name": answers.azdo_project_name as string, + azdo_org_name: answers.azdo_org_name as string, + azdo_pat: answers.azdo_pat as string, + azdo_project_name: answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -73,8 +73,8 @@ export const getConfig = (): ConfigYaml => { } catch (_) { logger.info("current config is not found."); return { - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" } @@ -141,39 +141,31 @@ export const handleIntrospectionInteractive = async ( promptBuilder.azureStorageAccountName(azure.account_name), promptBuilder.azureStorageTableName(azure.table_name), promptBuilder.azureStoragePartitionKey(azure.partition_key), - promptBuilder.azureStorageRepoAccessKey(azure.source_repo_access_token) + promptBuilder.azureStorageRepoAccessKey(azure.source_repo_access_token), + promptBuilder.azureKeyVaultName(curConfig.key_vault_name) ]); azure["account_name"] = ans.azdo_storage_account_name; azure["table_name"] = ans.azdo_storage_table_name; azure["partition_key"] = ans.azdo_storage_partition_key; azure["source_repo_access_token"] = ans.azdo_storage_repo_access_key; -}; - -/** - * Clone configuration and keep the storage key function - */ -export const cloneConfig = (): ConfigYaml => { - const config = getConfig(); - const introspection = config.introspection; - const azure = introspection?.azure; - const storageKey = azure ? azure.key : undefined; - const curConfig = deepClone(config); - if (storageKey) { - const introspectionCloned = curConfig.introspection; - const azureCloned = introspectionCloned?.azure; - if (azureCloned) { - azureCloned.key = storageKey; - } + const keyVaultName = ans.azdo_storage_key_vault_name.trim(); + if (keyVaultName) { + curConfig["key_vault_name"] = keyVaultName; + } else { + delete curConfig["key_vault_name"]; } - return curConfig; }; /** * Handles the interactive mode of the command. */ export const handleInteractiveMode = async (): Promise => { - const curConfig = cloneConfig(); + const conf = getConfig(); + if (conf.introspection && conf.introspection.azure) { + delete conf.introspection.azure.key; + } + const curConfig = deepClone(conf); const answer = await prompt(curConfig); curConfig["azure_devops"] = curConfig.azure_devops || {}; diff --git a/src/lib/i18n.json b/src/lib/i18n.json index bb5b97385..3aa9b8fb6 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -12,6 +12,7 @@ "storageAccountName": "Enter storage account name", "storageTableName": "Enter storage table name", "storagePartitionKey": "Enter storage partition key", - "storageRepoAccessKey": "Enter storage repo access key" + "storageRepoAccessKey": "Enter storage repo access key", + "storageKeVaultName": "Enter key vault name (have the value as empty and hit enter key to skip)" } } diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 3d13509f8..8b94d801a 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -167,3 +167,15 @@ export const azureStorageRepoAccessKey = ( validate: validator.validateStoragePartitionKey }; }; + +export const azureKeyVaultName = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + message: `${i18n.prompt.storageKeVaultName}\n`, + name: "azdo_storage_key_vault_name", + type: "input", + validate: validator.validateStorageKeVaultName + }; +}; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index 2b0fc34a6..6e933b971 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -19,6 +19,7 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateStorageAccountName, + validateStorageKeVaultName, validateStoragePartitionKey, validateStorageTableName, validateSubscriptionId @@ -320,3 +321,27 @@ describe("test validateACRName function", () => { expect(validateACRName("abc12356")).toBeTruthy(); }); }); + +describe("test validateStorageKeVaultName function", () => { + it("sanity test", () => { + expect(validateStorageKeVaultName("ab*")).toBe( + "The value for Key Value Name is invalid." + ); + expect(validateStorageKeVaultName("1abc0")).toBe( + "Key Value Name must start with a letter." + ); + expect(validateStorageKeVaultName("abc0-")).toBe( + "Key Value Name must end with letter or digit." + ); + expect(validateStorageKeVaultName("a--b")).toBe( + "Key Value Name cannot contain consecutive hyphens." + ); + expect(validateStorageKeVaultName("ab")).toBe( + "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long." + ); + expect(validateStorageKeVaultName("a12345678".repeat(3))).toBe( + "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long." + ); + expect(validateStorageKeVaultName("abc-12356")).toBeTruthy(); + }); +}); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index b3f5878cc..eea696d6a 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -120,6 +120,10 @@ export const isAlphaNumeric = (value: string): boolean => { return !!value.match(/^[a-zA-Z0-9]+$/); }; +export const isDashAlphaNumeric = (value: string): boolean => { + return !!value.match(/^[a-zA-Z0-9-]+$/); +}; + /** * Returns true if password is proper. Typical password validation * @@ -328,3 +332,25 @@ export const validateACRName = (value: string): string | boolean => { } return true; }; + +export const validateStorageKeVaultName = (value: string): string | boolean => { + if (!hasValue(value)) { + return true; // optional + } + if (!isDashAlphaNumeric(value)) { + return "The value for Key Value Name is invalid."; + } + if (!value.match(/^[a-zA-Z]/)) { + return "Key Value Name must start with a letter."; + } + if (!value.match(/[a-zA-Z0-9]$/)) { + return "Key Value Name must end with letter or digit."; + } + if (value.indexOf("--") !== -1) { + return "Key Value Name cannot contain consecutive hyphens."; + } + if (value.length < 3 || value.length > 24) { + return "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long."; + } + return true; +}; From 3da6f86c4b9a79090e49f2bee19312e13de80bb2 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 18 Mar 2020 10:18:47 -0700 Subject: [PATCH 11/18] fix lint and update doc --- .../interactiveModeForIntrospectionConfig.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/technical-docs/designs/initialization/interactiveModeForIntrospectionConfig.md b/technical-docs/designs/initialization/interactiveModeForIntrospectionConfig.md index 38d130db9..74c454d10 100644 --- a/technical-docs/designs/initialization/interactiveModeForIntrospectionConfig.md +++ b/technical-docs/designs/initialization/interactiveModeForIntrospectionConfig.md @@ -12,10 +12,11 @@ Authors: --- -| Revision | Date | Author | Remarks | -| -------: | ------------ | ----------- | --------------------------------- | -| 0.1 | Mar-16, 2020 | Dennis Seah | Initial Draft | -| 0.2 | Mar-17, 2020 | Dennis Seah | Having key vault name as optional | +| Revision | Date | Author | Remarks | +| -------: | ------------ | ----------- | ----------------------------------------- | +| 0.1 | Mar-16, 2020 | Dennis Seah | Initial Draft | +| 0.2 | Mar-17, 2020 | Dennis Seah | Having key vault name as optional | +| 1.0 | Mar-18, 2020 | Dennis Seah | Reviewed and this doc is committed to git | ## 1. Overview From 8ac94b458b70ea48bb095e03dfb5bf3fd1e67737 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 18 Mar 2020 10:30:07 -0700 Subject: [PATCH 12/18] fix lint --- package.json | 11 ----------- src/commands/init.test.ts | 38 +++++++++++++++++++------------------- src/commands/init.ts | 10 +++++----- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 94f80ea72..6a276d31a 100644 --- a/package.json +++ b/package.json @@ -55,17 +55,6 @@ "proseWrap": "always", "quote-props": "preserve" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.{ts,tsx,js,jsx,css,json,md}": [ - "prettier --write", - "git add" - ] - }, "dependencies": { "@azure/arm-containerregistry": "^7.0.0", "@azure/arm-resources": "^2.1.0", diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 1d109cf6e..b4eaaf168 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -107,8 +107,8 @@ describe("Test execute function", () => { describe("test getConfig function", () => { it("with configuration file", () => { const mockedValues = { - azure_devops: { - access_token: "access_token", + "azure_devops": { + "access_token": "access_token", org: "org", project: "project" } @@ -125,8 +125,8 @@ describe("test getConfig function", () => { }); const cfg = getConfig(); expect(cfg).toStrictEqual({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } @@ -142,7 +142,7 @@ describe("test validatePersonalAccessToken function", () => { }) ); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -154,7 +154,7 @@ describe("test validatePersonalAccessToken function", () => { .spyOn(axios, "get") .mockReturnValueOnce(Promise.reject(new Error("fake"))); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -167,8 +167,8 @@ const testHandleInteractiveModeFunc = async ( verified: boolean ): Promise => { jest.spyOn(init, "getConfig").mockReturnValueOnce({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" }, @@ -181,9 +181,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - azdo_org_name: "org_name", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org_name", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }); jest @@ -216,9 +216,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -243,11 +243,11 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - azdo_storage_account_name: "storagetest", - azdo_storage_table_name: "storagetabletest", - azdo_storage_partition_key: "test1234key", - azdo_storage_repo_access_key: "repokey", - azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" + "azdo_storage_account_name": "storagetest", + "azdo_storage_table_name": "storagetabletest", + "azdo_storage_partition_key": "test1234key", + "azdo_storage_repo_access_key": "repokey", + "azdo_storage_key_vault_name": withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); diff --git a/src/commands/init.ts b/src/commands/init.ts index 03f00ddba..52fc3bf8e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -55,9 +55,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - azdo_org_name: answers.azdo_org_name as string, - azdo_pat: answers.azdo_pat as string, - azdo_project_name: answers.azdo_project_name as string, + "azdo_org_name": answers.azdo_org_name as string, + "azdo_pat": answers.azdo_pat as string, + "azdo_project_name": answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -73,8 +73,8 @@ export const getConfig = (): ConfigYaml => { } catch (_) { logger.info("current config is not found."); return { - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } From 19ba0176f55298e26c42cc0156aed77085f75268 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 18 Mar 2020 10:30:17 -0700 Subject: [PATCH 13/18] Revert "fix lint" This reverts commit 8ac94b458b70ea48bb095e03dfb5bf3fd1e67737. --- package.json | 11 +++++++++++ src/commands/init.test.ts | 38 +++++++++++++++++++------------------- src/commands/init.ts | 10 +++++----- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 6a276d31a..94f80ea72 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,17 @@ "proseWrap": "always", "quote-props": "preserve" }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{ts,tsx,js,jsx,css,json,md}": [ + "prettier --write", + "git add" + ] + }, "dependencies": { "@azure/arm-containerregistry": "^7.0.0", "@azure/arm-resources": "^2.1.0", diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index b4eaaf168..1d109cf6e 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -107,8 +107,8 @@ describe("Test execute function", () => { describe("test getConfig function", () => { it("with configuration file", () => { const mockedValues = { - "azure_devops": { - "access_token": "access_token", + azure_devops: { + access_token: "access_token", org: "org", project: "project" } @@ -125,8 +125,8 @@ describe("test getConfig function", () => { }); const cfg = getConfig(); expect(cfg).toStrictEqual({ - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" } @@ -142,7 +142,7 @@ describe("test validatePersonalAccessToken function", () => { }) ); const result = await validatePersonalAccessToken({ - "access_token": "token", + access_token: "token", org: "org", project: "project" }); @@ -154,7 +154,7 @@ describe("test validatePersonalAccessToken function", () => { .spyOn(axios, "get") .mockReturnValueOnce(Promise.reject(new Error("fake"))); const result = await validatePersonalAccessToken({ - "access_token": "token", + access_token: "token", org: "org", project: "project" }); @@ -167,8 +167,8 @@ const testHandleInteractiveModeFunc = async ( verified: boolean ): Promise => { jest.spyOn(init, "getConfig").mockReturnValueOnce({ - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" }, @@ -181,9 +181,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - "azdo_org_name": "org_name", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org_name", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }); jest @@ -216,9 +216,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - "azdo_org_name": "org", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -243,11 +243,11 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - "azdo_storage_account_name": "storagetest", - "azdo_storage_table_name": "storagetabletest", - "azdo_storage_partition_key": "test1234key", - "azdo_storage_repo_access_key": "repokey", - "azdo_storage_key_vault_name": withKeyVault ? "keyvault" : "" + azdo_storage_account_name: "storagetest", + azdo_storage_table_name: "storagetabletest", + azdo_storage_partition_key: "test1234key", + azdo_storage_repo_access_key: "repokey", + azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); diff --git a/src/commands/init.ts b/src/commands/init.ts index 52fc3bf8e..03f00ddba 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -55,9 +55,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - "azdo_org_name": answers.azdo_org_name as string, - "azdo_pat": answers.azdo_pat as string, - "azdo_project_name": answers.azdo_project_name as string, + azdo_org_name: answers.azdo_org_name as string, + azdo_pat: answers.azdo_pat as string, + azdo_project_name: answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -73,8 +73,8 @@ export const getConfig = (): ConfigYaml => { } catch (_) { logger.info("current config is not found."); return { - "azure_devops": { - "access_token": "", + azure_devops: { + access_token: "", org: "", project: "" } From e8fad7c443c55382db6ea746ba938b21ab2a0fe2 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 18 Mar 2020 10:31:05 -0700 Subject: [PATCH 14/18] fix lint --- src/commands/init.test.ts | 38 +++++++++++++++++++------------------- src/commands/init.ts | 10 +++++----- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 1d109cf6e..b4eaaf168 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -107,8 +107,8 @@ describe("Test execute function", () => { describe("test getConfig function", () => { it("with configuration file", () => { const mockedValues = { - azure_devops: { - access_token: "access_token", + "azure_devops": { + "access_token": "access_token", org: "org", project: "project" } @@ -125,8 +125,8 @@ describe("test getConfig function", () => { }); const cfg = getConfig(); expect(cfg).toStrictEqual({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } @@ -142,7 +142,7 @@ describe("test validatePersonalAccessToken function", () => { }) ); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -154,7 +154,7 @@ describe("test validatePersonalAccessToken function", () => { .spyOn(axios, "get") .mockReturnValueOnce(Promise.reject(new Error("fake"))); const result = await validatePersonalAccessToken({ - access_token: "token", + "access_token": "token", org: "org", project: "project" }); @@ -167,8 +167,8 @@ const testHandleInteractiveModeFunc = async ( verified: boolean ): Promise => { jest.spyOn(init, "getConfig").mockReturnValueOnce({ - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" }, @@ -181,9 +181,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - azdo_org_name: "org_name", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org_name", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }); jest @@ -216,9 +216,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - azdo_org_name: "org", - azdo_pat: "pat", - azdo_project_name: "project", + "azdo_org_name": "org", + "azdo_pat": "pat", + "azdo_project_name": "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -243,11 +243,11 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - azdo_storage_account_name: "storagetest", - azdo_storage_table_name: "storagetabletest", - azdo_storage_partition_key: "test1234key", - azdo_storage_repo_access_key: "repokey", - azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" + "azdo_storage_account_name": "storagetest", + "azdo_storage_table_name": "storagetabletest", + "azdo_storage_partition_key": "test1234key", + "azdo_storage_repo_access_key": "repokey", + "azdo_storage_key_vault_name": withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); diff --git a/src/commands/init.ts b/src/commands/init.ts index 03f00ddba..52fc3bf8e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -55,9 +55,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - azdo_org_name: answers.azdo_org_name as string, - azdo_pat: answers.azdo_pat as string, - azdo_project_name: answers.azdo_project_name as string, + "azdo_org_name": answers.azdo_org_name as string, + "azdo_pat": answers.azdo_pat as string, + "azdo_project_name": answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -73,8 +73,8 @@ export const getConfig = (): ConfigYaml => { } catch (_) { logger.info("current config is not found."); return { - azure_devops: { - access_token: "", + "azure_devops": { + "access_token": "", org: "", project: "" } From 562f991dfb1353415ec5ba0213352a3cd9db92a7 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Thu, 19 Mar 2020 08:39:37 -0700 Subject: [PATCH 15/18] Update init.md --- src/commands/init.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commands/init.md b/src/commands/init.md index cf5b8fd42..95664b20e 100644 --- a/src/commands/init.md +++ b/src/commands/init.md @@ -15,6 +15,12 @@ shall be no default values. These are the questions 1. Organization Name of Azure dev-op account 2. Project Name of Azure dev-op account 3. Personal Access Token (guides) +4. Would like to have introspection configuration setup? If yes + 1. Storage Account Name + 1. Storage Table Name + 1. Storage Partition Key + 1. Azure Storage Repo AccessKey + 1. Key Vault Name (optional) This tool shall verify these values by making an API call to Azure dev-op. They shall be written to `config.yaml` regardless the verification is successful or From 67d686f3bf59f4f1f227dd55c7af75c8d281df1f Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Thu, 19 Mar 2020 10:00:59 -0700 Subject: [PATCH 16/18] incorporated review feedback --- src/commands/init.md | 1 - src/commands/init.test.ts | 22 ++++++++++------------ src/commands/init.ts | 8 +++----- src/lib/promptBuilder.ts | 15 +-------------- src/lib/validator.test.ts | 18 +++++++++--------- src/lib/validator.ts | 4 +++- 6 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/commands/init.md b/src/commands/init.md index 95664b20e..dea61b02a 100644 --- a/src/commands/init.md +++ b/src/commands/init.md @@ -19,7 +19,6 @@ shall be no default values. These are the questions 1. Storage Account Name 1. Storage Table Name 1. Storage Partition Key - 1. Azure Storage Repo AccessKey 1. Key Vault Name (optional) This tool shall verify these values by making an API call to Azure dev-op. They diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index c82aa0073..19b9e5072 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -182,9 +182,9 @@ const testHandleInteractiveModeFunc = async ( } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ - "azdo_org_name": "org_name", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org_name", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }); jest @@ -217,9 +217,9 @@ describe("test handleInteractiveMode function", () => { describe("test prompt function", () => { it("positive test", async done => { const answers = { - "azdo_org_name": "org", - "azdo_pat": "pat", - "azdo_project_name": "project", + azdo_org_name: "org", + azdo_pat: "pat", + azdo_project_name: "project", toSetupIntrospectionConfig: true }; jest.spyOn(inquirer, "prompt").mockResolvedValueOnce(answers); @@ -244,17 +244,15 @@ const testHandleIntrospectionInteractive = async ( }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - "azdo_storage_account_name": "storagetest", - "azdo_storage_table_name": "storagetabletest", - "azdo_storage_partition_key": "test1234key", - "azdo_storage_repo_access_key": "repokey", - "azdo_storage_key_vault_name": withKeyVault ? "keyvault" : "" + azdo_storage_account_name: "storagetest", + azdo_storage_table_name: "storagetabletest", + azdo_storage_partition_key: "test1234key", + azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); expect(config.introspection?.azure?.table_name).toBe("storagetabletest"); expect(config.introspection?.azure?.partition_key).toBe("test1234key"); - expect(config.introspection?.azure?.source_repo_access_token).toBe("repokey"); if (withKeyVault) { expect(config.key_vault_name).toBe("keyvault"); diff --git a/src/commands/init.ts b/src/commands/init.ts index fc7b1cea2..e282100ce 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -56,9 +56,9 @@ export const prompt = async (curConfig: ConfigYaml): Promise => { ]; const answers = await inquirer.prompt(questions); return { - "azdo_org_name": answers.azdo_org_name as string, - "azdo_pat": answers.azdo_pat as string, - "azdo_project_name": answers.azdo_project_name as string, + azdo_org_name: answers.azdo_org_name as string, + azdo_pat: answers.azdo_pat as string, + azdo_project_name: answers.azdo_project_name as string, toSetupIntrospectionConfig: answers.toSetupIntrospectionConfig }; }; @@ -142,13 +142,11 @@ export const handleIntrospectionInteractive = async ( promptBuilder.azureStorageAccountName(azure.account_name), promptBuilder.azureStorageTableName(azure.table_name), promptBuilder.azureStoragePartitionKey(azure.partition_key), - promptBuilder.azureStorageRepoAccessKey(azure.source_repo_access_token), promptBuilder.azureKeyVaultName(curConfig.key_vault_name) ]); azure["account_name"] = ans.azdo_storage_account_name; azure["table_name"] = ans.azdo_storage_table_name; azure["partition_key"] = ans.azdo_storage_partition_key; - azure["source_repo_access_token"] = ans.azdo_storage_repo_access_key; const keyVaultName = ans.azdo_storage_key_vault_name.trim(); if (keyVaultName) { diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 8b94d801a..7e1e4ee07 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -155,19 +155,6 @@ export const azureStoragePartitionKey = ( }; }; -export const azureStorageRepoAccessKey = ( - defaultValue?: string | undefined -): QuestionCollection => { - return { - default: defaultValue, - mask: "*", - message: `${i18n.prompt.storageRepoAccessKey}\n`, - name: "azdo_storage_repo_access_key", - type: "password", - validate: validator.validateStoragePartitionKey - }; -}; - export const azureKeyVaultName = ( defaultValue?: string | undefined ): QuestionCollection => { @@ -176,6 +163,6 @@ export const azureKeyVaultName = ( message: `${i18n.prompt.storageKeVaultName}\n`, name: "azdo_storage_key_vault_name", type: "input", - validate: validator.validateStorageKeVaultName + validate: validator.validateStorageKeyVaultName }; }; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index 6e933b971..2bedd1055 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -19,7 +19,7 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateStorageAccountName, - validateStorageKeVaultName, + validateStorageKeyVaultName, validateStoragePartitionKey, validateStorageTableName, validateSubscriptionId @@ -322,26 +322,26 @@ describe("test validateACRName function", () => { }); }); -describe("test validateStorageKeVaultName function", () => { +describe("test validateStorageKeyVaultName function", () => { it("sanity test", () => { - expect(validateStorageKeVaultName("ab*")).toBe( + expect(validateStorageKeyVaultName("ab*")).toBe( "The value for Key Value Name is invalid." ); - expect(validateStorageKeVaultName("1abc0")).toBe( + expect(validateStorageKeyVaultName("1abc0")).toBe( "Key Value Name must start with a letter." ); - expect(validateStorageKeVaultName("abc0-")).toBe( + expect(validateStorageKeyVaultName("abc0-")).toBe( "Key Value Name must end with letter or digit." ); - expect(validateStorageKeVaultName("a--b")).toBe( + expect(validateStorageKeyVaultName("a--b")).toBe( "Key Value Name cannot contain consecutive hyphens." ); - expect(validateStorageKeVaultName("ab")).toBe( + expect(validateStorageKeyVaultName("ab")).toBe( "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long." ); - expect(validateStorageKeVaultName("a12345678".repeat(3))).toBe( + expect(validateStorageKeyVaultName("a12345678".repeat(3))).toBe( "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long." ); - expect(validateStorageKeVaultName("abc-12356")).toBeTruthy(); + expect(validateStorageKeyVaultName("abc-12356")).toBeTruthy(); }); }); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index eea696d6a..5a724faec 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -333,7 +333,9 @@ export const validateACRName = (value: string): string | boolean => { return true; }; -export const validateStorageKeVaultName = (value: string): string | boolean => { +export const validateStorageKeyVaultName = ( + value: string +): string | boolean => { if (!hasValue(value)) { return true; // optional } From c79899ab05b113888a763d899a48aa70b598ebfc Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Thu, 19 Mar 2020 11:08:01 -0700 Subject: [PATCH 17/18] incorporate review feedback --- src/commands/deployment/onboard.test.ts | 2 +- src/commands/deployment/onboard.ts | 2 +- src/commands/init.md | 1 + src/commands/init.test.ts | 17 +++++------ src/commands/init.ts | 15 ++-------- src/config.ts | 40 +------------------------ src/lib/i18n.json | 2 +- src/lib/promptBuilder.ts | 13 ++++++++ src/lib/validator.test.ts | 10 +++++++ src/lib/validator.ts | 7 +++++ src/types.d.ts | 2 +- 11 files changed, 46 insertions(+), 65 deletions(-) diff --git a/src/commands/deployment/onboard.test.ts b/src/commands/deployment/onboard.test.ts index ed7b873ac..80be04b97 100644 --- a/src/commands/deployment/onboard.test.ts +++ b/src/commands/deployment/onboard.test.ts @@ -78,7 +78,7 @@ const testPopulatedVal = ( const configYaml: ConfigYaml = { introspection: { azure: { - key: Promise.resolve("key") + key: "key" } } }; diff --git a/src/commands/deployment/onboard.ts b/src/commands/deployment/onboard.ts index a1d7a2797..6db253139 100644 --- a/src/commands/deployment/onboard.ts +++ b/src/commands/deployment/onboard.ts @@ -40,7 +40,7 @@ export interface CommandOptions { */ export const populateValues = (opts: CommandOptions): CommandOptions => { const config = Config(); - const { azure } = config.introspection!; + const azure = config.introspection ? config.introspection.azure : undefined; opts.storageAccountName = opts.storageAccountName || azure?.account_name || undefined; diff --git a/src/commands/init.md b/src/commands/init.md index dea61b02a..f5d8ba46a 100644 --- a/src/commands/init.md +++ b/src/commands/init.md @@ -19,6 +19,7 @@ shall be no default values. These are the questions 1. Storage Account Name 1. Storage Table Name 1. Storage Partition Key + 1. Storage Access Key 1. Key Vault Name (optional) This tool shall verify these values by making an API call to Azure dev-op. They diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 19b9e5072..80891b3ed 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -162,6 +162,9 @@ describe("test validatePersonalAccessToken function", () => { expect(result).toBe(false); done(); }); + it("negative test, no values in parameter", async () => { + await expect(validatePersonalAccessToken({})).rejects.toThrow(); + }); }); const testHandleInteractiveModeFunc = async ( @@ -174,11 +177,7 @@ const testHandleInteractiveModeFunc = async ( project: "" }, introspection: { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }) - } + azure: {} } }); jest.spyOn(init, "prompt").mockResolvedValueOnce({ @@ -236,23 +235,21 @@ const testHandleIntrospectionInteractive = async ( const config: ConfigYaml = {}; if (!withIntrospection) { config["introspection"] = { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }) - } + azure: {} }; } jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ azdo_storage_account_name: "storagetest", azdo_storage_table_name: "storagetabletest", azdo_storage_partition_key: "test1234key", + azdo_storage_access_key: "accessKey", azdo_storage_key_vault_name: withKeyVault ? "keyvault" : "" }); await handleIntrospectionInteractive(config); expect(config.introspection?.azure?.account_name).toBe("storagetest"); expect(config.introspection?.azure?.table_name).toBe("storagetabletest"); expect(config.introspection?.azure?.partition_key).toBe("test1234key"); + expect(config.introspection?.azure?.key).toBe("accessKey"); if (withKeyVault) { expect(config.key_vault_name).toBe("keyvault"); diff --git a/src/commands/init.ts b/src/commands/init.ts index e282100ce..2f7860a84 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -127,11 +127,7 @@ export const handleIntrospectionInteractive = async ( ): Promise => { if (!isIntrospectionAzureDefined(curConfig)) { curConfig.introspection = { - azure: { - key: new Promise(resolve => { - resolve(undefined); - }) - } + azure: {} }; } @@ -142,11 +138,13 @@ export const handleIntrospectionInteractive = async ( promptBuilder.azureStorageAccountName(azure.account_name), promptBuilder.azureStorageTableName(azure.table_name), promptBuilder.azureStoragePartitionKey(azure.partition_key), + promptBuilder.azureStorageAccessKey(azure.key), promptBuilder.azureKeyVaultName(curConfig.key_vault_name) ]); azure["account_name"] = ans.azdo_storage_account_name; azure["table_name"] = ans.azdo_storage_table_name; azure["partition_key"] = ans.azdo_storage_partition_key; + azure.key = ans.azdo_storage_access_key; const keyVaultName = ans.azdo_storage_key_vault_name.trim(); if (keyVaultName) { @@ -176,13 +174,6 @@ export const handleInteractiveMode = async (): Promise => { await handleIntrospectionInteractive(curConfig); } - if (curConfig.introspection && curConfig.introspection.azure) { - // key is needed to create the azure object in - // introspction object however it causes - // problem when we do `yaml.safeDump` because - // key is a function. - delete curConfig.introspection.azure.key; - } const data = yaml.safeDump(curConfig); fs.writeFileSync(defaultConfigFile(), data); diff --git a/src/config.ts b/src/config.ts index f735d9a4f..9eb98aeb4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,6 @@ import yaml from "js-yaml"; import * as os from "os"; import path from "path"; import { writeVersion } from "./lib/fileutils"; -import { getSecret } from "./lib/azure/keyvault"; import { logger } from "./logger"; import { AzurePipelinesYaml, @@ -86,29 +85,6 @@ export const loadConfigurationFromLocalEnv = (configObj: T): T => { return configObj; }; -const getKeyVaultSecret = async ( - keyVaultName: string | undefined, - storageAccountName: string | undefined -): Promise => { - logger.debug(`Fetching key from key vault`); - let keyVaultKey: string | undefined; - - // fetch storage access key from key vault when it is configured - if ( - keyVaultName !== undefined && - keyVaultName !== null && - storageAccountName !== undefined - ) { - keyVaultKey = await getSecret(keyVaultName, `${storageAccountName}Key`); - } - - if (keyVaultKey === undefined) { - keyVaultKey = await spkConfig.introspection?.azure?.key; - } - - return keyVaultKey; -}; - /** * Fetches the absolute default directory of the spk global config */ @@ -168,21 +144,7 @@ export const Config = (): ConfigYaml => { } } - const introspectionAzure = { - ...spkConfig.introspection?.azure, - get key(): Promise { - const accountName = spkConfig.introspection?.azure?.account_name; - return getKeyVaultSecret(spkConfig.key_vault_name, accountName); - } - }; - - return { - ...spkConfig, - introspection: { - ...spkConfig.introspection, - azure: introspectionAzure - } - }; + return spkConfig; }; /** diff --git a/src/lib/i18n.json b/src/lib/i18n.json index 3aa9b8fb6..f39b6e95c 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -12,7 +12,7 @@ "storageAccountName": "Enter storage account name", "storageTableName": "Enter storage table name", "storagePartitionKey": "Enter storage partition key", - "storageRepoAccessKey": "Enter storage repo access key", + "storageAccessKey": "Enter storage access key", "storageKeVaultName": "Enter key vault name (have the value as empty and hit enter key to skip)" } } diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts index 7e1e4ee07..d47715c33 100644 --- a/src/lib/promptBuilder.ts +++ b/src/lib/promptBuilder.ts @@ -166,3 +166,16 @@ export const azureKeyVaultName = ( validate: validator.validateStorageKeyVaultName }; }; + +export const azureStorageAccessKey = ( + defaultValue?: string | undefined +): QuestionCollection => { + return { + default: defaultValue, + mask: "*", + message: `${i18n.prompt.storageAccessKey}\n`, + name: "azdo_storage_access_key", + type: "password", + validate: validator.validateStorageAccessKey + }; +}; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index 2bedd1055..ee1ea9d50 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -19,6 +19,7 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateStorageAccountName, + validateStorageAccessKey, validateStorageKeyVaultName, validateStoragePartitionKey, validateStorageTableName, @@ -345,3 +346,12 @@ describe("test validateStorageKeyVaultName function", () => { expect(validateStorageKeyVaultName("abc-12356")).toBeTruthy(); }); }); + +describe("test validateStorageAccessKey function", () => { + it("sanity test", () => { + expect(validateStorageAccessKey("")).toBe( + "Must enter an Storage Access Key." + ); + expect(validateStorageAccessKey("abc-12356")).toBeTruthy(); + }); +}); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 5a724faec..8d7667f5d 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -356,3 +356,10 @@ export const validateStorageKeyVaultName = ( } return true; }; + +export const validateStorageAccessKey = (value: string): string | boolean => { + if (!hasValue(value)) { + return "Must enter an Storage Access Key."; + } + return true; +}; diff --git a/src/types.d.ts b/src/types.d.ts index ff6d103af..05dec7543 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -197,7 +197,7 @@ export interface ConfigYaml { account_name?: string; table_name?: string; partition_key?: string; - key: Promise; + key?: string; source_repo_access_token?: string; service_principal_id?: string; service_principal_secret?: string; From 6b0bc2efbee978f00d101caee679b6be9509fd9d Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Thu, 19 Mar 2020 11:23:45 -0700 Subject: [PATCH 18/18] Update validate.test.ts --- src/commands/deployment/validate.test.ts | 30 +++++++++--------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/commands/deployment/validate.test.ts b/src/commands/deployment/validate.test.ts index 3de9a76ba..6ea33ded3 100644 --- a/src/commands/deployment/validate.test.ts +++ b/src/commands/deployment/validate.test.ts @@ -142,7 +142,7 @@ describe("Validate deployment configuration", () => { introspection: { azure: { account_name: uuid(), - key: Promise.resolve(uuid()), + key: uuid(), partition_key: uuid(), table_name: uuid() } @@ -210,7 +210,7 @@ describe("test runSelfTest function", () => { const config: ConfigYaml = { introspection: { azure: { - key: Promise.resolve(uuid()), + key: uuid(), table_name: undefined } } @@ -229,7 +229,7 @@ describe("test runSelfTest function", () => { const config: ConfigYaml = { introspection: { azure: { - key: Promise.resolve(uuid()), + key: uuid(), table_name: undefined } } @@ -244,7 +244,7 @@ describe("test runSelfTest function", () => { const config: ConfigYaml = { introspection: { azure: { - key: Promise.resolve(uuid()), + key: uuid(), table_name: undefined } } @@ -279,7 +279,7 @@ describe("Validate missing deployment.storage configuration", () => { introspection: { azure: { account_name: undefined, - key: Promise.resolve(uuid()) + key: uuid() } } }; @@ -292,7 +292,7 @@ describe("Validate missing deployment.storage configuration", () => { const config: ConfigYaml = { introspection: { azure: { - key: Promise.resolve(uuid()), + key: uuid(), table_name: undefined } } @@ -306,7 +306,7 @@ describe("Validate missing deployment.storage configuration", () => { const config: ConfigYaml = { introspection: { azure: { - key: Promise.resolve(uuid()), + key: uuid(), partition_key: undefined } } @@ -319,9 +319,7 @@ describe("Validate missing deployment.storage configuration", () => { test("missing deployment.storage.key configuration", async () => { const config: ConfigYaml = { introspection: { - azure: { - key: Promise.resolve(undefined) - } + azure: {} } }; await expect(isValidConfig(config)).rejects.toThrow(); @@ -332,9 +330,7 @@ describe("Validate missing deployment.pipeline configuration", () => { test("missing deployment.pipeline configuration", async () => { const config: ConfigYaml = { introspection: { - azure: { - key: Promise.resolve(undefined) - } + azure: {} } }; await expect(isValidConfig(config)).rejects.toThrow(); @@ -348,9 +344,7 @@ describe("Validate missing deployment.pipeline configuration", () => { org: undefined }, introspection: { - azure: { - key: Promise.resolve(undefined) - } + azure: {} } }; await expect(isValidConfig(config)).rejects.toThrow(); @@ -365,9 +359,7 @@ describe("Validate missing deployment.pipeline configuration", () => { project: undefined }, introspection: { - azure: { - key: Promise.resolve(undefined) - } + azure: {} } }; await expect(isValidConfig(config)).rejects.toThrow();