Skip to content
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);
}
}
171 changes: 171 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/ProjectNameValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case expects spaces-only strings to be invalid, but the current regex ^[^/\\]{1,254}$ will actually match single spaces since space is not a path separator. The test expectation contradicts the actual regex behavior.

Copilot uses AI. Check for mistakes.
[InlineData(" ", false)] // Multiple spaces only
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case expects multiple spaces to be invalid, but the regex ^[^/\\]{1,254}$ will match multiple spaces since spaces are not path separators. The test expectation contradicts the actual regex behavior.

Suggested change
[InlineData(" ", false)] // Multiple spaces only
[InlineData(" ", true)] // Multiple spaces only

Copilot uses AI. Check for mistakes.
[InlineData("\t", false)] // Tab only
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case expects tab-only strings to be invalid, but the regex ^[^/\\]{1,254}$ will match tabs since tab is not a path separator. The test expectation contradicts the actual regex behavior.

Suggested change
[InlineData("\t", false)] // Tab only
[InlineData("\t", true)] // Tab only

Copilot uses AI. Check for mistakes.
[InlineData("\n", false)] // Newline only
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case expects newline-only strings to be invalid, but the regex ^[^/\\]{1,254}$ will match newlines since newline is not a path separator. The test expectation contradicts the actual regex behavior.

Suggested change
[InlineData("\n", false)] // Newline only
[InlineData("\n", true)] // Newline only

Copilot uses AI. Check for mistakes.
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