using System; using System.IO; using System.Linq; using Xunit; using ytLive.Models; using ytLive.Services.Entitlement; using ytLive.ViewModels; namespace ytLive.Tests; /// /// Steam ownership is cached once into the app-wide binary flag. /// /// The rule that must never break: a missing paid app ID, Steam client, or native library /// fails closed. /// // Shares the static MainViewModel.LayoutPathOverride with the sibling class; without a // shared collection xUnit runs them in parallel and the two view models fight over one temp DB. [Collection("MainViewModelLayoutPath")] public class EntitlementTests { [Fact] public void No_configured_paid_app_id_reads_as_not_owned() { var entitlement = SteamEntitlement.NotConfigured(); Assert.False(entitlement.IsAppOwned); // The diagnostic is deliberately neutral (2026-11-05): the licence state is shown to the // creator as a status indicator, so the log text carries no "contact support" wording. Assert.Contains("Steam licensing is not configured", entitlement.ErrorMessage); } [Fact] public void Steam_ownership_checks_the_paid_app_id() { const uint paidAppId = 1234567u; uint queriedAppId = 0; var entitlement = new SteamEntitlement( paidAppId, initializeSteam: () => true, isAppSubscribed: appId => { queriedAppId = appId; return true; }, shutdownSteam: () => { }); Assert.True(entitlement.IsAppOwned); Assert.Equal(paidAppId, queriedAppId); Assert.Null(entitlement.ErrorMessage); } [Fact] public void A_steam_entitlement_without_a_client_fails_closed() { var entitlement = new SteamEntitlement(1234567u, () => false, _ => true, () => { }); Assert.False(entitlement.IsAppOwned); Assert.Contains("Open Steam", entitlement.ErrorMessage); } [Fact] public void A_valid_steam_check_for_an_unowned_account_is_not_an_authentication_error() { var entitlement = new SteamEntitlement(1234567u, () => true, _ => false, () => { }); Assert.False(entitlement.IsAppOwned); Assert.Null(entitlement.ErrorMessage); } [Fact] public void The_fake_reports_what_a_test_sets() { var fake = new FakeEntitlement(true); Assert.True(fake.IsAppOwned); fake.IsAppOwned = false; Assert.False(fake.IsAppOwned); } [Fact] public void The_view_model_takes_its_answer_from_the_rail() { var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-entitlement-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { var vm = new MainViewModel(); Assert.True(vm.IsAppOwned); Assert.True(vm.HasProOutput); vm.EntitlementOverride = new FakeEntitlement(false); Assert.False(vm.IsAppOwned); // Pro output is what the frame-rate boundary answers, and while the pre-release switch is // off that answer is yes regardless of ownership. The rail still moves IsAppOwned itself, // and the paid-limit row is absent because nothing is being limited. Assert.Equal(!OutputProfile.EntitlementEnforced, vm.HasProOutput); Assert.Equal(OutputProfile.EntitlementEnforced, vm.AttentionAlerts.Any(alert => alert.Id == "steam-licence")); vm.EntitlementOverride = new FakeEntitlement(true); Assert.True(vm.IsAppOwned); Assert.True(vm.HasProOutput); } finally { MainViewModel.LayoutPathOverride = null; try { File.Delete(tempDb); } catch (IOException) { } } } /// /// The pre-release switch is the only thing that lets an unowned build reach 60 fps, which /// is what makes Release-build performance testing possible before the paid edition ships. /// /// Written against both states of so /// flipping it at launch does not leave a red test behind: with the switch off the unowned view /// model keeps 1080p60; with it on the tier setter refuses it and the free tier stands. /// [Fact] public void An_unowned_build_reaches_60_fps_only_while_the_gate_is_unenforced() { var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-entitlement-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { var vm = new MainViewModel { EntitlementOverride = new FakeEntitlement(false) }; vm.SelectedQuality = vm.QualityOptions.Single(tier => tier.Display == "1080p60"); Assert.False(vm.IsAppOwned); // The tier the encoder would actually be built from (MainViewModel.Streaming.Operations). var landed = OutputProfile.Resolve(vm.SelectedQuality, vm.QualityOptions, vm.HasProOutput); Assert.Equal(OutputProfile.EntitlementEnforced ? 30 : 60, landed.Fps); } finally { MainViewModel.LayoutPathOverride = null; try { File.Delete(tempDb); } catch (IOException) { } } } }