Skip to content

Commit d3774ae

Browse files
github-actions[bot]Copilotmitchdennydavidfowl
authored
[release/9.4] Simplify Aspire CLI project name validation to only reject path separators (#10832)
* Initial plan * Update project name validation to support Unicode characters and add comprehensive tests Co-authored-by: mitchdenny <[email protected]> * Add documentation comment explaining the Unicode-aware project name validation regex Co-authored-by: mitchdenny <[email protected]> * Remove project name validation - let dotnet new handle validation Co-authored-by: davidfowl <[email protected]> * Revert to validation logic and simplify to only reject path separators Co-authored-by: mitchdenny <[email protected]> * Fix test compile. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mitchdenny <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: Mitch Denny <[email protected]>
1 parent 7adcde9 commit d3774ae

File tree

2 files changed

+184
-3
lines changed

2 files changed

+184
-3
lines changed

src/Aspire.Cli/Commands/NewCommand.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,21 @@ public virtual async Task<ITemplate> PromptForTemplateAsync(ITemplate[] validTem
222222

223223
internal static partial class ProjectNameValidator
224224
{
225-
[GeneratedRegex(@"^[a-zA-Z0-9_][a-zA-Z0-9_.]{0,253}[a-zA-Z0-9_]$", RegexOptions.Compiled)]
226-
internal static partial Regex GetAssemblyNameRegex();
225+
// Regex for project name validation:
226+
// - Can be any characters except path separators (/ and \)
227+
// - Length: 1-254 characters
228+
// - Must not be empty or whitespace only
229+
[GeneratedRegex(@"^[^/\\]{1,254}$", RegexOptions.Compiled)]
230+
internal static partial Regex GetProjectNameRegex();
227231

228232
public static bool IsProjectNameValid(string projectName)
229233
{
230-
var regex = GetAssemblyNameRegex();
234+
if (string.IsNullOrWhiteSpace(projectName))
235+
{
236+
return false;
237+
}
238+
239+
var regex = GetProjectNameRegex();
231240
return regex.IsMatch(projectName);
232241
}
233242
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Cli.Commands;
5+
using Xunit;
6+
7+
namespace Aspire.Cli.Tests.Commands;
8+
9+
public class ProjectNameValidatorTests
10+
{
11+
[Theory]
12+
[InlineData("项目1", true)] // Chinese
13+
[InlineData("Проект1", true)] // Cyrillic
14+
[InlineData("プロジェクト1", true)] // Japanese
15+
[InlineData("مشروع1", true)] // Arabic
16+
[InlineData("Project_1", true)] // Latin with underscore
17+
[InlineData("Project-1", true)] // Latin with dash
18+
[InlineData("Project.1", true)] // Latin with dot
19+
[InlineData("MyApp", true)] // Simple ASCII
20+
[InlineData("A", true)] // Single character
21+
[InlineData("1", true)] // Single number
22+
[InlineData("プ", true)] // Single Unicode character
23+
[InlineData("Test123", true)] // Mixed letters and numbers
24+
[InlineData("My_Cool-Project.v2", true)] // Complex valid name
25+
[InlineData("Project:1", true)] // Colon (now allowed)
26+
[InlineData("Project*1", true)] // Asterisk (now allowed)
27+
[InlineData("Project?1", true)] // Question mark (now allowed)
28+
[InlineData("Project\"1", true)] // Quote (now allowed)
29+
[InlineData("Project<1", true)] // Less than (now allowed)
30+
[InlineData("Project>1", true)] // Greater than (now allowed)
31+
[InlineData("Project|1", true)] // Pipe (now allowed)
32+
[InlineData("Project ", true)] // Ends with space (now allowed)
33+
[InlineData(" Project", true)] // Starts with space (now allowed)
34+
[InlineData("Pro ject", true)] // Space in middle (now allowed)
35+
[InlineData("-Project", true)] // Starts with dash (now allowed)
36+
[InlineData("Project-", true)] // Ends with dash (now allowed)
37+
[InlineData(".Project", true)] // Starts with dot (now allowed)
38+
[InlineData("Project.", true)] // Ends with dot (now allowed)
39+
[InlineData("_Project", true)] // Starts with underscore (now allowed)
40+
[InlineData("Project_", true)] // Ends with underscore (now allowed)
41+
public void IsProjectNameValid_ValidNames_ReturnsTrue(string projectName, bool expected)
42+
{
43+
// Act
44+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
45+
46+
// Assert
47+
Assert.Equal(expected, result);
48+
}
49+
50+
[Theory]
51+
[InlineData("Project/1", false)] // Forward slash (path separator)
52+
[InlineData("Project\\1", false)] // Backslash (path separator)
53+
[InlineData("", false)] // Empty string
54+
[InlineData(" ", false)] // Space only
55+
[InlineData(" ", false)] // Multiple spaces only
56+
[InlineData("\t", false)] // Tab only
57+
[InlineData("\n", false)] // Newline only
58+
public void IsProjectNameValid_InvalidNames_ReturnsFalse(string projectName, bool expected)
59+
{
60+
// Act
61+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
62+
63+
// Assert
64+
Assert.Equal(expected, result);
65+
}
66+
67+
[Fact]
68+
public void IsProjectNameValid_MaxLength254_ReturnsTrue()
69+
{
70+
// Arrange
71+
var projectName = new string('A', 254);
72+
73+
// Act
74+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
75+
76+
// Assert
77+
Assert.True(result);
78+
}
79+
80+
[Fact]
81+
public void IsProjectNameValid_Length255_ReturnsFalse()
82+
{
83+
// Arrange
84+
var projectName = new string('A', 255);
85+
86+
// Act
87+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
88+
89+
// Assert
90+
Assert.False(result);
91+
}
92+
93+
[Theory]
94+
[InlineData("项目测试名称很长的中文项目名称")] // Long Chinese name
95+
[InlineData("очень_длинное_русское_имя_проекта")] // Long Russian name
96+
[InlineData("とても長い日本語のプロジェクト名")] // Long Japanese name
97+
[InlineData("اسم_مشروع_طويل_جدا_بالعربية")] // Long Arabic name
98+
public void IsProjectNameValid_LongUnicodeNames_ReturnsTrue(string projectName)
99+
{
100+
// Act
101+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
102+
103+
// Assert
104+
Assert.True(result, $"Unicode project name should be valid: {projectName}");
105+
}
106+
107+
[Theory]
108+
[InlineData("Ελληνικά", true)] // Greek
109+
[InlineData("עברית", true)] // Hebrew
110+
[InlineData("हिन्दी", true)] // Hindi
111+
[InlineData("ไทย", true)] // Thai
112+
[InlineData("한국어", true)] // Korean
113+
[InlineData("Türkçe", true)] // Turkish
114+
[InlineData("Português", true)] // Portuguese with accent
115+
[InlineData("Français", true)] // French with accent
116+
[InlineData("Español", true)] // Spanish with accent
117+
[InlineData("Deutsch", true)] // German
118+
public void IsProjectNameValid_VariousLanguages_ReturnsTrue(string projectName, bool expected)
119+
{
120+
// Act
121+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
122+
123+
// Assert
124+
Assert.Equal(expected, result);
125+
}
126+
127+
[Theory]
128+
[InlineData("Test123-Project_Name.v2")] // Complex valid with all allowed characters
129+
[InlineData("A1-B2_C3.D4")] // Mixed with separators
130+
[InlineData("项目-测试_版本.1")] // Unicode with separators
131+
public void IsProjectNameValid_ComplexValidNames_ReturnsTrue(string projectName)
132+
{
133+
// Act
134+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
135+
136+
// Assert
137+
Assert.True(result, $"Complex valid project name should be valid: {projectName}");
138+
}
139+
140+
[Theory]
141+
[InlineData("Test..Name")] // Double dot
142+
[InlineData("Test--Name")] // Double dash
143+
[InlineData("Test__Name")] // Double underscore
144+
public void IsProjectNameValid_ConsecutiveSpecialChars_ReturnsTrue(string projectName)
145+
{
146+
// These should be valid as the spec doesn't prohibit consecutive allowed characters
147+
// Act
148+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
149+
150+
// Assert
151+
Assert.True(result, $"Consecutive allowed characters should be valid: {projectName}");
152+
}
153+
154+
[Theory]
155+
[InlineData("My/Project")] // Forward slash in middle
156+
[InlineData("/MyProject")] // Forward slash at start
157+
[InlineData("MyProject/")] // Forward slash at end
158+
[InlineData("My\\Project")] // Backslash in middle
159+
[InlineData("\\MyProject")] // Backslash at start
160+
[InlineData("MyProject\\")] // Backslash at end
161+
[InlineData("My/Project/Name")] // Multiple forward slashes
162+
[InlineData("My\\Project\\Name")] // Multiple backslashes
163+
[InlineData("My/Project\\Name")] // Mixed path separators
164+
public void IsProjectNameValid_PathSeparators_ReturnsFalse(string projectName)
165+
{
166+
// Act
167+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
168+
169+
// Assert
170+
Assert.False(result, $"Project name with path separators should be invalid: {projectName}");
171+
}
172+
}

0 commit comments

Comments
 (0)