// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Xml.Linq;
using Aspire.Hosting.Maui.Utilities;
namespace Aspire.Hosting.Tests;
/// <summary>
/// Tests for MauiEnvironmentHelper utility methods including targets file generation,
/// semicolon encoding, and filename sanitization.
/// </summary>
public class MauiEnvironmentHelperTests
{
[Fact]
public void GenerateAndroidTargetsFileContent_ProducesValidXml()
{
var envVars = new Dictionary<string, string>
{
["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317",
["MY_VAR"] = "hello"
};
var content = MauiEnvironmentHelper.GenerateAndroidTargetsFileContent(envVars);
// Should be valid XML
var doc = XDocument.Parse(content);
Assert.NotNull(doc.Root);
Assert.Equal("Project", doc.Root.Name.LocalName);
}
[Fact]
public void GenerateAndroidTargetsFileContent_ContainsEnvironmentVariablesInItemGroup()
{
var envVars = new Dictionary<string, string>
{
["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317",
["MY_VAR"] = "hello"
};
var content = MauiEnvironmentHelper.GenerateAndroidTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var items = doc.Descendants("_GeneratedAndroidEnvironment").ToList();
Assert.Equal(2, items.Count);
// Items should be ordered by key
Assert.Equal("MY_VAR=hello", items[0].Attribute("Include")?.Value);
Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317", items[1].Attribute("Include")?.Value);
}
[Fact]
public void GenerateAndroidTargetsFileContent_ContainsAspireTargetDefinition()
{
var envVars = new Dictionary<string, string>
{
["MY_VAR"] = "value"
};
var content = MauiEnvironmentHelper.GenerateAndroidTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var target = doc.Descendants("Target")
.FirstOrDefault(t => t.Attribute("Name")?.Value == "AspireGenerateAndroidEnvironmentFiles");
Assert.NotNull(target);
Assert.Equal("_GenerateEnvironmentFiles", target.Attribute("BeforeTargets")?.Value);
}
[Fact]
public void GenerateAndroidTargetsFileContent_EncodesEnvironmentFilePath()
{
var envVars = new Dictionary<string, string>
{
["KEY"] = "value"
};
var content = MauiEnvironmentHelper.GenerateAndroidTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var writeLines = doc.Descendants("WriteLinesToFile").FirstOrDefault();
Assert.NotNull(writeLines);
Assert.Equal("$(IntermediateOutputPath)__aspire_environment__.txt", writeLines.Attribute("File")?.Value);
}
[Fact]
public void GenerateAndroidTargetsFileContent_EncodesSemicolonsInValues()
{
var envVars = new Dictionary<string, string>
{
["LITERAL_ESCAPE"] = "already%3Bencoded",
["PATH"] = "/usr/bin;/usr/local/bin"
};
var content = MauiEnvironmentHelper.GenerateAndroidTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var items = doc.Descendants("_GeneratedAndroidEnvironment")
.Select(item => item.Attribute("Include")?.Value)
.ToList();
Assert.Equal(
[
"LITERAL_ESCAPE=already%253Bencoded",
"PATH=/usr/bin%3B/usr/local/bin"
],
items);
}
[Fact]
public void GenerateiOSTargetsFileContent_ProducesValidXml()
{
var envVars = new Dictionary<string, string>
{
["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317",
["MY_VAR"] = "hello"
};
var content = MauiEnvironmentHelper.GenerateiOSTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
Assert.NotNull(doc.Root);
Assert.Equal("Project", doc.Root.Name.LocalName);
}
[Fact]
public void GenerateiOSTargetsFileContent_ContainsMlaunchEnvironmentVariables()
{
var envVars = new Dictionary<string, string>
{
["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317",
["MY_VAR"] = "hello"
};
var content = MauiEnvironmentHelper.GenerateiOSTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var items = doc.Descendants("MlaunchEnvironmentVariables").ToList();
Assert.Equal(2, items.Count);
// Items should be ordered by key
Assert.Equal("MY_VAR=hello", items[0].Attribute("Include")?.Value);
Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317", items[1].Attribute("Include")?.Value);
}
[Fact]
public void GenerateiOSTargetsFileContent_ContainsDiagnosticTarget()
{
var envVars = new Dictionary<string, string>
{
["MY_VAR"] = "value"
};
var content = MauiEnvironmentHelper.GenerateiOSTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var target = doc.Descendants("Target")
.FirstOrDefault(t => t.Attribute("Name")?.Value == "AspireLogMlaunchEnvironmentVariables");
Assert.NotNull(target);
Assert.Equal("PrepareForBuild", target.Attribute("AfterTargets")?.Value);
}
[Fact]
public void GenerateiOSTargetsFileContent_EncodesSemicolonsInValues()
{
var envVars = new Dictionary<string, string>
{
["LITERAL_ESCAPE"] = "already%3Bencoded",
["PATH"] = "/usr/bin;/usr/local/bin"
};
var content = MauiEnvironmentHelper.GenerateiOSTargetsFileContent(envVars);
var doc = XDocument.Parse(content);
var items = doc.Descendants("MlaunchEnvironmentVariables")
.Select(item => item.Attribute("Include")?.Value)
.ToList();
Assert.Equal(
[
"LITERAL_ESCAPE=already%253Bencoded",
"PATH=/usr/bin%3B/usr/local/bin"
],
items);
}
[Theory]
[InlineData("simple-value", "simple-value", false)]
[InlineData("has;semicolons", "has%3Bsemicolons", true)]
[InlineData("multiple;semi;colons", "multiple%3Bsemi%3Bcolons", true)]
[InlineData("literal%3Bescape", "literal%253Bescape", true)]
[InlineData("literal%percent;and;semicolons", "literal%25percent%3Band%3Bsemicolons", true)]
[InlineData("", "", false)]
[InlineData("no-special-chars", "no-special-chars", false)]
public void EncodeMSBuildItemValue_EncodesCorrectly(string input, string expectedOutput, bool expectedWasEncoded)
{
var result = MauiEnvironmentHelper.EncodeMSBuildItemValue(input, out var wasEncoded);
Assert.Equal(expectedOutput, result);
Assert.Equal(expectedWasEncoded, wasEncoded);
}
[Theory]
[InlineData("simple-name", "simple-name")]
[InlineData("name-with-dots.here", "name-with-dots.here")]
[InlineData("valid_file_name", "valid_file_name")]
public void SanitizeFileName_ValidNames_ReturnsUnchanged(string input, string expected)
{
var result = MauiEnvironmentHelper.SanitizeFileName(input);
Assert.Equal(expected, result);
}
[Fact]
public void SanitizeFileName_InvalidChars_ReplacesWithUnderscore()
{
// Build a test string with a printable character that's invalid on the current platform.
// Skip '\0' since it has special behavior in .NET string comparisons.
var invalidChar = Path.GetInvalidFileNameChars()
.FirstOrDefault(c => c != '\0');
if (invalidChar == '\0')
{
Assert.Skip("No printable invalid filename characters on this platform");
return;
}
var input = $"name{invalidChar}test";
var result = MauiEnvironmentHelper.SanitizeFileName(input);
Assert.DoesNotContain(invalidChar.ToString(), result);
Assert.Equal("name_test", result);
}
}