Skip to content

[release/9.4] Simplify Aspire CLI project name validation to only reject path separators #10832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,21 @@ public virtual async Task<ITemplate> 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)]
internal static partial Regex GetAssemblyNameRegex();
// 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)
{
var regex = GetAssemblyNameRegex();
if (string.IsNullOrWhiteSpace(projectName))
{
return false;
}

var regex = GetProjectNameRegex();
return regex.IsMatch(projectName);
}
}
172 changes: 172 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// 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;
using Xunit;

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}");
}
}
Loading