From 9fc4e8a5ecd0dada4bb64b49a366cfbd4f7d49cf Mon Sep 17 00:00:00 2001 From: James Maes Date: Tue, 10 Nov 2026 13:41:09 -0600 Subject: [PATCH] fix(app): scope the UI test fixture to its token, never in an installed build With GCLO_UITEST_FIXTURE=1 the fixture's git client answered "not a repository" for every folder, whatever the token, so a real session started with the variable set treated every existing repository as missing. The variable was also honoured by installed release builds, and nothing recorded that it was in effect. - The fixture asks the real git client whether a folder is a repository; its own clones write nothing, so the fixture token still always clones. - An installed build (one with a Velopack app id) ignores the variable. - The app logs one line when the fixture is enabled; a UI test asserts it. Threat model: the interface row for the variable says what now holds. Closes #145 Co-Authored-By: Claude Fable 5.2 Signed-off-by: James Maes --- docs/security/threat-model.md | 2 +- gclo.UiTests/AppSession.cs | 3 ++- gclo.UiTests/SmokeTests.cs | 8 ++++++++ gclo/MainWindow.xaml.cs | 6 ++++++ gclo/UiTestFixture.cs | 37 +++++++++++++++++++++++++++++++---- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 29a908a..ee70fbf 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -65,7 +65,7 @@ flowchart LR | Activity logs | out | Actions and errors; token *lengths* only | File ACL | `IActivityLog` callers never pass tokens | | `.git/gclo-recovery.json`, `gclo.checkoutpending` config key | out/in | Path-recovery plan; checkout-pending marker | File ACL | Written atomically; read only for the repository it belongs to | | Command line (CLI) | in | Options, org, target, filters | n/a | `OptionReader` / `RepoFilterSpec`; parallelism bounded 1..64; token options refused on argv | -| `GCLO_DATA_DIR`, `GCLO_UITEST_FIXTURE` env | in | Data root override; offline test seam | n/a | Fixture only answers for the exact fixture token; never set in production | +| `GCLO_DATA_DIR`, `GCLO_UITEST_FIXTURE` env | in | Data root override; offline test seam | n/a | Fixture only answers for the exact fixture token (every other token, and the question whether a folder is a repository, go to the real services); ignored by an installed build; logged when enabled; never set in production (#145) | | `wsl.exe -e sh -s` | out/in | POSIX script on stdin, org/repo/url as positional args, `GCLO_GIT_TOKEN` via `WSLENV` | PAT via env | Nothing is re-parsed by wsl.exe's command line; token never on argv or disk; UTF-16 wsl.exe messages decoded | | Update feed (Velopack) | in | `releases..json`, packages | HTTPS; package hashes | Velopack verifies package hashes against the feed; channel never crosses | | Release assets (users) | in | Setup, portable, CLI zip, nupkg, SBOMs | HTTPS | `SHA256SUMS`, build-provenance and SBOM attestations (`gh attestation verify`) | diff --git a/gclo.UiTests/AppSession.cs b/gclo.UiTests/AppSession.cs index c5a8c41..632f21d 100644 --- a/gclo.UiTests/AppSession.cs +++ b/gclo.UiTests/AppSession.cs @@ -94,7 +94,8 @@ public sealed class AppSession : IDisposable - private bool LogContains(string fragment) + /// True when any of the app's activity-log files holds . + public bool LogContains(string fragment) { string logs = Path.Combine(DataDirectory, "logs"); if(!Directory.Exists(logs)) diff --git a/gclo.UiTests/SmokeTests.cs b/gclo.UiTests/SmokeTests.cs index d29ede7..9485164 100644 --- a/gclo.UiTests/SmokeTests.cs +++ b/gclo.UiTests/SmokeTests.cs @@ -37,6 +37,14 @@ public sealed partial class SmokeTests : UiTestBase + // (1b) The offline fixture this suite runs against announces itself in the app's + // log: a session with it enabled is never mistaken for an ordinary one (#145). + [Fact] + public void ActivityLog_AfterLaunch_SaysTheUiTestFixtureIsEnabled() + => Assert.True(Session.LogContains("UI test fixture enabled"), "the app logs one line when the fixture is honoured"); + + + // (2) The navigation pane carries the pinned Quick Sync plus both footer commands. [Fact] public void NavigationPane_AfterLaunch_ShowsQuickSyncAddAccountAndSyncAll() diff --git a/gclo/MainWindow.xaml.cs b/gclo/MainWindow.xaml.cs index 0e8551b..f487c77 100644 --- a/gclo/MainWindow.xaml.cs +++ b/gclo/MainWindow.xaml.cs @@ -114,6 +114,12 @@ public sealed partial class MainWindow : Window _log = new FileActivityLog(System.IO.Path.Combine(GcloPaths.DataRoot, "logs")); App.CrashLog = _log; // the unhandled-exception net now reaches the activity log _log.Info($"gclo {BuildVersion.Describe(typeof(MainWindow).Assembly)} starting."); + if(UiTestFixture.IsEnabled) + { + // Worth a line in every log it applies to: with it on, one fixed token is + // answered from a built-in organization instead of GitHub (#145). + _log.Info("UI test fixture enabled (GCLO_UITEST_FIXTURE=1): the fixture token is answered offline."); + } _updateService = new UpdateService(_log); _tokenVault = new CredentialManagerVault(); _accountsStore = new AccountsStore(_tokenVault, log: _log); diff --git a/gclo/UiTestFixture.cs b/gclo/UiTestFixture.cs index 7cf2388..dbd91cc 100644 --- a/gclo/UiTestFixture.cs +++ b/gclo/UiTestFixture.cs @@ -20,7 +20,9 @@ namespace gclo; /// network - a fixed organization with three repositories, and a git client that /// succeeds without touching disk. Any other token (even with the variable set) goes /// to the real services, so the existing offline-input tests keep their meaning -/// (a dummy token still gets GitHub's 401). Production never sets the variable. +/// (a dummy token still gets GitHub's 401). Production never sets the variable, and an +/// installed build ignores it: the seam exists for a build run from its output folder +/// by the test suite, not for the app people install (#145). /// internal static class UiTestFixture { @@ -30,8 +32,13 @@ internal static class UiTestFixture /// The organization the fixture lists. public const string Organization = "fixture-org"; - /// True when the process was started for the UI suite with the fixture enabled. - public static bool IsEnabled { get; } = string.Equals(Environment.GetEnvironmentVariable("GCLO_UITEST_FIXTURE"), "1", StringComparison.Ordinal); + /// + /// True when the process was started for the UI suite with the fixture enabled, and + /// is not an installed build. + /// + public static bool IsEnabled { get; } = + string.Equals(Environment.GetEnvironmentVariable("GCLO_UITEST_FIXTURE"), "1", StringComparison.Ordinal) + && !IsInstalledBuild(); private static readonly IReadOnlyList s_repositories = [ @@ -42,6 +49,25 @@ internal static class UiTestFixture + /// + /// True when the app was installed by Velopack (it then has an app id). A build run + /// from its output folder, which is what the UI suite launches, has none. + /// + private static bool IsInstalledBuild() + { + try + { + return !string.IsNullOrEmpty(Velopack.Locators.VelopackLocator.Current?.AppId); + } + catch(Exception) + { + // No locator for this platform or layout: not an installed build. + return false; + } + } + + + public static IRepositoryLister WrapRepositoryLister(IRepositoryLister real) => IsEnabled ? new RepositoryLister(real) : real; @@ -81,7 +107,10 @@ internal static class UiTestFixture /// Succeeds without writing anything; the real client handles every other token. private sealed class GitClient(IGitClient real) : IGitClient { - public bool IsValidRepository(string path) => false; + // No token is passed here, so the real client answers for everyone. The + // fixture's own clones write nothing, so its folders are never repositories + // and the fixture token always takes the clone path (#145). + public bool IsValidRepository(string path) => real.IsValidRepository(path); -- 2.54.0