Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,38 @@ public void Test_GetSpecRepoPath()
Assert.That(result.EndsWith("TypeSpecTestData"), Is.True);
}

[TestCase("https://github.com/Azure/azure-rest-api-specs.git")]
[TestCase("https://github.com/Azure/azure-rest-api-specs")]
[TestCase("https://github.com/myuser/azure-rest-api-specs.git")]
[TestCase("https://github.com/Azure/azure-rest-api-specs-pr.git")]
[TestCase("https://github.com/myuser/azure-rest-api-specs-pr.git")]
[TestCase("git@github.com:Azure/azure-rest-api-specs.git")]
[TestCase("git@github.com:myuser/azure-rest-api-specs.git")]
[Test]
public void Test_IsRepoPathForSpecRepo(Uri repo)
{
var gitHelper = CreateGitHelper(repo);
var helper = new TypeSpecHelper(gitHelper);
Assert.That(helper.IsRepoPathForSpecRepo("unused because of mock"), "is a specs repo (public or private)");
}

[TestCase("https://github.com/Azure/azure-rest-api-specs-pr.git")]
[TestCase("https://github.com/myuser/azure-rest-api-specs-pr.git")]
[TestCase("git@github.com:Azure/azure-rest-api-specs-pr.git")]
[TestCase("git@github.com:myuser/azure-rest-api-specs-pr.git")]
[TestCase("git@github.com:Azure/azure-sdk-for-php.git")]
[Test]
public void Test_IsRepoPathForPublicSpecRepo(Uri repo)
{
var helper = new TypeSpecHelper(CreateGitHelper(repo));
Assert.That(!helper.IsRepoPathForPublicSpecRepo("unused because of the mock"), "not the public specs repo");
}

private static IGitHelper CreateGitHelper(Uri getRepoRemoteUri)
{
var gitHelperMock = new Mock<IGitHelper>();
gitHelperMock.Setup(ghm => ghm.GetRepoRemoteUri(It.IsAny<string>())).Returns(getRepoRemoteUri);
return gitHelperMock.Object;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void GetCommand_ShouldReturnCommand()
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);

// Act
var command = tool.GetCommand();
Expand All @@ -34,7 +34,7 @@ public async Task Init_WithInvalidTemplate_ShouldReturnError()
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);

var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "invalid-template", serviceNamespace: "MyService", isCli: false);

Expand All @@ -52,7 +52,7 @@ public async Task Init_WithInvalidServiceNamespace_ShouldReturnError()
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);

var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "azure-core", serviceNamespace: "", isCli: false);

Expand All @@ -64,20 +64,91 @@ public async Task Init_WithInvalidServiceNamespace_ShouldReturnError()
}

[Test]
public async Task Init_WithNonExistentDirectory_ShouldReturnError()
public async Task Init_WithNonEmptyDirectory_ShouldReturnError()
{
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");

var result = await tool.InitTypeSpecProjectAsync(outputDirectory: Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}"), template: "azure-core", serviceNamespace: "MyService", isCli: false);
Directory.CreateDirectory(tempDir);

Assert.Multiple(() =>
try
{
Assert.That(result.IsSuccessful, Is.False);
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
});
await File.WriteAllTextAsync(Path.Join(tempDir, "somefile.txt"), "some file's contents");

var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);

Assert.Multiple(() =>
{
Assert.That(result.IsSuccessful, Is.False);
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
});
}
finally
{
Directory.Delete(tempDir, true);
}
}

[Test]
public async Task Init_IncorrectGitRepo()
{
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(false), logger, outputService);
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");

try
{
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);

Assert.Multiple(() =>
{
Assert.That(result.IsSuccessful, Is.False);
Assert.That(result.ResponseError, Is.EqualTo($"Failed: Invalid --output-directory, must be under the azure-rest-api-specs or azure-rest-api-specs-pr repo"
));
});
}
finally
{
Directory.Delete(tempDir, true);
}
}

[Test]
public async Task Init_NotUnderSpecifications()
{
var npxHelper = new Mock<INpxHelper>().Object;
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
var outputService = new Mock<IOutputHelper>().Object;
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(true), logger, outputService);
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");

try
{
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);

Assert.Multiple(() =>
{
Assert.That(result.IsSuccessful, Is.False);
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
Assert.That(result.ResponseError, Is.EqualTo($"Failed: Invalid --output-directory, must be under <azure-rest-api-specs or azure-rest-api-specs-pr>{Path.DirectorySeparatorChar}specification"));
});
}
finally
{
Directory.Delete(tempDir, true);
}
}

