From c5ed9a2403ce64fecdaa3b79751933dfa70a5d32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:24:20 +0000 Subject: [PATCH 1/5] Initial plan From ca1a26fb945f3b09d8238438105d135eb6231431 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:48:09 +0000 Subject: [PATCH 2/5] Update project name validation to support Unicode characters and add comprehensive tests Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Commands/NewCommand.cs | 2 +- .../Commands/ProjectNameValidatorTests.cs | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 884dfc88ad8..59ea5115a0d 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -222,7 +222,7 @@ public virtual async Task PromptForTemplateAsync(ITemplate[] validTem internal static partial class ProjectNameValidator { - [GeneratedRegex(@"^[a-zA-Z0-9_][a-zA-Z0-9_.]{0,253}[a-zA-Z0-9_]$", RegexOptions.Compiled)] + [GeneratedRegex(@"^[\p{L}\p{N}]([\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}][\p{Mn}\p{Mc}]*|[\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}])?$", RegexOptions.Compiled)] internal static partial Regex GetAssemblyNameRegex(); public static bool IsProjectNameValid(string projectName) diff --git a/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs b/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs new file mode 100644 index 00000000000..e93aebb7cb3 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; + +namespace Aspire.Cli.Tests.Commands; + +public class ProjectNameValidatorTests +{ + [Theory] + [InlineData("项目1", true)] // Chinese + [InlineData("Проект1", true)] // Cyrillic + [InlineData("プロジェクト1", true)] // Japanese + [InlineData("مشروع1", true)] // Arabic + [InlineData("Project_1", true)] // Latin with underscore + [InlineData("Project-1", true)] // Latin with dash + [InlineData("Project.1", true)] // Latin with dot + [InlineData("MyApp", true)] // Simple ASCII + [InlineData("A", true)] // Single character + [InlineData("1", true)] // Single number + [InlineData("プ", true)] // Single Unicode character + [InlineData("Test123", true)] // Mixed letters and numbers + [InlineData("My_Cool-Project.v2", true)] // Complex valid name + public void IsProjectNameValid_ValidNames_ReturnsTrue(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("Project/1", false)] // Forward slash (unsafe) + [InlineData("Project\\1", false)] // Backslash (unsafe) + [InlineData("Project:1", false)] // Colon (unsafe) + [InlineData("Project*1", false)] // Asterisk (unsafe) + [InlineData("Project?1", false)] // Question mark (unsafe) + [InlineData("Project\"1", false)] // Quote (unsafe) + [InlineData("Project<1", false)] // Less than (unsafe) + [InlineData("Project>1", false)] // Greater than (unsafe) + [InlineData("Project|1", false)] // Pipe (unsafe) + [InlineData("", false)] // Empty string + [InlineData(" ", false)] // Space only + [InlineData("Project ", false)] // Ends with space + [InlineData(" Project", false)] // Starts with space + [InlineData("Pro ject", false)] // Space in middle + [InlineData("-Project", false)] // Starts with dash + [InlineData("Project-", false)] // Ends with dash + [InlineData(".Project", false)] // Starts with dot + [InlineData("Project.", false)] // Ends with dot + [InlineData("_Project", false)] // Starts with underscore + [InlineData("Project_", false)] // Ends with underscore + public void IsProjectNameValid_InvalidNames_ReturnsFalse(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void IsProjectNameValid_MaxLength254_ReturnsTrue() + { + // Arrange + var projectName = new string('A', 254); + + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsProjectNameValid_Length255_ReturnsFalse() + { + // Arrange + var projectName = new string('A', 255); + + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("项目测试名称很长的中文项目名称")] // Long Chinese name + [InlineData("очень_длинное_русское_имя_проекта")] // Long Russian name + [InlineData("とても長い日本語のプロジェクト名")] // Long Japanese name + [InlineData("اسم_مشروع_طويل_جدا_بالعربية")] // Long Arabic name + public void IsProjectNameValid_LongUnicodeNames_ReturnsTrue(string projectName) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Unicode project name should be valid: {projectName}"); + } + + [Theory] + [InlineData("Ελληνικά", true)] // Greek + [InlineData("עברית", true)] // Hebrew + [InlineData("हिन्दी", true)] // Hindi + [InlineData("ไทย", true)] // Thai + [InlineData("한국어", true)] // Korean + [InlineData("Türkçe", true)] // Turkish + [InlineData("Português", true)] // Portuguese with accent + [InlineData("Français", true)] // French with accent + [InlineData("Español", true)] // Spanish with accent + [InlineData("Deutsch", true)] // German + public void IsProjectNameValid_VariousLanguages_ReturnsTrue(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("Test123-Project_Name.v2")] // Complex valid with all allowed characters + [InlineData("A1-B2_C3.D4")] // Mixed with separators + [InlineData("项目-测试_版本.1")] // Unicode with separators + public void IsProjectNameValid_ComplexValidNames_ReturnsTrue(string projectName) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Complex valid project name should be valid: {projectName}"); + } + + [Theory] + [InlineData("Test..Name")] // Double dot + [InlineData("Test--Name")] // Double dash + [InlineData("Test__Name")] // Double underscore + public void IsProjectNameValid_ConsecutiveSpecialChars_ReturnsTrue(string projectName) + { + // These should be valid as the spec doesn't prohibit consecutive allowed characters + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Consecutive allowed characters should be valid: {projectName}"); + } +} \ No newline at end of file From 6a0e7d3c5aa844a83af8c910cd72e8eabe7f1b47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:52:06 +0000 Subject: [PATCH 3/5] Add documentation comment explaining the Unicode-aware project name validation regex Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Commands/NewCommand.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 59ea5115a0d..a4885be241d 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -222,6 +222,12 @@ public virtual async Task PromptForTemplateAsync(ITemplate[] validTem internal static partial class ProjectNameValidator { + // Regex for Unicode-aware project name validation: + // - Starts with Unicode letter or number [\p{L}\p{N}] + // - Can contain Unicode letters, numbers, connector punctuation (underscore), dash, dot, and combining marks + // - Must end with Unicode letter or number, optionally followed by combining marks + // - Length: 1-254 characters + // - Excludes unsafe characters: / \ : * ? " < > | [GeneratedRegex(@"^[\p{L}\p{N}]([\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}][\p{Mn}\p{Mc}]*|[\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}])?$", RegexOptions.Compiled)] internal static partial Regex GetAssemblyNameRegex(); From 1fd9a51482009d73831f8a9a1de73c08748d5ae2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 05:57:28 +0000 Subject: [PATCH 4/5] Remove project name validation - let dotnet new handle validation Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Cli/Commands/NewCommand.cs | 23 --- .../Templating/DotNetTemplateFactory.cs | 2 +- .../Commands/ProjectNameValidatorTests.cs | 149 ------------------ 3 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index a4885be241d..9c742f1194e 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using System.Text.RegularExpressions; using Aspire.Cli.Certificates; using Aspire.Cli.Configuration; using Aspire.Cli.DotNet; @@ -13,7 +12,6 @@ using Aspire.Cli.Templating; using Aspire.Cli.Utils; using Semver; -using Spectre.Console; using NuGetPackage = Aspire.Shared.NuGetPackageCli; namespace Aspire.Cli.Commands; @@ -203,9 +201,6 @@ public virtual async Task PromptForProjectNameAsync(string defaultName, return await interactionService.PromptForStringAsync( NewCommandStrings.EnterTheProjectName, defaultValue: defaultName, - validator: name => ProjectNameValidator.IsProjectNameValid(name) - ? ValidationResult.Success() - : ValidationResult.Error(NewCommandStrings.InvalidProjectName), cancellationToken: cancellationToken); } @@ -219,21 +214,3 @@ public virtual async Task PromptForTemplateAsync(ITemplate[] validTem ); } } - -internal static partial class ProjectNameValidator -{ - // Regex for Unicode-aware project name validation: - // - Starts with Unicode letter or number [\p{L}\p{N}] - // - Can contain Unicode letters, numbers, connector punctuation (underscore), dash, dot, and combining marks - // - Must end with Unicode letter or number, optionally followed by combining marks - // - Length: 1-254 characters - // - Excludes unsafe characters: / \ : * ? " < > | - [GeneratedRegex(@"^[\p{L}\p{N}]([\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}][\p{Mn}\p{Mc}]*|[\p{L}\p{N}\p{Pc}.\-\p{Mn}\p{Mc}]{0,252}[\p{L}\p{N}])?$", RegexOptions.Compiled)] - internal static partial Regex GetAssemblyNameRegex(); - - public static bool IsProjectNameValid(string projectName) - { - var regex = GetAssemblyNameRegex(); - return regex.IsMatch(projectName); - } -} diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs index f30ca32ff8c..086011a7071 100644 --- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs @@ -313,7 +313,7 @@ private async Task ApplyTemplateAsync(CallbackTemplate template, private async Task GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken) { - if (parseResult.GetValue("--name") is not { } name || !ProjectNameValidator.IsProjectNameValid(name)) + if (parseResult.GetValue("--name") is not { } name) { var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; name = await prompter.PromptForProjectNameAsync(defaultName, cancellationToken); diff --git a/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs b/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs deleted file mode 100644 index e93aebb7cb3..00000000000 --- a/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Aspire.Cli.Commands; - -namespace Aspire.Cli.Tests.Commands; - -public class ProjectNameValidatorTests -{ - [Theory] - [InlineData("项目1", true)] // Chinese - [InlineData("Проект1", true)] // Cyrillic - [InlineData("プロジェクト1", true)] // Japanese - [InlineData("مشروع1", true)] // Arabic - [InlineData("Project_1", true)] // Latin with underscore - [InlineData("Project-1", true)] // Latin with dash - [InlineData("Project.1", true)] // Latin with dot - [InlineData("MyApp", true)] // Simple ASCII - [InlineData("A", true)] // Single character - [InlineData("1", true)] // Single number - [InlineData("プ", true)] // Single Unicode character - [InlineData("Test123", true)] // Mixed letters and numbers - [InlineData("My_Cool-Project.v2", true)] // Complex valid name - public void IsProjectNameValid_ValidNames_ReturnsTrue(string projectName, bool expected) - { - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("Project/1", false)] // Forward slash (unsafe) - [InlineData("Project\\1", false)] // Backslash (unsafe) - [InlineData("Project:1", false)] // Colon (unsafe) - [InlineData("Project*1", false)] // Asterisk (unsafe) - [InlineData("Project?1", false)] // Question mark (unsafe) - [InlineData("Project\"1", false)] // Quote (unsafe) - [InlineData("Project<1", false)] // Less than (unsafe) - [InlineData("Project>1", false)] // Greater than (unsafe) - [InlineData("Project|1", false)] // Pipe (unsafe) - [InlineData("", false)] // Empty string - [InlineData(" ", false)] // Space only - [InlineData("Project ", false)] // Ends with space - [InlineData(" Project", false)] // Starts with space - [InlineData("Pro ject", false)] // Space in middle - [InlineData("-Project", false)] // Starts with dash - [InlineData("Project-", false)] // Ends with dash - [InlineData(".Project", false)] // Starts with dot - [InlineData("Project.", false)] // Ends with dot - [InlineData("_Project", false)] // Starts with underscore - [InlineData("Project_", false)] // Ends with underscore - public void IsProjectNameValid_InvalidNames_ReturnsFalse(string projectName, bool expected) - { - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.Equal(expected, result); - } - - [Fact] - public void IsProjectNameValid_MaxLength254_ReturnsTrue() - { - // Arrange - var projectName = new string('A', 254); - - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsProjectNameValid_Length255_ReturnsFalse() - { - // Arrange - var projectName = new string('A', 255); - - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.False(result); - } - - [Theory] - [InlineData("项目测试名称很长的中文项目名称")] // Long Chinese name - [InlineData("очень_длинное_русское_имя_проекта")] // Long Russian name - [InlineData("とても長い日本語のプロジェクト名")] // Long Japanese name - [InlineData("اسم_مشروع_طويل_جدا_بالعربية")] // Long Arabic name - public void IsProjectNameValid_LongUnicodeNames_ReturnsTrue(string projectName) - { - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.True(result, $"Unicode project name should be valid: {projectName}"); - } - - [Theory] - [InlineData("Ελληνικά", true)] // Greek - [InlineData("עברית", true)] // Hebrew - [InlineData("हिन्दी", true)] // Hindi - [InlineData("ไทย", true)] // Thai - [InlineData("한국어", true)] // Korean - [InlineData("Türkçe", true)] // Turkish - [InlineData("Português", true)] // Portuguese with accent - [InlineData("Français", true)] // French with accent - [InlineData("Español", true)] // Spanish with accent - [InlineData("Deutsch", true)] // German - public void IsProjectNameValid_VariousLanguages_ReturnsTrue(string projectName, bool expected) - { - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("Test123-Project_Name.v2")] // Complex valid with all allowed characters - [InlineData("A1-B2_C3.D4")] // Mixed with separators - [InlineData("项目-测试_版本.1")] // Unicode with separators - public void IsProjectNameValid_ComplexValidNames_ReturnsTrue(string projectName) - { - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.True(result, $"Complex valid project name should be valid: {projectName}"); - } - - [Theory] - [InlineData("Test..Name")] // Double dot - [InlineData("Test--Name")] // Double dash - [InlineData("Test__Name")] // Double underscore - public void IsProjectNameValid_ConsecutiveSpecialChars_ReturnsTrue(string projectName) - { - // These should be valid as the spec doesn't prohibit consecutive allowed characters - // Act - var result = ProjectNameValidator.IsProjectNameValid(projectName); - - // Assert - Assert.True(result, $"Consecutive allowed characters should be valid: {projectName}"); - } -} \ No newline at end of file From 020c0762f4cf5b724f5cc59408ac989ead5c606d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:56:47 +0000 Subject: [PATCH 5/5] Revert to validation logic and simplify to only reject path separators Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Commands/NewCommand.cs | 26 +++ .../Templating/DotNetTemplateFactory.cs | 2 +- .../Commands/ProjectNameValidatorTests.cs | 171 ++++++++++++++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 9c742f1194e..81cf290f4f3 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using System.Text.RegularExpressions; using Aspire.Cli.Certificates; using Aspire.Cli.Configuration; using Aspire.Cli.DotNet; @@ -12,6 +13,7 @@ using Aspire.Cli.Templating; using Aspire.Cli.Utils; using Semver; +using Spectre.Console; using NuGetPackage = Aspire.Shared.NuGetPackageCli; namespace Aspire.Cli.Commands; @@ -201,6 +203,9 @@ public virtual async Task PromptForProjectNameAsync(string defaultName, return await interactionService.PromptForStringAsync( NewCommandStrings.EnterTheProjectName, defaultValue: defaultName, + validator: name => ProjectNameValidator.IsProjectNameValid(name) + ? ValidationResult.Success() + : ValidationResult.Error(NewCommandStrings.InvalidProjectName), cancellationToken: cancellationToken); } @@ -214,3 +219,24 @@ public virtual async Task PromptForTemplateAsync(ITemplate[] validTem ); } } + +internal static partial class ProjectNameValidator +{ + // Regex for project name validation: + // - Can be any characters except path separators (/ and \) + // - Length: 1-254 characters + // - Must not be empty or whitespace only + [GeneratedRegex(@"^[^/\\]{1,254}$", RegexOptions.Compiled)] + internal static partial Regex GetProjectNameRegex(); + + public static bool IsProjectNameValid(string projectName) + { + if (string.IsNullOrWhiteSpace(projectName)) + { + return false; + } + + var regex = GetProjectNameRegex(); + return regex.IsMatch(projectName); + } +} diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs index 086011a7071..f30ca32ff8c 100644 --- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs @@ -313,7 +313,7 @@ private async Task ApplyTemplateAsync(CallbackTemplate template, private async Task GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken) { - if (parseResult.GetValue("--name") is not { } name) + if (parseResult.GetValue("--name") is not { } name || !ProjectNameValidator.IsProjectNameValid(name)) { var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; name = await prompter.PromptForProjectNameAsync(defaultName, cancellationToken); diff --git a/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs b/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs new file mode 100644 index 00000000000..ffcf4aa5649 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Commands; + +namespace Aspire.Cli.Tests.Commands; + +public class ProjectNameValidatorTests +{ + [Theory] + [InlineData("项目1", true)] // Chinese + [InlineData("Проект1", true)] // Cyrillic + [InlineData("プロジェクト1", true)] // Japanese + [InlineData("مشروع1", true)] // Arabic + [InlineData("Project_1", true)] // Latin with underscore + [InlineData("Project-1", true)] // Latin with dash + [InlineData("Project.1", true)] // Latin with dot + [InlineData("MyApp", true)] // Simple ASCII + [InlineData("A", true)] // Single character + [InlineData("1", true)] // Single number + [InlineData("プ", true)] // Single Unicode character + [InlineData("Test123", true)] // Mixed letters and numbers + [InlineData("My_Cool-Project.v2", true)] // Complex valid name + [InlineData("Project:1", true)] // Colon (now allowed) + [InlineData("Project*1", true)] // Asterisk (now allowed) + [InlineData("Project?1", true)] // Question mark (now allowed) + [InlineData("Project\"1", true)] // Quote (now allowed) + [InlineData("Project<1", true)] // Less than (now allowed) + [InlineData("Project>1", true)] // Greater than (now allowed) + [InlineData("Project|1", true)] // Pipe (now allowed) + [InlineData("Project ", true)] // Ends with space (now allowed) + [InlineData(" Project", true)] // Starts with space (now allowed) + [InlineData("Pro ject", true)] // Space in middle (now allowed) + [InlineData("-Project", true)] // Starts with dash (now allowed) + [InlineData("Project-", true)] // Ends with dash (now allowed) + [InlineData(".Project", true)] // Starts with dot (now allowed) + [InlineData("Project.", true)] // Ends with dot (now allowed) + [InlineData("_Project", true)] // Starts with underscore (now allowed) + [InlineData("Project_", true)] // Ends with underscore (now allowed) + public void IsProjectNameValid_ValidNames_ReturnsTrue(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("Project/1", false)] // Forward slash (path separator) + [InlineData("Project\\1", false)] // Backslash (path separator) + [InlineData("", false)] // Empty string + [InlineData(" ", false)] // Space only + [InlineData(" ", false)] // Multiple spaces only + [InlineData("\t", false)] // Tab only + [InlineData("\n", false)] // Newline only + public void IsProjectNameValid_InvalidNames_ReturnsFalse(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void IsProjectNameValid_MaxLength254_ReturnsTrue() + { + // Arrange + var projectName = new string('A', 254); + + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsProjectNameValid_Length255_ReturnsFalse() + { + // Arrange + var projectName = new string('A', 255); + + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("项目测试名称很长的中文项目名称")] // Long Chinese name + [InlineData("очень_длинное_русское_имя_проекта")] // Long Russian name + [InlineData("とても長い日本語のプロジェクト名")] // Long Japanese name + [InlineData("اسم_مشروع_طويل_جدا_بالعربية")] // Long Arabic name + public void IsProjectNameValid_LongUnicodeNames_ReturnsTrue(string projectName) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Unicode project name should be valid: {projectName}"); + } + + [Theory] + [InlineData("Ελληνικά", true)] // Greek + [InlineData("עברית", true)] // Hebrew + [InlineData("हिन्दी", true)] // Hindi + [InlineData("ไทย", true)] // Thai + [InlineData("한국어", true)] // Korean + [InlineData("Türkçe", true)] // Turkish + [InlineData("Português", true)] // Portuguese with accent + [InlineData("Français", true)] // French with accent + [InlineData("Español", true)] // Spanish with accent + [InlineData("Deutsch", true)] // German + public void IsProjectNameValid_VariousLanguages_ReturnsTrue(string projectName, bool expected) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("Test123-Project_Name.v2")] // Complex valid with all allowed characters + [InlineData("A1-B2_C3.D4")] // Mixed with separators + [InlineData("项目-测试_版本.1")] // Unicode with separators + public void IsProjectNameValid_ComplexValidNames_ReturnsTrue(string projectName) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Complex valid project name should be valid: {projectName}"); + } + + [Theory] + [InlineData("Test..Name")] // Double dot + [InlineData("Test--Name")] // Double dash + [InlineData("Test__Name")] // Double underscore + public void IsProjectNameValid_ConsecutiveSpecialChars_ReturnsTrue(string projectName) + { + // These should be valid as the spec doesn't prohibit consecutive allowed characters + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.True(result, $"Consecutive allowed characters should be valid: {projectName}"); + } + + [Theory] + [InlineData("My/Project")] // Forward slash in middle + [InlineData("/MyProject")] // Forward slash at start + [InlineData("MyProject/")] // Forward slash at end + [InlineData("My\\Project")] // Backslash in middle + [InlineData("\\MyProject")] // Backslash at start + [InlineData("MyProject\\")] // Backslash at end + [InlineData("My/Project/Name")] // Multiple forward slashes + [InlineData("My\\Project\\Name")] // Multiple backslashes + [InlineData("My/Project\\Name")] // Mixed path separators + public void IsProjectNameValid_PathSeparators_ReturnsFalse(string projectName) + { + // Act + var result = ProjectNameValidator.IsProjectNameValid(projectName); + + // Assert + Assert.False(result, $"Project name with path separators should be invalid: {projectName}"); + } +} \ No newline at end of file