From e24b5589e7f99d6152ff1f492d808147e3a0b1a7 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 1 Apr 2020 12:41:31 -0700 Subject: [PATCH 1/2] [FEATURE] Adding error code to validation lib --- docs/commands/data.json | 4 +- src/commands/deployment/onboard.test.ts | 5 +- src/commands/hld/pipeline.test.ts | 3 +- src/commands/setup.ts | 6 +- src/lib/errorBuilder.ts | 53 ++++---- src/lib/i18n.json | 43 ++++++- src/lib/setup/prompt.ts | 70 +++-------- src/lib/validator.test.ts | 138 +++++++++++++-------- src/lib/validator.ts | 155 +++++++++++++++++------- 9 files changed, 302 insertions(+), 175 deletions(-) diff --git a/docs/commands/data.json b/docs/commands/data.json index 79b206aa9..67938bb06 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -14,7 +14,7 @@ "defaultValue": false } ], - "markdown": "This command creates a configuration file, `config.yaml` in a folder `.spk`\nunder your home directory. There are two options for creating this file\n\n1. an interactive mode where you have to answer a few questions; and\n2. you provide a `yaml` file and this `yaml` will be copied to the target\n location.\n\n## Interactive mode\n\nThe command line tool attempts to read `config.yaml` in a folder `.spk` under\nyour home directory. Configuration values shall be read from it if it exists.\nAnd these values shall be default values for the questions. Otherwise, there\nshall be no default values. These are the questions\n\n1. Organization Name of Azure dev-op account\n2. Project Name of Azure dev-op account\n3. Personal Access Token (guides)\n4. Would like to have introspection configuration setup? If yes\n 1. Storage Account Name\n 1. Storage Table Name\n 1. Storage Partition Key\n 1. Storage Access Key\n 1. Key Vault Name (optional)\n\nThis tool shall verify these values by making an API call to Azure dev-op. They\nshall be written to `config.yaml` regardless the verification is successful or\nnot.\n\n> Note: In the event that you do not have internet connection, this verification\n> shall not be possible\n\n## Example\n\n```\nspk init --interactive\n```\n\nor\n\n```\nspk init --file myConfig.yaml\n```\n" + "markdown": "This command creates a configuration file, `config.yaml` in a folder `.spk`\nunder your home directory. There are two options for creating this file\n\n1. an interactive mode where you have to answer a few questions; and\n2. you provide a `yaml` file and this `yaml` will be copied to the target\n location.\n\n## Interactive mode\n\nThe command line tool attempts to read `config.yaml` in a folder `.spk` under\nyour home directory. Configuration values shall be read from it if it exists.\nAnd these values shall be default values for the questions. Otherwise, there\nshall be no default values. These are the questions\n\n1. Organization Name of Azure dev-op account\n2. Project Name of Azure dev-op account\n3. Personal Access Token (guides)\n4. Would like to have introspection configuration setup? If yes\n 1. Storage Account Name\n 1. Storage Table Name\n 1. Storage Partition Key\n 1. Storage Access Key\n\nThis tool shall verify these values by making an API call to Azure dev-op. They\nshall be written to `config.yaml` regardless the verification is successful or\nnot.\n\n> Note: In the event that you do not have internet connection, this verification\n> shall not be possible\n\n## Example\n\n```\nspk init --interactive\n```\n\nor\n\n```\nspk init --file myConfig.yaml\n```\n" }, "setup": { "command": "setup", @@ -674,4 +674,4 @@ } ] } -} +} \ No newline at end of file diff --git a/src/commands/deployment/onboard.test.ts b/src/commands/deployment/onboard.test.ts index 3ff873c71..c65e5c2d1 100644 --- a/src/commands/deployment/onboard.test.ts +++ b/src/commands/deployment/onboard.test.ts @@ -24,6 +24,7 @@ import { validateValues, } from "./onboard"; import * as onboardImpl from "./onboard"; +import { getErrorMessage } from "../../lib/errorBuilder"; beforeAll(() => { enableVerboseLogging(); @@ -237,9 +238,7 @@ describe("test validateValues function", () => { vals.storageAccountName = "#123"; expect(() => { validateValues(vals); - }).toThrow( - "The value for storage account name is invalid. Lowercase letters and numbers are allowed." - ); + }).toThrow(getErrorMessage("validation-err-storage-account-name-invalid")); }); it("[-ve]: invalid storageTableName value", () => { const vals = getMockedValues(); diff --git a/src/commands/hld/pipeline.test.ts b/src/commands/hld/pipeline.test.ts index 698643b89..86e6476c2 100644 --- a/src/commands/hld/pipeline.test.ts +++ b/src/commands/hld/pipeline.test.ts @@ -3,6 +3,7 @@ import * as azdo from "../../lib/azdoClient"; import { BUILD_SCRIPT_URL } from "../../lib/constants"; import { getRepositoryName } from "../../lib/gitutils"; import { disableVerboseLogging, enableVerboseLogging } from "../../logger"; +import { getErrorMessage } from "../../lib/errorBuilder"; jest.mock("../../lib/pipelines/pipelines"); import { @@ -88,7 +89,7 @@ const orgNameTest = (hasVal: boolean): void => { if (hasVal) { expect(() => populateValues(data)).toThrow( - "Organization names must start with a letter or number, followed by letters, numbers or hyphens, and must end with a letter or number." + getErrorMessage("validation-err-org-name") ); } else { expect(() => populateValues(data)).toThrow( diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 20209a23b..5e21a4b2a 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -40,6 +40,8 @@ import { create as createSetupLog } from "../lib/setup/setupLog"; import { logger } from "../logger"; import decorator from "./setup.decorator.json"; import { createStorage } from "../lib/setup/azureStorage"; +import { build as buildError, log as logError } from "../lib/errorBuilder"; +import { errorStatusCode } from "../lib/errorStatusCode"; import { ConfigYaml } from "../types"; interface CommandOptions { @@ -211,6 +213,8 @@ export const execute = async ( createSetupLog(rc); await exitFn(0); } catch (err) { + logError(buildError(errorStatusCode.CMD_EXE_ERR, "setup-cmd-failed", err)); + const msg = getErrorMessage(requestContext, err); // requestContext will not be created if input validation failed @@ -218,8 +222,6 @@ export const execute = async ( requestContext.error = msg; } createSetupLog(requestContext); - - logger.error(msg); await exitFn(1); } }; diff --git a/src/lib/errorBuilder.ts b/src/lib/errorBuilder.ts index 69521376a..9499edf26 100644 --- a/src/lib/errorBuilder.ts +++ b/src/lib/errorBuilder.ts @@ -8,6 +8,36 @@ interface ErrorParam { errorKey: string; values: string[]; } + +/** + * Returns error message + * + * @param errorInstance Error instance + */ +export const getErrorMessage = (errorInstance: string | ErrorParam): string => { + let key = ""; + let values: string[] | undefined = undefined; + + if (typeof errorInstance === "string") { + key = errorInstance; + } else { + key = errorInstance.errorKey; + values = errorInstance.values; + } + + // if key is found in i18n json + if (key in errors) { + let results = errors[key]; + if (values) { + values.forEach((val, i) => { + const re = new RegExp("\\{" + i + "}", "g"); + results = results.replace(re, val); + }); + } + return `${key}: ${results}`; + } + return key; +}; class ErrorChain extends Error { errorCode: number; details: string | undefined; @@ -25,28 +55,7 @@ class ErrorChain extends Error { * @param errorInstance Error instance */ getErrorMessage(errorInstance: string | ErrorParam): string { - let key = ""; - let values: string[] | undefined = undefined; - - if (typeof errorInstance === "string") { - key = errorInstance; - } else { - key = errorInstance.errorKey; - values = errorInstance.values; - } - - // if key is found in i18n json - if (key in errors) { - let results = errors[key]; - if (values) { - values.forEach((val, i) => { - const re = new RegExp("\\{" + i + "}", "g"); - results = results.replace(re, val); - }); - } - return `${key}: ${results}`; - } - return key; + return getErrorMessage(errorInstance); } /** * Generates error messages and have them in messages array. diff --git a/src/lib/i18n.json b/src/lib/i18n.json index 9f39d2ece..5c95c9ec6 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -16,6 +16,8 @@ "storageKeVaultName": "Enter key vault name (have the value as empty and hit enter key to skip)" }, "errors": { + "setup-cmd-failed": "Setup command was not successfully executed.", + "hld-init-cmd-failed": "Hld init command was not successfully executed.", "hld-init-cmd-project-path-missing": "Value for project path was not provided. Provide it.", @@ -69,6 +71,45 @@ "deployment-table-update-hld-manifest-pipeline-failed": "Could not update HLD to manifest pipeline.", "deployment-table-update-manifest-commit-id-failed": "Could not update manifest commit Id.", - "deployment-table-update-manifest-commit-id-failed-no-generation": "No manifest generation found to update manifest commit {0}." + "deployment-table-update-manifest-commit-id-failed-no-generation": "No manifest generation found to update manifest commit {0}.", + + "validation-err-org-name-missing": "Organization name was missing. Provide it.", + "validation-err-org-name": "Organization name must start with a letter or number, followed by letters, numbers or hyphens, and must end with a letter or number.", + "validation-err-password-missing": "Password was missing. Provide it.", + "validation-err-password-too-short": "Password was too short, it must be more than 8 characters long. Reenter password.", + "validation-err-project-name-missing": "Project name was missing. Provide it.", + "validation-err-project-name-too-long": "Project name was too long, it cannot be longer than 64 characters.", + "validation-err-project-name-begin-underscore": "Project name was invalid as it cannot begin with an underscore", + "validation-err-project-name-period": "Project name was invalid as it cannot begin or end with a period", + "validation-err-project-name-special-char": "Project name can't contain special characters, such as / : \\ ~ & % ; @ ' \" ? < > | # $ * } { , + = [ ]", + "validation-err-personal-access-token-missing": "Personal access token was missing. Provide it", + + "validation-err-service-principal-id-missing": "Service Principal Id was missing. Provide it.", + "validation-err-service-principal-id-invalid": "Service Principal Id was invalid. Check and re-enter.", + "validation-err-service-principal-pwd-missing": "Service Principal Password was missing. Provide it.", + "validation-err-service-principal-pwd-invalid": "Service Principal Password was invalid. Check and re-enter.", + "validation-err-service-principal-tenant-id-missing": "Service Principal Tenant Id was missing. Provide it.", + "validation-err-service-principal-tenant-id-invalid": "Service Principal Tenant Id was invalid. Check and re-enter.", + + "validation-err-subscription-id-missing": "Subscription Id was missing. Provide it.", + "validation-err-subscription-id-invalid": "Subscription Id was invalid. Check and re-enter.", + + "validation-err-storage-account-name-missing": "Storage Account Name was missing. Provide it.", + "validation-err-storage-account-name-invalid": "Storage Account Name was invalid. Only lowercase letters and numbers are allowed.", + "validation-err-storage-account-name-length": "Storage Account Name was invalid. It has to be between 3 and 24 characters long.", + "validation-err-storage-table-name-missing": "Storage Table Name was missing. Provide it.", + "validation-err-storage-table-name-invalid": "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet.", + "validation-err-storage-table-name-length": "The value for storage table name is invalid. It has to be between 3 and 63 characters long.", + "validation-err-storage-partition-key-missing": "Storage Partition Key was missing. Provide it.", + "validation-err-storage-partition-key-invalid": "The value for storage partition key is invalid. /, \\, # and ? characters are not allowed.", + "validation-err-acr-missing": "Azure Container Registry Name was missing. Provide it.", + "validation-err-acr-invalid": "The value for Azure Container Registry name was invalid. It has to be alphanumeric.", + "validation-err-acr-length": "The value for Azure Container Registry name was invalid as it has to be between 5 and 50 characters long.", + "validation-err-storage-key-vault-invalid": "Storage Key Vault was invalid as it cannot only has dash and alphanumeric characters.", + "validation-err-storage-key-vault-start-letter": "Storage Key Vault was invalid as it must start with a letter.", + "validation-err-storage-key-vault-end-char": "Storage Key Vault was invalid as it must end with letter or digit.", + "validation-err-storage-key-vault-hyphen": "Storage Key Vault was invalid as it cannot contain consecutive hyphens.", + "validation-err-storage-key-vault-length": "Storage Key Vault was invalid as it has to be between 3 and 24 characters long.", + "validation-err-storage-access-key-missing": "Storage Access Key was missing. Provide it." } } diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index c29ada23d..6be676357 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -2,16 +2,16 @@ import fs from "fs"; import inquirer from "inquirer"; import * as promptBuilder from "../promptBuilder"; import { - validateAccessToken, + validateAccessTokenThrowable, validateACRName, - validateOrgName, - validateProjectName, - validateServicePrincipalId, - validateServicePrincipalPassword, - validateServicePrincipalTenantId, - validateSubscriptionId, - validateStorageAccountName, - validateStorageTableName, + validateOrgNameThrowable, + validateProjectNameThrowable, + validateServicePrincipalIdThrowable, + validateServicePrincipalPasswordThrowable, + validateServicePrincipalTenantIdThrowable, + validateSubscriptionIdThrowable, + validateStorageAccountNameThrowable, + validateStorageTableNameThrowable, } from "../validator"; import { ACR_NAME, @@ -182,24 +182,12 @@ export const validationServicePrincipalInfoFromFile = ( // file needs to contain sp information if user // choose not to create SP if (!rc.toCreateSP) { - const vSPId = validateServicePrincipalId(map.az_sp_id); - if (typeof vSPId === "string") { - throw new Error(vSPId); - } - const vSPPassword = validateServicePrincipalPassword(map.az_sp_password); - if (typeof vSPPassword === "string") { - throw new Error(vSPPassword); - } - const vSPTenantId = validateServicePrincipalTenantId(map.az_sp_tenant); - if (typeof vSPTenantId === "string") { - throw new Error(vSPTenantId); - } + validateServicePrincipalIdThrowable(map.az_sp_id); + validateServicePrincipalPasswordThrowable(map.az_sp_password); + validateServicePrincipalTenantIdThrowable(map.az_sp_tenant); } - const vSubscriptionId = validateSubscriptionId(map.az_subscription_id); - if (typeof vSubscriptionId === "string") { - throw new Error(vSubscriptionId); - } + validateSubscriptionIdThrowable(map.az_subscription_id); rc.subscriptionId = map.az_subscription_id; } }; @@ -234,32 +222,11 @@ export const getAnswerFromFile = (file: string): RequestContext => { const map = parseInformationFromFile(file); map["azdo_project_name"] = map.azdo_project_name || DEFAULT_PROJECT_NAME; - const vOrgName = validateOrgName(map.azdo_org_name); - if (typeof vOrgName === "string") { - throw new Error(vOrgName); - } - - const vProjectName = validateProjectName(map.azdo_project_name); - if (typeof vProjectName === "string") { - throw new Error(vProjectName); - } - - const vToken = validateAccessToken(map.azdo_pat); - if (typeof vToken === "string") { - throw new Error(vToken); - } - - const vStorageAccountName = validateStorageAccountName( - map.az_storage_account_name - ); - if (typeof vStorageAccountName === "string") { - throw new Error(vStorageAccountName); - } - - const vStorageTable = validateStorageTableName(map.az_storage_table); - if (typeof vStorageTable === "string") { - throw new Error(vStorageTable); - } + validateOrgNameThrowable(map.azdo_org_name); + validateProjectNameThrowable(map.azdo_project_name); + validateAccessTokenThrowable(map.azdo_pat); + validateStorageAccountNameThrowable(map.az_storage_account_name); + validateStorageTableNameThrowable(map.az_storage_table); const rc: RequestContext = { accessToken: map.azdo_pat, @@ -276,7 +243,6 @@ export const getAnswerFromFile = (file: string): RequestContext => { rc.toCreateAppRepo = map.az_create_app === "true"; validationServicePrincipalInfoFromFile(rc, map); - return rc; }; diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index b55e9370b..4487ac7eb 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -5,8 +5,8 @@ import { isDashHex, isIntegerString, isPortNumberString, - ORG_NAME_VIOLATION, validateAccessToken, + validateAccessTokenThrowable, validateACRName, validateForNonEmptyValue, validateOrgName, @@ -19,12 +19,16 @@ import { validateServicePrincipalPassword, validateServicePrincipalTenantId, validateStorageAccountName, + validateStorageAccountNameThrowable, validateStorageAccessKey, validateStorageKeyVaultName, validateStoragePartitionKey, validateStorageTableName, + validateStorageTableNameThrowable, validateSubscriptionId, + validateSubscriptionIdThrowable, } from "./validator"; +import { getErrorMessage } from "./errorBuilder"; describe("Tests on validator helper functions", () => { it("Test hasValue function", () => { @@ -142,25 +146,31 @@ describe("Validating executable prerequisites in spk-config", () => { describe("test validateOrgName function", () => { it("empty value and value with space", () => { - expect(validateOrgName("")).toBe("Must enter an organization"); - expect(validateOrgName(" ")).toBe("Must enter an organization"); + expect(validateOrgName("")).toBe( + getErrorMessage("validation-err-org-name-missing") + ); + expect(validateOrgName(" ")).toBe( + getErrorMessage("validation-err-org-name-missing") + ); expect(() => { validateOrgNameThrowable(""); - }).toThrow(); + }).toThrow(getErrorMessage("validation-err-org-name-missing")); expect(() => { validateOrgNameThrowable(" "); - }).toThrow(); + }).toThrow(getErrorMessage("validation-err-org-name-missing")); }); it("invalid value", () => { const values = ["-abc", ".abc", "abc.", "a b"]; values.forEach((v) => { - expect(validateOrgName(v)).toBe(ORG_NAME_VIOLATION); + expect(validateOrgName(v)).toBe( + getErrorMessage("validation-err-org-name") + ); }); values.forEach((v) => { expect(() => { validateOrgNameThrowable(v); - }).toThrow(); + }).toThrow(getErrorMessage("validation-err-org-name")); }); }); it("valid value", () => { @@ -173,8 +183,12 @@ describe("test validateOrgName function", () => { describe("test validateProjectName function", () => { it("empty value and value with space", () => { - expect(validateProjectName("")).toBe("Must enter a project name"); - expect(validateProjectName(" ")).toBe("Must enter a project name"); + expect(validateProjectName("")).toBe( + getErrorMessage("validation-err-project-name-missing") + ); + expect(validateProjectName(" ")).toBe( + getErrorMessage("validation-err-project-name-missing") + ); expect(() => { validateProjectNameThrowable(""); @@ -186,7 +200,7 @@ describe("test validateProjectName function", () => { it("value over 64 chars long", () => { const val = "a".repeat(65); expect(validateProjectName(val)).toBe( - "Project name cannot be longer than 64 characters" + getErrorMessage("validation-err-project-name-too-long") ); expect(() => { @@ -195,19 +209,19 @@ describe("test validateProjectName function", () => { }); it("invalid value", () => { expect(validateProjectName("_abc")).toBe( - "Project name cannot begin with an underscore" + getErrorMessage("validation-err-project-name-begin-underscore") ); expect(validateProjectName(".abc")).toBe( - "Project name cannot begin or end with a period" + getErrorMessage("validation-err-project-name-period") ); expect(validateProjectName("abc.")).toBe( - "Project name cannot begin or end with a period" + getErrorMessage("validation-err-project-name-period") ); expect(validateProjectName(".abc.")).toBe( - "Project name cannot begin or end with a period" + getErrorMessage("validation-err-project-name-period") ); expect(validateProjectName("a*b")).toBe( - `Project name can't contain special characters, such as / : \\ ~ & % ; @ ' " ? < > | # $ * } { , + = [ ]` + getErrorMessage("validation-err-project-name-special-char") ); ["_abc", ".abc", "abc.", ".abc.", "a*b"].forEach((val) => { @@ -225,8 +239,11 @@ describe("test validateProjectName function", () => { describe("test validateAccessToken function", () => { it("empty value", () => { expect(validateAccessToken("")).toBe( - "Must enter a personal access token with read/write/manage permissions" + getErrorMessage("validation-err-personal-access-token-missing") ); + expect(() => { + validateAccessTokenThrowable(""); + }).toThrow(); }); it("validate value", () => { expect(validateAccessToken("mysecretshhhh")).toBe(true); @@ -246,22 +263,23 @@ describe("test validateServicePrincipal functions", () => { [ { fn: validateServicePrincipalId, - prop: "Service Principal Id", + missing: "validation-err-service-principal-id-missing", + invalid: "validation-err-service-principal-id-invalid", }, { fn: validateServicePrincipalPassword, - prop: "Service Principal Password", + missing: "validation-err-service-principal-pwd-missing", + invalid: "validation-err-service-principal-pwd-invalid", }, { fn: validateServicePrincipalTenantId, - prop: "Service Principal Tenant Id", + missing: "validation-err-service-principal-tenant-id-missing", + invalid: "validation-err-service-principal-tenant-id-invalid", }, ].forEach((item) => { - expect(item.fn("")).toBe(`Must enter a ${item.prop}.`); + expect(item.fn("")).toBe(getErrorMessage(item.missing)); expect(item.fn("b510c1ff-358c-4ed4-96c8-eb23f42bb65b")).toBe(true); - expect(item.fn(".eb23f42bb65b")).toBe( - `The value for ${item.prop} is invalid.` - ); + expect(item.fn(".eb23f42bb65b")).toBe(getErrorMessage(item.invalid)); }); }); }); @@ -269,29 +287,42 @@ describe("test validateServicePrincipal functions", () => { describe("test validateSubscriptionId function", () => { it("sanity test", () => { expect(validateSubscriptionId("")).toBe( - "Must enter a subscription identifier." + getErrorMessage("validation-err-subscription-id-missing") ); expect(validateSubscriptionId("xyz")).toBe( - "The value for subscription identifier is invalid." + getErrorMessage("validation-err-subscription-id-invalid") ); expect(validateSubscriptionId("abc123-456")).toBeTruthy(); + expect(() => { + validateSubscriptionIdThrowable(""); + }).toThrow(); + expect(() => { + validateSubscriptionIdThrowable("xyz"); + }).toThrow(); }); }); describe("test validateStorageAccountName test", () => { it("sanity test", () => { expect(validateStorageAccountName("")).toBe( - "Must enter a storage account name." + getErrorMessage("validation-err-storage-account-name-missing") ); expect(validateStorageAccountName("XYZ123")).toBe( - "The value for storage account name is invalid. Lowercase letters and numbers are allowed." + getErrorMessage("validation-err-storage-account-name-invalid") ); expect(validateStorageAccountName("ab")).toBe( - "The value for storage account name is invalid. It has to be between 3 and 24 characters long" + getErrorMessage("validation-err-storage-account-name-length") ); expect(validateStorageAccountName("12345678a".repeat(3))).toBe( - "The value for storage account name is invalid. It has to be between 3 and 24 characters long" + getErrorMessage("validation-err-storage-account-name-length") ); + + ["", "XYZ123", "ab", "12345678a".repeat(3)].forEach((val) => { + expect(() => { + validateStorageAccountNameThrowable(val); + }).toThrow(); + }); + expect(validateStorageAccountName("abc123456")).toBeTruthy(); }); }); @@ -299,29 +330,37 @@ describe("test validateStorageAccountName test", () => { describe("test validateStorageTableName test", () => { it("sanity test", () => { expect(validateStorageTableName("")).toBe( - "Must enter a storage table name." + getErrorMessage("validation-err-storage-table-name-missing") ); expect(validateStorageTableName("XYZ123*")).toBe( - "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet." + getErrorMessage("validation-err-storage-table-name-invalid") ); expect(validateStorageTableName("1XYZ123")).toBe( - "The value for storage table name is invalid. It has to be alphanumeric and start with an alphabet." + getErrorMessage("validation-err-storage-table-name-invalid") ); expect(validateStorageTableName("ab")).toBe( - "The value for storage table name is invalid. It has to be between 3 and 63 characters long" + getErrorMessage("validation-err-storage-table-name-length") ); expect(validateStorageTableName("a123456789".repeat(7))).toBe( - "The value for storage table name is invalid. It has to be between 3 and 63 characters long" + getErrorMessage("validation-err-storage-table-name-length") ); expect(validateStorageTableName("abc123456")).toBeTruthy(); + + ["", "XYZ123*", "1XYZ123", "ab", "a123456789".repeat(7)].forEach((val) => { + expect(() => { + validateStorageTableNameThrowable(val); + }).toThrow(); + }); }); }); describe("test validatePassword test", () => { it("sanity test", () => { - expect(validatePassword("")).toBe("Must enter a value."); + expect(validatePassword("")).toBe( + getErrorMessage("validation-err-password-missing") + ); expect(validatePassword("1234567")).toBe( - "Must be more than 8 characters long." + getErrorMessage("validation-err-password-too-short") ); expect(validatePassword("abcd1234")).toBeTruthy(); expect(validatePassword("abcdefg123456678")).toBeTruthy(); @@ -331,11 +370,11 @@ describe("test validatePassword test", () => { describe("test validateStoragePartitionKey test", () => { it("sanity test", () => { expect(validateStoragePartitionKey("")).toBe( - "Must enter a storage partition key." + getErrorMessage("validation-err-storage-partition-key-missing") ); ["abc\\", "abc/", "abc?", "abc#"].forEach((s) => { expect(validateStoragePartitionKey(s)).toBe( - "The value for storage partition key is invalid. /, \\, # and ? characters are not allowed." + getErrorMessage("validation-err-storage-partition-key-invalid") ); }); expect(validateStoragePartitionKey("abcdefg123456678")).toBeTruthy(); @@ -345,18 +384,17 @@ describe("test validateStoragePartitionKey test", () => { describe("test validateACRName function", () => { it("sanity test", () => { expect(validateACRName("")).toBe( - "Must enter an Azure Container Registry Name." + getErrorMessage("validation-err-acr-missing") ); expect(validateACRName("xyz-")).toBe( - "The value for Azure Container Registry Name is invalid." + getErrorMessage("validation-err-acr-invalid") ); expect(validateACRName("1")).toBe( - "The value for Azure Container Registry Name is invalid because it has to be between 5 and 50 characters long." + getErrorMessage("validation-err-acr-length") ); expect(validateACRName("1234567890a".repeat(10))).toBe( - "The value for Azure Container Registry Name is invalid because it has to be between 5 and 50 characters long." + getErrorMessage("validation-err-acr-length") ); - expect(validateACRName("abc12356")).toBeTruthy(); }); }); @@ -365,22 +403,22 @@ describe("test validateStorageKeyVaultName function", () => { it("sanity test", () => { expect(validateStorageKeyVaultName("")).toBeTruthy(); expect(validateStorageKeyVaultName("ab*")).toBe( - "The value for Key Value Name is invalid." + getErrorMessage("validation-err-storage-key-vault-invalid") ); expect(validateStorageKeyVaultName("1abc0")).toBe( - "Key Value Name must start with a letter." + getErrorMessage("validation-err-storage-key-vault-start-letter") ); expect(validateStorageKeyVaultName("abc0-")).toBe( - "Key Value Name must end with letter or digit." + getErrorMessage("validation-err-storage-key-vault-end-char") ); expect(validateStorageKeyVaultName("a--b")).toBe( - "Key Value Name cannot contain consecutive hyphens." + getErrorMessage("validation-err-storage-key-vault-hyphen") ); expect(validateStorageKeyVaultName("ab")).toBe( - "The value for Key Vault Name is invalid because it has to be between 3 and 24 characters long." + getErrorMessage("validation-err-storage-key-vault-length") ); 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." + getErrorMessage("validation-err-storage-key-vault-length") ); expect(validateStorageKeyVaultName("abc-12356")).toBeTruthy(); }); @@ -389,7 +427,7 @@ describe("test validateStorageKeyVaultName function", () => { describe("test validateStorageAccessKey function", () => { it("sanity test", () => { expect(validateStorageAccessKey("")).toBe( - "Must enter an Storage Access Key." + getErrorMessage("validation-err-storage-access-key-missing") ); expect(validateStorageAccessKey("abc-12356")).toBeTruthy(); }); diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 6aef4c680..3c0f5a264 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -1,9 +1,8 @@ import shelljs from "shelljs"; import { Config } from "../config"; import { logger } from "../logger"; - -export const ORG_NAME_VIOLATION = - "Organization names must start with a letter or number, followed by letters, numbers or hyphens, and must end with a letter or number."; +import { build as buildError, getErrorMessage } from "./errorBuilder"; +import { errorStatusCode } from "./errorStatusCode"; /** * Values to be validated @@ -101,7 +100,7 @@ export const validatePrereqs = ( */ export const validateOrgName = (value: string): string | boolean => { if (!hasValue((value || "").trim())) { - return "Must enter an organization"; + return getErrorMessage("validation-err-org-name-missing"); } const pass = value.match( /^[0-9a-zA-Z][^\s]*[0-9a-zA-Z]$/ // No Spaces @@ -109,13 +108,13 @@ export const validateOrgName = (value: string): string | boolean => { if (pass) { return true; } - return ORG_NAME_VIOLATION; + return getErrorMessage("validation-err-org-name"); }; export const validateOrgNameThrowable = (value: string): void => { const err = validateOrgName(value); if (typeof err == "string") { - throw Error(err); + throw buildError(errorStatusCode.VALIDATION_ERR, err); } }; @@ -138,10 +137,10 @@ export const isDashAlphaNumeric = (value: string): boolean => { */ export const validatePassword = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a value."; + return getErrorMessage("validation-err-password-missing"); } if (value.length < 8) { - return "Must be more than 8 characters long."; + return getErrorMessage("validation-err-password-too-short"); } return true; }; @@ -153,16 +152,16 @@ export const validatePassword = (value: string): string | boolean => { */ export const validateProjectName = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a project name"; + return getErrorMessage("validation-err-project-name-missing"); } if (value.length > 64) { - return "Project name cannot be longer than 64 characters"; + return getErrorMessage("validation-err-project-name-too-long"); } if (value.startsWith("_")) { - return "Project name cannot begin with an underscore"; + return getErrorMessage("validation-err-project-name-begin-underscore"); } if (value.startsWith(".") || value.endsWith(".")) { - return "Project name cannot begin or end with a period"; + return getErrorMessage("validation-err-project-name-period"); } const invalidChars = [ @@ -192,7 +191,7 @@ export const validateProjectName = (value: string): string | boolean => { "]", ]; if (invalidChars.some((x) => value.indexOf(x) !== -1)) { - return `Project name can't contain special characters, such as / : \\ ~ & % ; @ ' " ? < > | # $ * } { , + = [ ]`; + return getErrorMessage("validation-err-project-name-special-char"); } return true; @@ -201,7 +200,7 @@ export const validateProjectName = (value: string): string | boolean => { export const validateProjectNameThrowable = (value: string): void => { const err = validateProjectName(value); if (typeof err == "string") { - throw Error(err); + throw buildError(errorStatusCode.VALIDATION_ERR, err); } }; @@ -212,20 +211,28 @@ export const validateProjectNameThrowable = (value: string): void => { */ export const validateAccessToken = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a personal access token with read/write/manage permissions"; + return getErrorMessage("validation-err-personal-access-token-missing"); } return true; }; +export const validateAccessTokenThrowable = (value: string): void => { + const err = validateAccessToken(value); + if (typeof err == "string") { + throw buildError(errorStatusCode.VALIDATION_ERR, err); + } +}; + export const validateServicePrincipal = ( value: string, - property: string + missing: string, + invalid: string ): string | boolean => { if (!hasValue(value)) { - return `Must enter a ${property}.`; + return getErrorMessage(missing); } if (!isDashHex(value)) { - return `The value for ${property} is invalid.`; + return getErrorMessage(invalid); } return true; }; @@ -236,7 +243,23 @@ export const validateServicePrincipal = ( * @param value service principal id */ export const validateServicePrincipalId = (value: string): string | boolean => { - return validateServicePrincipal(value, "Service Principal Id"); + return validateServicePrincipal( + value, + "validation-err-service-principal-id-missing", + "validation-err-service-principal-id-invalid" + ); +}; + +/** + * Validate service principal id + * + * @param value service principal id + */ +export const validateServicePrincipalIdThrowable = (value: string): void => { + const msg = validateServicePrincipalId(value); + if (typeof msg === "string") { + throw buildError(errorStatusCode.VALIDATION_ERR, msg); + } }; /** @@ -247,7 +270,25 @@ export const validateServicePrincipalId = (value: string): string | boolean => { export const validateServicePrincipalPassword = ( value: string ): string | boolean => { - return validateServicePrincipal(value, "Service Principal Password"); + return validateServicePrincipal( + value, + "validation-err-service-principal-pwd-missing", + "validation-err-service-principal-pwd-invalid" + ); +}; + +/** + * Validate service principal password + * + * @param value service principal password + */ +export const validateServicePrincipalPasswordThrowable = ( + value: string +): void => { + const msg = validateServicePrincipalPassword(value); + if (typeof msg === "string") { + throw buildError(errorStatusCode.VALIDATION_ERR, msg); + } }; /** @@ -258,7 +299,25 @@ export const validateServicePrincipalPassword = ( export const validateServicePrincipalTenantId = ( value: string ): string | boolean => { - return validateServicePrincipal(value, "Service Principal Tenant Id"); + return validateServicePrincipal( + value, + "validation-err-service-principal-tenant-id-missing", + "validation-err-service-principal-tenant-id-invalid" + ); +}; + +/** + * Validate service principal tenant Id + * + * @param value service principal tenant Id + */ +export const validateServicePrincipalTenantIdThrowable = ( + value: string +): void => { + const msg = validateServicePrincipalTenantId(value); + if (typeof msg === "string") { + throw buildError(errorStatusCode.VALIDATION_ERR, msg); + } }; /** @@ -268,14 +327,26 @@ export const validateServicePrincipalTenantId = ( */ export const validateSubscriptionId = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a subscription identifier."; + return getErrorMessage("validation-err-subscription-id-missing"); } if (!isDashHex(value)) { - return "The value for subscription identifier is invalid."; + return getErrorMessage("validation-err-subscription-id-invalid"); } return true; }; +/** + * Validate subscription identifier + * + * @param value subscription identifier + */ +export const validateSubscriptionIdThrowable = (value: string): void => { + const msg = validateSubscriptionId(value); + if (typeof msg === "string") { + throw buildError(errorStatusCode.VALIDATION_ERR, msg); + } +}; + /** * Returns true if storage account name is valid. * @@ -283,13 +354,13 @@ export const validateSubscriptionId = (value: string): string | boolean => { */ export const validateStorageAccountName = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a storage account name."; + return getErrorMessage("validation-err-storage-account-name-missing"); } if (!value.match(/^[a-z0-9]+$/)) { - return "The value for storage account name is invalid. Lowercase letters and numbers are allowed."; + return getErrorMessage("validation-err-storage-account-name-invalid"); } 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 getErrorMessage("validation-err-storage-account-name-length"); } return true; }; @@ -302,7 +373,7 @@ export const validateStorageAccountName = (value: string): string | boolean => { export const validateStorageAccountNameThrowable = (value: string): void => { const msg = validateStorageAccountName(value); if (typeof msg === "string") { - throw Error(msg); + throw buildError(errorStatusCode.VALIDATION_ERR, msg); } }; @@ -313,13 +384,13 @@ export const validateStorageAccountNameThrowable = (value: string): void => { */ export const validateStorageTableName = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter a storage table name."; + return getErrorMessage("validation-err-storage-table-name-missing"); } 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."; + return getErrorMessage("validation-err-storage-table-name-invalid"); } 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 getErrorMessage("validation-err-storage-table-name-length"); } return true; }; @@ -332,7 +403,7 @@ export const validateStorageTableName = (value: string): string | boolean => { export const validateStorageTableNameThrowable = (value: string): void => { const msg = validateStorageTableName(value); if (typeof msg === "string") { - throw Error(msg); + throw buildError(errorStatusCode.VALIDATION_ERR, msg); } }; @@ -345,10 +416,10 @@ export const validateStoragePartitionKey = ( value: string ): string | boolean => { if (!hasValue(value)) { - return "Must enter a storage partition key."; + return getErrorMessage("validation-err-storage-partition-key-missing"); } if (value.match(/[/\\#?]/)) { - return "The value for storage partition key is invalid. /, \\, # and ? characters are not allowed."; + return getErrorMessage("validation-err-storage-partition-key-invalid"); } return true; }; @@ -360,13 +431,13 @@ export const validateStoragePartitionKey = ( */ export const validateACRName = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter an Azure Container Registry Name."; + return getErrorMessage("validation-err-acr-missing"); } if (!isAlphaNumeric(value)) { - return "The value for Azure Container Registry Name is invalid."; + return getErrorMessage("validation-err-acr-invalid"); } if (value.length < 5 || value.length > 50) { - return "The value for Azure Container Registry Name is invalid because it has to be between 5 and 50 characters long."; + return getErrorMessage("validation-err-acr-length"); } return true; }; @@ -378,26 +449,26 @@ export const validateStorageKeyVaultName = ( return true; // optional } if (!isDashAlphaNumeric(value)) { - return "The value for Key Value Name is invalid."; + return getErrorMessage("validation-err-storage-key-vault-invalid"); } if (!value.match(/^[a-zA-Z]/)) { - return "Key Value Name must start with a letter."; + return getErrorMessage("validation-err-storage-key-vault-start-letter"); } if (!value.match(/[a-zA-Z0-9]$/)) { - return "Key Value Name must end with letter or digit."; + return getErrorMessage("validation-err-storage-key-vault-end-char"); } if (value.indexOf("--") !== -1) { - return "Key Value Name cannot contain consecutive hyphens."; + return getErrorMessage("validation-err-storage-key-vault-hyphen"); } 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 getErrorMessage("validation-err-storage-key-vault-length"); } return true; }; export const validateStorageAccessKey = (value: string): string | boolean => { if (!hasValue(value)) { - return "Must enter an Storage Access Key."; + return getErrorMessage("validation-err-storage-access-key-missing"); } return true; }; From c7e283edd46fda69dd6389eaa8c03b22ad738852 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 1 Apr 2020 12:44:00 -0700 Subject: [PATCH 2/2] Update data.json --- docs/commands/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands/data.json b/docs/commands/data.json index 67938bb06..966eda283 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -14,7 +14,7 @@ "defaultValue": false } ], - "markdown": "This command creates a configuration file, `config.yaml` in a folder `.spk`\nunder your home directory. There are two options for creating this file\n\n1. an interactive mode where you have to answer a few questions; and\n2. you provide a `yaml` file and this `yaml` will be copied to the target\n location.\n\n## Interactive mode\n\nThe command line tool attempts to read `config.yaml` in a folder `.spk` under\nyour home directory. Configuration values shall be read from it if it exists.\nAnd these values shall be default values for the questions. Otherwise, there\nshall be no default values. These are the questions\n\n1. Organization Name of Azure dev-op account\n2. Project Name of Azure dev-op account\n3. Personal Access Token (guides)\n4. Would like to have introspection configuration setup? If yes\n 1. Storage Account Name\n 1. Storage Table Name\n 1. Storage Partition Key\n 1. Storage Access Key\n\nThis tool shall verify these values by making an API call to Azure dev-op. They\nshall be written to `config.yaml` regardless the verification is successful or\nnot.\n\n> Note: In the event that you do not have internet connection, this verification\n> shall not be possible\n\n## Example\n\n```\nspk init --interactive\n```\n\nor\n\n```\nspk init --file myConfig.yaml\n```\n" + "markdown": "This command creates a configuration file, `config.yaml` in a folder `.spk`\nunder your home directory. There are two options for creating this file\n\n1. an interactive mode where you have to answer a few questions; and\n2. you provide a `yaml` file and this `yaml` will be copied to the target\n location.\n\n## Interactive mode\n\nThe command line tool attempts to read `config.yaml` in a folder `.spk` under\nyour home directory. Configuration values shall be read from it if it exists.\nAnd these values shall be default values for the questions. Otherwise, there\nshall be no default values. These are the questions\n\n1. Organization Name of Azure dev-op account\n2. Project Name of Azure dev-op account\n3. Personal Access Token (guides)\n4. Would like to have introspection configuration setup? If yes\n 1. Storage Account Name\n 1. Storage Table Name\n 1. Storage Partition Key\n 1. Storage Access Key\n 1. Key Vault Name (optional)\n\nThis tool shall verify these values by making an API call to Azure dev-op. They\nshall be written to `config.yaml` regardless the verification is successful or\nnot.\n\n> Note: In the event that you do not have internet connection, this verification\n> shall not be possible\n\n## Example\n\n```\nspk init --interactive\n```\n\nor\n\n```\nspk init --file myConfig.yaml\n```\n" }, "setup": { "command": "setup",