private static ITypeSpecHelper CreateTypeSpecHelper(bool isSpecRepo = false)
{
var mock = new Mock<ITypeSpecHelper>();
mock.Setup(m => m.IsRepoPathForSpecRepo(It.IsAny<string>())).Returns(isSpecRepo);
return mock.Object;
}
}
}
8 changes: 5 additions & 3 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public interface IGitHelper
public string DiscoverRepoRoot(string path);
public string GetRepoName(string path);
}

public class GitHelper(IGitHubService gitHubService, ILogger<GitHelper> logger) : IGitHelper
{
private readonly ILogger<GitHelper> logger = logger;
Expand Down Expand Up @@ -66,7 +67,8 @@ public async Task<string> GetRepoOwnerNameAsync(string path, bool findUpstreamPa
repoName = segments[^1].TrimEnd(".git".ToCharArray());
}

if(findUpstreamParent) {
if (findUpstreamParent)
{
// Check if the repo is a fork and get the parent repo
var parentRepoUrl = await gitHubService.GetGitHubParentRepoUrlAsync(repoOwner, repoName);
logger.LogDebug($"Parent repo URL: {parentRepoUrl}");
Expand All @@ -86,7 +88,7 @@ public async Task<string> GetRepoOwnerNameAsync(string path, bool findUpstreamPa
}

throw new InvalidOperationException("Unable to determine repository owner.");
}
}

public string DiscoverRepoRoot(string path)
{
Expand All @@ -95,7 +97,7 @@ public string DiscoverRepoRoot(string path)
{
throw new InvalidOperationException($"No git repository found at or above the path: {path}");
}

// Repository.Discover returns the path to .git directory
// The repository root is the parent directory of .git
var gitDir = new DirectoryInfo(repoPath);
Expand Down
34 changes: 31 additions & 3 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/TypeSpecHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text.RegularExpressions;
using Azure.Sdk.Tools.Cli.Models;

namespace Azure.Sdk.Tools.Cli.Helpers
Expand All @@ -8,14 +9,35 @@ public interface ITypeSpecHelper
{
public bool IsValidTypeSpecProjectPath(string path);
public bool IsTypeSpecProjectForMgmtPlane(string Path);

/// <summary>
/// Checks if the path is within either the azure-rest-api-specs repo.
/// This should also work for forks of these repos.
/// </summary>
/// <param name="path">Path within a repo</param>
/// <returns>true if within the azure-rest-api-specs repo, false otherwise</returns>
public bool IsRepoPathForPublicSpecRepo(string path);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

who is using this function?


/// <summary>
/// Checks if the path is within either the azure-rest-api-specs or azure-rest-api-specs-pr repo.
/// This should also work for forks of these repos.
/// </summary>
/// <param name="path">Path within a repo</param>
/// <returns>true if one of our specs repos, false otherwise</returns>
public bool IsRepoPathForSpecRepo(string path);

public string GetSpecRepoRootPath(string path);
public string GetTypeSpecProjectRelativePath(string typeSpecProjectPath);
}
public class TypeSpecHelper : ITypeSpecHelper
public partial class TypeSpecHelper : ITypeSpecHelper
{
[GeneratedRegex("azure-rest-api-specs(-pr){0,1}(.git){0,1}$")]
private static partial Regex RestApiSpecsPublicOrPrivateRegex();

[GeneratedRegex("azure-rest-api-specs{0,1}(.git){0,1}$")]
private static partial Regex RestApiSpecsPublicRegex();

private IGitHelper _gitHelper;
private static readonly string SPEC_REPO_NAME = "azure-rest-api-specs";

public TypeSpecHelper(IGitHelper gitHelper)
{
Expand All @@ -36,7 +58,13 @@ public bool IsTypeSpecProjectForMgmtPlane(string Path)
public bool IsRepoPathForPublicSpecRepo(string path)
{
var uri = _gitHelper.GetRepoRemoteUri(path);
return uri.ToString().Contains(SPEC_REPO_NAME);
return RestApiSpecsPublicRegex().IsMatch(uri.ToString());
}

public bool IsRepoPathForSpecRepo(string path)
{
var uri = _gitHelper.GetRepoRemoteUri(path);
return RestApiSpecsPublicOrPrivateRegex().IsMatch(uri.ToString());
}

public string GetSpecRepoRootPath(string path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ public override string ToString()
}
else
{
return $"### TypeSpec Project Path: {TypeSpecProjectPath}";
return string.Join(
Environment.NewLine,
[
$"### TypeSpec Project Path: {TypeSpecProjectPath}",
string.Empty,
..this.NextSteps ?? Enumerable.Empty<string>()
]
);
}
}
}
Expand Down
Loading
Loading