using System.Text.Json; using FluentAssertions; using Xunit; namespace BluejayInfraLint.Tests; [Trait("Category", "Unit")] public sealed class PiSignagePlayerArtifactTests { private static readonly string Root = FindRepoRoot(); private static readonly string AppRoot = Path.Combine(Root, "apps", "fc-signage-pi-player"); public static TheoryData RequiredArtifacts => new() { "README.md", "systemd/flowercore-signage-player-pi.service", "systemd/flowercore-signage-player-pi-hdmi.service", "systemd/flowercore-signage-bootstrap.service", "systemd/flowercore-signage-renew.service", "systemd/flowercore-signage-renew.timer", "systemd/flowercore-signage-detect-display.service", "systemd/flowercore-signage-detect-display.timer", "systemd/99-flowercore-signage-hdmi.rules", "chromium-policies/flowercore-signage.json", "scripts/flowercore-signage-launch.sh", "scripts/flowercore-signage-prelaunch.sh", "scripts/flowercore-signage-bootstrap.sh", "scripts/flowercore-signage-renew-cert.sh", "scripts/flowercore-signage-hdmi-respond.sh", "scripts/fc-signage-detect-display", }; [Theory] [MemberData(nameof(RequiredArtifacts))] public void RequiredArtifacts_ArePresent(string relativePath) { File.Exists(Path.Combine(AppRoot, relativePath)).Should().BeTrue(relativePath); } [Fact] public void PlayerService_UsesExpectedRestartAndMemoryGuards() { var unit = Read("systemd/flowercore-signage-player-pi.service"); unit.Should().Contain("Restart=always"); unit.Should().Contain("RestartSec=10s"); unit.Should().Contain("StartLimitBurst=5"); unit.Should().Contain("StartLimitIntervalSec=300s"); unit.Should().Contain("MemoryMax=2G"); } [Fact] public void PlayerService_IsGatedByNodeIdentityAndMtlsCertificate() { var unit = Read("systemd/flowercore-signage-player-pi.service"); unit.Should().Contain("ConditionPathExists=/etc/flowercore/signage-node.json"); unit.Should().Contain("ConditionPathExists=/etc/fc-signage-player/client.p12"); unit.Should().Contain("ExecStartPre=/usr/local/bin/flowercore-signage-prelaunch.sh"); } [Fact] public void LaunchScript_TriesEmbedThenFallsBackToBarePlayerRoute() { var script = Read("scripts/flowercore-signage-launch.sh"); script.Should().Contain("/player/${NODE_ID}/embed?token=${CERT_THUMB}"); script.Should().Contain("url-divergence.log"); script.Should().Contain("/player/${NODE_ID}?token=${CERT_THUMB}"); } [Fact] public void LaunchScript_DisablesChromiumPromptsAndRuntimeUpdates() { var script = Read("scripts/flowercore-signage-launch.sh"); script.Should().Contain("--noerrdialogs"); script.Should().Contain("--disable-infobars"); script.Should().Contain("--password-store=basic"); script.Should().Contain("--check-for-update-interval=2592000"); } [Fact] public void PrelaunchScript_AbortsWhenRequiredFilesAreMissing() { var script = Read("scripts/flowercore-signage-prelaunch.sh"); script.Should().Contain("for f in /etc/flowercore/signage-node.json /etc/fc-signage-player/client.p12 /etc/fc-signage-player/client.p12.pass"); script.Should().Contain("exit 1"); script.Should().Contain("-checkend $((7*24*3600))"); } [Fact] public void BootstrapScript_IsIdempotentWhenAlreadyEnrolled() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("already enrolled"); script.Should().Contain("exit 0"); script.Should().Contain(".enrolledAt"); } [Fact] public void BootstrapScript_GeneratesStableMachineIdFromUuid() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("uuidgen"); script.Should().Contain("cut -c1-16"); script.Should().Contain("machineId"); } [Fact] public void BootstrapScript_RetriesRegisterOnceForFirstCallRace() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("for attempt in 1 2"); script.Should().Contain("register attempt $attempt returned"); script.Should().Contain("sleep 5"); } [Fact] public void BootstrapScript_SupportsSetupCodeAndApprovalPollingBudget() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("signage-setup-code"); script.Should().Contain("approve-via-setup-code"); script.Should().Contain("+ 1800"); script.Should().Contain("sleep 15"); } [Fact] public void BootstrapScript_CsrSubjectIdentifiesPiPlayer() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("/CN=${NODE_ID}/O=FlowerCore/OU=SignagePlayer-Pi"); } [Fact] public void BootstrapScript_PersistsCertificateAsP12WithRestrictivePermissions() { var script = Read("scripts/flowercore-signage-bootstrap.sh"); script.Should().Contain("openssl pkcs12 -export"); script.Should().Contain("client.p12.pass"); script.Should().Contain("chmod 0600"); script.Should().Contain("chmod 0640"); } [Fact] public void RenewScript_OnlyRunsWhenCertHasLessThanThirtyDays() { var script = Read("scripts/flowercore-signage-renew-cert.sh"); script.Should().Contain("-checkend $((30*24*3600))"); script.Should().Contain("exit 0"); script.Should().Contain("/renew"); } [Fact] public void RenewScript_AtomicallySwapsNewCertificateFiles() { var script = Read("scripts/flowercore-signage-renew-cert.sh"); script.Should().Contain("client.key.new"); script.Should().Contain("mv \"$CERT_DIR/client.key.new\" \"$CERT_DIR/client.key\""); script.Should().Contain("mv \"$CERT_DIR/client.p12.new\" \"$CERT_DIR/client.p12\""); } [Fact] public void HdmiRule_RestartsPlayerAndRunsCapabilityDetection() { var rule = Read("systemd/99-flowercore-signage-hdmi.rules"); var responder = Read("scripts/flowercore-signage-hdmi-respond.sh"); rule.Should().Contain("KERNEL==\"card?-HDMI-A-?\""); rule.Should().Contain("start flowercore-signage-player-pi-hdmi.service"); responder.Should().Contain("sleep 2"); responder.Should().Contain("start flowercore-signage-detect-display.service"); responder.Should().Contain("restart flowercore-signage-player-pi.service"); } [Fact] public void DetectDisplayServiceAndTimer_RunAtBootAndDaily() { var service = Read("systemd/flowercore-signage-detect-display.service"); var timer = Read("systemd/flowercore-signage-detect-display.timer"); service.Should().Contain("ExecStart=/usr/local/bin/fc-signage-detect-display"); timer.Should().Contain("OnBootSec=30s"); timer.Should().Contain("OnCalendar=daily"); timer.Should().Contain("RandomizedDelaySec=1h"); } [Fact] public void DetectDisplayScript_EmitsDisconnectedProfileWhenNoHdmiIsPresent() { var script = Read("scripts/fc-signage-detect-display"); script.Should().Contain("displayConnected: false"); script.Should().Contain("No HDMI display detected"); } [Fact] public void DetectDisplayScript_ParsesEdidForHdrResolutionAndAudio() { var script = Read("scripts/fc-signage-detect-display"); script.Should().Contain("edid-decode"); script.Should().Contain("HDR (Static|Dynamic) Metadata Block"); script.Should().Contain("maxResolution"); script.Should().Contain("hasAudioOutput"); } [Fact] public void DetectDisplayScript_TriesBothForwardCompatibleCapabilityEndpoints() { var script = Read("scripts/fc-signage-detect-display"); script.Should().Contain("/api/v1/nodes/${NODE_ID}/capabilities"); script.Should().Contain("/api/v1/displays/${NODE_ID}/capability-profile"); script.Should().Contain("no endpoint accepted the profile"); } [Fact] public void ChromiumPolicy_IsValidJsonAndDisablesCredentialPrompts() { using var doc = JsonDocument.Parse(Read("chromium-policies/flowercore-signage.json")); var root = doc.RootElement; root.GetProperty("AutofillAddressEnabled").GetBoolean().Should().BeFalse(); root.GetProperty("AutofillCreditCardEnabled").GetBoolean().Should().BeFalse(); root.GetProperty("PasswordManagerEnabled").GetBoolean().Should().BeFalse(); root.GetProperty("ExtensionInstallBlocklist")[0].GetString().Should().Be("*"); } [Fact] public void RenewalTimer_UsesDailyCadenceWithTwoHourJitter() { var timer = Read("systemd/flowercore-signage-renew.timer"); timer.Should().Contain("OnCalendar=daily"); timer.Should().Contain("RandomizedDelaySec=2h"); timer.Should().Contain("Persistent=true"); } private static string Read(string relativePath) => File.ReadAllText(Path.Combine(AppRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))); private static string FindRepoRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (Directory.Exists(Path.Combine(current.FullName, "apps")) && File.Exists(Path.Combine(current.FullName, "README.md"))) { return current.FullName; } current = current.Parent; } throw new DirectoryNotFoundException("Could not find bluejay-infra root."); } }