| File: Templating\TemplateSourceConfigurationTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; using Aspire.Cli.Templating; using Microsoft.DotNet.RemoteExecutor; namespace Aspire.Cli.Tests.Templating; public class TemplateSourceConfigurationTests(ITestOutputHelper outputHelper) { [Theory] [InlineData(true, "../escape")] [InlineData(false, "../project/templates")] public void ValidateRepositoryPathRejectsLogicalEscapeRegardlessOfWorkingDirectory(bool volumeRoot, string path) { using var workspace = TemporaryWorkspace.Create(outputHelper); var options = new RemoteInvokeOptions(); options.StartInfo.WorkingDirectory = volumeRoot ? Path.GetPathRoot(workspace.Path)! : workspace.CreateDirectory("project").FullName; using (RemoteExecutor.Invoke(static repositoryPath => { Assert.Throws<TemplateSourceConfigurationException>(() => TemplateSourceConfiguration.ValidateRepositoryPath(repositoryPath, "templateSources.example.path")); }, path, options)) { } } [Theory] [InlineData(".", ".")] [InlineData("./templates", "templates")] [InlineData("templates/../api", "api")] [InlineData("templates/sub/../..", ".")] public void RepositoryRelativePathAllowsNormalizationWithinRoot(string path, string expected) { var root = Path.GetPathRoot(Path.GetFullPath("."))!; Assert.Equal(expected, TemplateSource.GetRepositoryRelativePath(path, root)); } [Theory] [InlineData("../escape")] [InlineData("templates/../../escape")] [InlineData("./../escape")] public void RepositoryRelativePathRejectsLogicalEscapeAtVolumeRoot(string path) { var root = Path.GetPathRoot(Path.GetFullPath("."))!; Assert.Throws<TemplateDiscoveryException>(() => TemplateSource.GetRepositoryRelativePath(path, root)); } [Theory] [InlineData("\"/absolute/path\"", "templateSources.example")] [InlineData("\"https://example.test/templates\"", "templateSources.example")] [InlineData("{}", "templateSources.example")] [InlineData("""{"path":"relative"}""", "templateSources.example")] [InlineData("""{"ref":"main"}""", "templateSources.example")] [InlineData("""{"repository":false}""", "templateSources.example.repository")] [InlineData("""{"repository":"http://example.test/repo"}""", "templateSources.example.repository")] [InlineData("""{"repository":"https://example.test/repo","ref":42}""", "templateSources.example.ref")] [InlineData("""{"repository":"https://example.test/repo","path":"../escape"}""", "templateSources.example.path")] public void ParseRejectsInvalidCompleteSourceWithSettingPath(string json, string settingPath) { using var document = JsonDocument.Parse(json); var exception = Assert.Throws<TemplateSourceConfigurationException>(() => TemplateSourceConfiguration.Parse(document.RootElement, "templateSources.example", requireComplete: true)); Assert.Contains($"'{settingPath}'", exception.Message); } [Theory] [InlineData("{}")] [InlineData("""{"ref":"release/17.0"}""")] [InlineData("""{"path":"templates/api"}""")] [InlineData("""{"ref":"main","path":"templates"}""")] public void ParsePermitsIncompleteObjectsOnlyDuringEditing(string json) { using var document = JsonDocument.Parse(json); var source = TemplateSourceConfiguration.Parse(document.RootElement, "templateSources.example", requireComplete: false); Assert.Null(source.Template); Assert.Throws<TemplateSourceConfigurationException>(() => TemplateSourceConfiguration.Parse(document.RootElement, "templateSources.example", requireComplete: true)); } [Fact] public void ParseAcceptsAbsoluteLocalPathObject() { var directory = Path.GetFullPath("."); using var document = JsonDocument.Parse(JsonSerializer.Serialize(new { path = directory })); var source = TemplateSourceConfiguration.Parse(document.RootElement, "templateSources.example", requireComplete: true); Assert.Equal(new TemplateSource(directory, null, null), source.Template); } [Theory] [InlineData("""{"repository":"https://example.test/repo","ref":null,"path":null}""")] [InlineData("""{"Repository":"https://example.test/repo","Ref":null,"Path":null}""")] [InlineData("""{"repository":"https://example.test/repo","futureFeature":{"enabled":true}}""")] public void ParseUsesConfigurationDeserializationConventions(string json) { using var document = JsonDocument.Parse(json); var source = TemplateSourceConfiguration.Parse(document.RootElement, "templateSources.example", requireComplete: true); Assert.Equal(new TemplateSource(".", "https://example.test/repo", null), source.Template); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("name.with.dots")] [InlineData("name:with:colons")] [InlineData("name\n")] public void ValidateNameRejectsAmbiguousConfigKeys(string name) => Assert.Throws<TemplateSourceConfigurationException>(() => TemplateSourceConfiguration.ValidateName(name)); }