The frame-rate boundary is correct as a product rule, but it only bites in a Release build, where DevBuild.IsDevelopment is false and DeveloperMode cannot unlock anything — exactly the build whose performance numbers are worth reading (see docs/ai/environment.md: Debug pixel paths are several times slower). 60 fps could not be tested at all. OutputProfile.EntitlementEnforced is false: every shipped tier is selectable by any build, and HasProOutput — the one property the tier setter, the dropdown's strikethrough and the locked-item style all read — answers yes regardless of ownership. static readonly rather than const, so the consumed `||` does not fold into unreachable operand code (same reason as DevBuild.IsDevelopment). The licence indicator row is suppressed while the switch is off: it exists to explain what the boundary costs, and claiming "Steam ID Limit: 60fps" to an unowned build would be a false statement. Docs corrected in place — the enforced rule is unchanged, only its state is recorded. Tests: one integration case covers both switch states (unowned build reaches 1080p60 with the switch off, 1080p30 with it on), so flipping at launch leaves no red test behind. The two suites that anchor on the pinned licence row early-return while it is absent; EntitlementTests covers that state meanwhile. 667 pass, build 0 warnings.
142 lines
5.2 KiB
C#
142 lines
5.2 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services.Entitlement;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Steam ownership is cached once into the app-wide binary flag.
|
|
///
|
|
/// <para>The rule that must never break: a missing paid app ID, Steam client, or native library
|
|
/// fails closed.</para>
|
|
/// </summary>
|
|
// 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) { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The pre-release switch is the only thing that lets an <b>unowned</b> build reach 60 fps, which
|
|
/// is what makes Release-build performance testing possible before the paid edition ships.
|
|
///
|
|
/// <para>Written against both states of <see cref="OutputProfile.EntitlementEnforced"/> 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.</para>
|
|
/// </summary>
|
|
[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) { }
|
|
}
|
|
}
|
|
}
|