using CliWrap; using ErsatzTV.Core.Scheduling.Engine; using ErsatzTV.Core.Scheduling.ScriptedScheduling; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Core.Tests.Scheduling.ScriptedScheduling; /// /// The two Core halves of the scripted-build credential (ersatztv#913): the session store that mints and /// validates a build-scoped token, and the process launch that hands it to the user's program. The filter /// half — that the token is accepted on ScriptedScheduleController and nowhere else — is /// ErsatzTV.Tests/Filters/ScriptedScheduleAuthorizationTests. /// [TestFixture] public class ScriptedBuildTokenTests { [Test] public void StartSession_Mints_A_256_Bit_Token_Distinct_From_The_Build_Id_And_Per_Build() { var service = new ScriptedPlayoutBuilderService(); ScriptedBuildSession first = service.StartSession(Substitute.For()); ScriptedBuildSession second = service.StartSession(Substitute.For()); first.Token.Length.ShouldBe(64); first.Token.ShouldAllBe(c => Uri.IsHexDigit(c)); first.Token.ShouldNotContain(first.BuildId.ToString("N"), Case.Insensitive); second.Token.ShouldNotBe(first.Token); second.BuildId.ShouldNotBe(first.BuildId); } [Test] public void Token_Is_Valid_Only_For_Its_Own_Running_Build() { var service = new ScriptedPlayoutBuilderService(); ScriptedBuildSession mine = service.StartSession(Substitute.For()); ScriptedBuildSession other = service.StartSession(Substitute.For()); service.IsBuildTokenValid(mine.BuildId, mine.Token).ShouldBeTrue(); service.IsBuildTokenValid(mine.BuildId, other.Token).ShouldBeFalse(); service.IsBuildTokenValid(mine.BuildId, mine.Token.ToUpperInvariant()).ShouldBeFalse(); service.IsBuildTokenValid(mine.BuildId, mine.Token[..^1]).ShouldBeFalse(); service.IsBuildTokenValid(mine.BuildId, "").ShouldBeFalse(); service.IsBuildTokenValid(mine.BuildId, null).ShouldBeFalse(); service.IsBuildTokenValid(Guid.NewGuid(), mine.Token).ShouldBeFalse(); service.EndSession(mine.BuildId); service.IsBuildTokenValid(mine.BuildId, mine.Token).ShouldBeFalse(); service.IsBuildTokenValid(other.BuildId, other.Token).ShouldBeTrue(); } [Test] public void A_Mocked_Session_Has_No_Token_That_Validates() { // MockSession is the in-process test seam; it registers no token, so no presented value — the empty // string included — authenticates against it. var service = new ScriptedPlayoutBuilderService(); var buildId = Guid.NewGuid(); service.MockSession(Substitute.For(), buildId).ShouldBeTrue(); service.GetEngine(buildId).ShouldNotBeNull(); service.IsBuildTokenValid(buildId, "").ShouldBeFalse(); service.IsBuildTokenValid(buildId, "anything").ShouldBeFalse(); } [Test] public void Launch_Hands_The_Token_To_The_Program_In_The_Environment_And_Not_On_The_Command_Line() { const string Token = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; string[] arguments = ["http://localhost:8409", Guid.NewGuid().ToString(), "reset", "user-arg"]; Command command = ScriptedPlayoutBuilder.CreateCommand("/scripts/run.py", arguments, Token); command.EnvironmentVariables.ShouldContainKeyAndValue(ScriptedBuildToken.EnvironmentVariable, Token); command.Arguments.ShouldNotContain(Token); // The published argument contract is unchanged: host, build id, mode, then the user's own arguments. command.Arguments.ShouldBe(string.Join(' ', arguments)); command.TargetFilePath.ShouldBe("/scripts/run.py"); command.Validation.ShouldBe(CommandResultValidation.None); } }