using FluentAssertions; using TeamsISO.App.Services; namespace TeamsISO.App.Tests.Services; /// /// Token-expansion + sanitization tests for . /// We don't touch / /// here because those round-trip through %LOCALAPPDATA% file IO; the file-IO /// path is exercised at integration test time. The token expander is pure and /// easy to cover. /// public class OutputNameTemplateTests { private static readonly Guid TestId = new("11223344-5566-7788-99aa-bbccddeeff00"); [Fact] public void Render_DefaultTemplate_ProducesGuidPrefix() { var name = OutputNameTemplate.Render(OutputNameTemplate.DefaultTemplate, TestId, "Jane"); // Default is "TEAMSISO_{guid}" → first 8 hex of TestId, uppercase. name.Should().Be("TEAMSISO_11223344"); } [Fact] public void Render_NameToken_UsesSanitizedDisplayName() { var name = OutputNameTemplate.Render("TEAMSISO_{name}", TestId, "Jane Doe"); name.Should().Be("TEAMSISO_Jane_Doe", because: "spaces become underscores in the sanitizer"); } [Fact] public void Render_NameToken_StripsSpecialCharacters() { // NDI accepts more chars than we allow, but we keep it conservative // (alphanumeric + underscore + hyphen + period). Anything else is dropped // or converted to underscore (whitespace). var name = OutputNameTemplate.Render("{name}", TestId, "Jane (PM) — Lead!"); // Expect: "Jane_PM__Lead" — parens dropped, em-dash dropped, exclamation dropped. // Whitespace runs collapse into adjacent underscores. name.Should().NotContain("("); name.Should().NotContain(")"); name.Should().NotContain("—"); name.Should().NotContain("!"); name.Should().Contain("Jane"); name.Should().Contain("Lead"); } [Fact] public void Render_GuidToken_IsUppercaseFirst8() { var name = OutputNameTemplate.Render("{guid}", TestId, "Jane"); name.Should().Be("11223344"); } [Fact] public void Render_MachineToken_UsesEnvironmentMachineName() { var name = OutputNameTemplate.Render("{machine}", TestId, "Jane"); // Sanitization may transform spaces in machine names, so just assert non-empty // and that it contains the machine name's alphanumeric-ish chars. name.Should().NotBeNullOrEmpty(); // MachineName itself is sanitized in render — equality check would be brittle. } [Fact] public void Render_TimestampToken_HasExpectedShape() { var name = OutputNameTemplate.Render("session_{timestamp}", TestId, "Jane"); // yyyyMMdd_HHmmss is 15 chars + underscore separator = 16. // Combined with "session_" prefix → length should be at least 23. name.Should().StartWith("session_"); name.Length.Should().BeGreaterThan("session_".Length + 14); } [Fact] public void Render_MultipleTokens_AllExpand() { var name = OutputNameTemplate.Render("{name}_{guid}_{machine}", TestId, "Jane"); name.Should().StartWith("Jane_11223344_"); name.Should().NotContain("{"); name.Should().NotContain("}"); } [Fact] public void Render_TemplateWithNoTokens_PassesThrough() { var name = OutputNameTemplate.Render("STATIC_NAME", TestId, "Jane"); name.Should().Be("STATIC_NAME"); } [Fact] public void Render_EmptyDisplayName_DegradesToEmptyToken() { var name = OutputNameTemplate.Render("PFX_{name}", TestId, ""); name.Should().Be("PFX_"); } [Theory] [InlineData("Jane123")] [InlineData("Jane-Doe")] [InlineData("Jane.PM")] public void Render_AllowedCharactersPreserved(string displayName) { var name = OutputNameTemplate.Render("{name}", TestId, displayName); name.Should().Be(displayName, because: "alphanumeric, underscore, hyphen, period are all valid NDI chars"); } }