From 453a41b05da3c1617c11cbcb3e53dd9d7e2a35bd Mon Sep 17 00:00:00 2001 From: Jim Przybylinski Date: Wed, 16 May 2018 17:22:22 -0700 Subject: [PATCH] Ensure the default deployment name is a valid name. (#959) * Ensure the default deployment name is a valid name. --- .../ArgumentCheckUtils.cs | 4 +- .../GkeStep/GkeStepViewModel.cs | 2 +- .../Utils/GcpPublishStepsUtils.cs | 32 ++++++---- .../GoogleCloudExtension/Utils/StringUtils.cs | 39 +++++++++++++ .../GoogleCloudExtensionUnitTests.csproj | 3 +- .../GkeStep/GkeStepViewModelTests.cs | 6 +- .../Utils/GcpPublishStepsUtilsTests.cs | 58 +++++++++++++++++++ .../{ => Utils}/StringFormatUtilsTests.cs | 2 +- .../Utils/StringUtilTests.cs | 51 ++++++++++++++++ 9 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/GcpPublishStepsUtilsTests.cs rename GoogleCloudExtension/GoogleCloudExtensionUnitTests/{ => Utils}/StringFormatUtilsTests.cs (96%) diff --git a/GoogleCloudExtension/GoogleCloudExtension.Utils/ArgumentCheckUtils.cs b/GoogleCloudExtension/GoogleCloudExtension.Utils/ArgumentCheckUtils.cs index 4c59ba0d5..203c35eda 100644 --- a/GoogleCloudExtension/GoogleCloudExtension.Utils/ArgumentCheckUtils.cs +++ b/GoogleCloudExtension/GoogleCloudExtension.Utils/ArgumentCheckUtils.cs @@ -28,11 +28,11 @@ public static string ThrowIfNullOrEmpty(this string arg, string message) return arg; } - public static T ThrowIfNull(this T arg, string message) + public static T ThrowIfNull(this T arg, string paramName) { if (arg == null) { - throw new ArgumentNullException(message ?? ""); + throw new ArgumentNullException(paramName ?? ""); } return arg; diff --git a/GoogleCloudExtension/GoogleCloudExtension/PublishDialogSteps/GkeStep/GkeStepViewModel.cs b/GoogleCloudExtension/GoogleCloudExtension/PublishDialogSteps/GkeStep/GkeStepViewModel.cs index b2c18272a..c3f069771 100644 --- a/GoogleCloudExtension/GoogleCloudExtension/PublishDialogSteps/GkeStep/GkeStepViewModel.cs +++ b/GoogleCloudExtension/GoogleCloudExtension/PublishDialogSteps/GkeStep/GkeStepViewModel.cs @@ -235,7 +235,7 @@ protected override async Task InitializeDialogAsync() Task initializeDialogTask = base.InitializeDialogAsync(); // In the meantime, set DeploymentName, which launches validations and updates the UI. - DeploymentName = PublishDialog.Project.Name.ToLower(); + DeploymentName = GcpPublishStepsUtils.ToValidName(PublishDialog.Project.Name); // Wait for the initialization task to be done. await initializeDialogTask; diff --git a/GoogleCloudExtension/GoogleCloudExtension/Utils/GcpPublishStepsUtils.cs b/GoogleCloudExtension/GoogleCloudExtension/Utils/GcpPublishStepsUtils.cs index 5a448b808..5d99768c8 100644 --- a/GoogleCloudExtension/GoogleCloudExtension/Utils/GcpPublishStepsUtils.cs +++ b/GoogleCloudExtension/GoogleCloudExtension/Utils/GcpPublishStepsUtils.cs @@ -25,18 +25,9 @@ namespace GoogleCloudExtension.Utils /// public static class GcpPublishStepsUtils { - /// - /// This regexp defines what names are valid for GCP deployments. Basically it defines a valid name - /// as only containing lowercase letters and numbers and optionally the - character. It also specifies - /// that the name has to be less than 100 chars. - /// This regexp is the same one used by gcloud to validate version names. - /// - private static readonly Regex s_validNamePattern = new Regex(@"^(?!-)[a-z\d\-]{1,100}$"); - // Static properties for unit testing. internal static DateTime? NowOverride { private get; set; } private static DateTime Now => NowOverride ?? DateTime.Now; - /// /// Returns a default version name suitable for publishing to GKE and Flex. /// @@ -51,8 +42,8 @@ public static string GetDefaultVersion() /// Determines if the given name is a valid name. /// /// The name to check. - /// True if the name is valid, false otherwise. - public static bool IsValidName(string name) => !string.IsNullOrEmpty(name) && s_validNamePattern.IsMatch(name); + /// The name of the field being validated. + /// The results of the validation. public static IEnumerable ValidateName(string name, string fieldName) { @@ -80,6 +71,25 @@ public static IEnumerable ValidateName(string name, string fie } } + /// + /// Converts a possibly invalid name to one that will pass name validation. + /// + /// The name to converto to valid. + /// A valid name built from the starting name. + public static string ToValidName(string name) + { + if (name == null) + { + return null; + } + string kebobedName = StringUtils.ToKebobCase(name); + string beginsWithLetterOrNumber = Regex.Replace(kebobedName, @"^[^a-z\d]+", ""); + string invalidCharactersReplaced = Regex.Replace(beginsWithLetterOrNumber, @"[^a-z\d\-]", "-"); + return invalidCharactersReplaced.Length > 100 ? + invalidCharactersReplaced.Substring(0, 100) : + invalidCharactersReplaced; + } + public static IEnumerable ValidatePositiveNonZeroInteger(string value, string fieldName) { int intValue; diff --git a/GoogleCloudExtension/GoogleCloudExtension/Utils/StringUtils.cs b/GoogleCloudExtension/GoogleCloudExtension/Utils/StringUtils.cs index 22bb6fad6..abaff5b06 100644 --- a/GoogleCloudExtension/GoogleCloudExtension/Utils/StringUtils.cs +++ b/GoogleCloudExtension/GoogleCloudExtension/Utils/StringUtils.cs @@ -174,5 +174,44 @@ public static int LastNonSpaceIndex(string text) return -1; } + + /// + /// Converts the input CameCase text to lower-kebob-case + /// + /// The input text to convert. + /// The text converted to kebob-case. + public static string ToKebobCase(string camelCaseText) + { + if (camelCaseText == null) + { + return null; + } + var lastCharLowerLetter = false; + var lastCharLetter = false; + var lastCharDidget = false; + var result = new StringBuilder(); + foreach (char c in camelCaseText) + { + if (char.IsUpper(c) && lastCharLowerLetter) + { + result.Append("-"); + } + else if (char.IsLetter(c) && lastCharDidget) + { + result.Append("-"); + } + else if (char.IsDigit(c) && lastCharLetter) + { + result.Append("-"); + } + + result.Append(char.ToLower(c)); + lastCharLowerLetter = char.IsLower(c); + lastCharLetter = char.IsLetter(c); + lastCharDidget = char.IsDigit(c); + } + + return result.ToString(); + } } } diff --git a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/GoogleCloudExtensionUnitTests.csproj b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/GoogleCloudExtensionUnitTests.csproj index 0aa6f40ca..e3b0c4d8d 100644 --- a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/GoogleCloudExtensionUnitTests.csproj +++ b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/GoogleCloudExtensionUnitTests.csproj @@ -276,10 +276,11 @@ + - + diff --git a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/PublishDialogSteps/GkeStep/GkeStepViewModelTests.cs b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/PublishDialogSteps/GkeStep/GkeStepViewModelTests.cs index 0c4f60e42..44934b111 100644 --- a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/PublishDialogSteps/GkeStep/GkeStepViewModelTests.cs +++ b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/PublishDialogSteps/GkeStep/GkeStepViewModelTests.cs @@ -352,13 +352,13 @@ public async Task TestRefreshClustersListCommand_SetsClustersInOrder() } [TestMethod] - public void TestInitializeDialogAsync_SetsDeploymentName() + public void TestInitializeDialogAsync_SetsValidDeploymentName() { - Mock.Get(_mockedPublishDialog).Setup(pd => pd.Project.Name).Returns("AProjectName"); + Mock.Get(_mockedPublishDialog).Setup(pd => pd.Project.Name).Returns("VisualStudioProjectName"); _objectUnderTest.OnVisible(_mockedPublishDialog); - Assert.AreEqual("aprojectname", _objectUnderTest.DeploymentName); + Assert.AreEqual("visual-studio-project-name", _objectUnderTest.DeploymentName); } [TestMethod] diff --git a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/GcpPublishStepsUtilsTests.cs b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/GcpPublishStepsUtilsTests.cs new file mode 100644 index 000000000..c62536ddf --- /dev/null +++ b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/GcpPublishStepsUtilsTests.cs @@ -0,0 +1,58 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GoogleCloudExtension.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace GoogleCloudExtensionUnitTests.Utils +{ + [TestClass] + public class GcpPublishStepsUtilsTests + { + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow("already-kebob-case")] + [DataRow("end-with-dash-")] + public void TestToValid_NameUnchanged(string unchangingName) + { + string result = GcpPublishStepsUtils.ToValidName(unchangingName); + + Assert.AreEqual(unchangingName, result); + } + + [TestMethod] + [DataRow("UpperCamelCase", "upper-camel-case")] + [DataRow("many.(*)symbols!@$", "many----symbols---")] + [DataRow("-start-with-dash", "start-with-dash")] + [DataRow("@start-with-symbol", "start-with-symbol")] + public void TestToValid_NameChange(string invalidName, string expectedResult) + { + string result = GcpPublishStepsUtils.ToValidName(invalidName); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void TestToValid_NameTruncatesAt100Characters() + { + string longString = string.Join("", Enumerable.Range(1, 200)); + + string result = GcpPublishStepsUtils.ToValidName(longString); + + Assert.IsTrue(result.Length == 100); + } + } +} diff --git a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/StringFormatUtilsTests.cs b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringFormatUtilsTests.cs similarity index 96% rename from GoogleCloudExtension/GoogleCloudExtensionUnitTests/StringFormatUtilsTests.cs rename to GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringFormatUtilsTests.cs index 56c9d517e..7047e7c7d 100644 --- a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/StringFormatUtilsTests.cs +++ b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringFormatUtilsTests.cs @@ -15,7 +15,7 @@ using GoogleCloudExtension.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace GoogleCloudExtensionUnitTests +namespace GoogleCloudExtensionUnitTests.Utils { [TestClass] public class StringFormatUtilsTests diff --git a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringUtilTests.cs b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringUtilTests.cs index 8fde95142..ed2ea81e0 100644 --- a/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringUtilTests.cs +++ b/GoogleCloudExtension/GoogleCloudExtensionUnitTests/Utils/StringUtilTests.cs @@ -13,6 +13,7 @@ // limitations under the License. using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; using static GoogleCloudExtension.Utils.StringUtils; namespace GoogleCloudExtensionUnitTests.Utils @@ -58,5 +59,55 @@ public void LastNonSpaceIndexTests() Assert.AreEqual(-1, LastNonSpaceIndex(null)); Assert.AreEqual(8, LastNonSpaceIndex(" uu pp ")); } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow("word")] + [DataRow("already-kebob-case")] + [DataRow("%$#(*&)(@#$")] + public void TestToKebobCase_Unchanged(string unchangingArgument) + { + string result = ToKebobCase(unchangingArgument); + + Assert.AreEqual(unchangingArgument, result); + } + + [TestMethod] + [DataRow("ALLUPPERCASE", "alluppercase")] + [DataRow("UpperCamelCase", "upper-camel-case")] + [DataRow("lowerCamelCase", "lower-camel-case")] + [DataRow("Upper-Kebob-Case", "upper-kebob-case")] + [DataRow("20number2", "20-number-2")] + [DataRow("UpperCamelCaseWith2Numbers100", "upper-camel-case-with-2-numbers-100")] + [DataRow("CamelCase&Symbols!", "camel-case&symbols!")] + public void TestToKebobCase_Changes(string argument, string expectedResult) + { + string result = ToKebobCase(argument); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void TestToKebobCase_ExtremelyLongValueChanges() + { + string longNumberString = string.Join("", Enumerable.Range(1, 200)); + string argument = "CamelCaseHead" + longNumberString + "CamelCaseTail"; + string expected = "camel-case-head-" + longNumberString + "-camel-case-tail"; + + string result = ToKebobCase(argument); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TestToKebobCase_ExtremelyLongValueUnchanged() + { + string longNumberString = string.Join("", Enumerable.Range(1, 200)); + + string result = ToKebobCase(longNumberString); + + Assert.AreEqual(longNumberString, result); + } } }