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);
}
}