From 16bfddee047bfea4e9c1883a293d4986566602c6 Mon Sep 17 00:00:00 2001 From: MarkS Date: Wed, 8 Apr 2026 10:13:12 -0600 Subject: [PATCH 1/2] SF-1992 Fail sync if push failed - When pushing a bundle to the PT SendReceive server, we can incorrectly think we gave commits to the server and that they were retained. This leads to additional problems later when pushing again, because the server doesn't have a commit that we are expecting it to have. - This patch checks if the PT SendReceive server has the commit we intended to push, and fails the sync if not. - The outgoing bundle size is also logged for help in investigating problems. - This patch does not fix the situation where pushing a commit to the SR server doesn't work. But it should prevent SF from (1) incorrectly communicating to the user that the sync succeeded, and (2) getting into a state where the SF project is stuck and can not sync without manual intervention. The SF project is left in a state where the sync can at least be re-tried. --- AGENTS.md | 1 + .../Services/HgWrapper.cs | 18 +++-- .../Services/IHgWrapper.cs | 7 +- .../JwtInternetSharedRepositorySource.cs | 68 +++++++++++++++-- .../Services/LazyScrTextCollection.cs | 3 +- .../JwtInternetSharedRepositorySourceTests.cs | 73 +++++++++++++++++++ 6 files changed, 151 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 264297818c4..a91e66a44c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ This repository contains three interconnected applications: - Do put comments into the code if the intent is not clear from the code. - All classes and interfaces should have a comment to briefly explain why it is there and what its purpose is in the overall system, even if it seems obvious. - Please do not fail to add a comment to any classes or interfaces that are created. All classes and interfaces should have a comment. +- Use good argument and variable names that explain themselves without needing a comment. Well named arguments or variables are better than unclearly named arguments or variables with a comment. # TypeScript language diff --git a/src/SIL.XForge.Scripture/Services/HgWrapper.cs b/src/SIL.XForge.Scripture/Services/HgWrapper.cs index 753cdee2253..972d25bca8e 100644 --- a/src/SIL.XForge.Scripture/Services/HgWrapper.cs +++ b/src/SIL.XForge.Scripture/Services/HgWrapper.cs @@ -6,7 +6,7 @@ namespace SIL.XForge.Scripture.Services; -/// A wrapper for the class for calling Mercurial. +/// A wrapper for the class for calling Mercurial. public class HgWrapper : IHgWrapper { public static string RunCommand(string repository, string cmd) @@ -16,18 +16,21 @@ public static string RunCommand(string repository, string cmd) return Hg.Default.RunCommand(repository, cmd).StdOut; } - public static byte[] Bundle(string repository, params string[] heads) + /// + /// Returns a Mercurial bundle containing changesets above the given base revisions. + /// + public byte[] Bundle(string repositoryPath, params string[] baseRevisions) { if (Hg.Default == null) throw new InvalidOperationException("Hg default has not been set."); - return Hg.Default.Bundle(repository, heads); + return Hg.Default.Bundle(repositoryPath, baseRevisions); } - public static string[] Pull(string repository, byte[] bundle) + public string[] Pull(string repositoryPath, byte[] bundle) { if (Hg.Default == null) throw new InvalidOperationException("Hg default has not been set."); - return Hg.Default.Pull(repository, bundle, true); + return Hg.Default.Pull(repositoryPath, bundle, true); } /// @@ -98,10 +101,9 @@ public void RestoreRepository(string destination, string backupFile) } /// - /// Mark all changesets available on the PT server public. + /// Mark all changesets as public. /// - /// The repository. - public void MarkSharedChangeSetsPublic(string repository) => RunCommand(repository, "phase -p -r 'tip'"); + public void MarkSharedChangeSetsPublic(string repositoryPath) => RunCommand(repositoryPath, "phase -p -r 'tip'"); /// /// Returns the ids of commits with draft phase. diff --git a/src/SIL.XForge.Scripture/Services/IHgWrapper.cs b/src/SIL.XForge.Scripture/Services/IHgWrapper.cs index bbfbb106d1d..8877ddf86ed 100644 --- a/src/SIL.XForge.Scripture/Services/IHgWrapper.cs +++ b/src/SIL.XForge.Scripture/Services/IHgWrapper.cs @@ -2,17 +2,20 @@ namespace SIL.XForge.Scripture.Services; +/// Mercurial operations public interface IHgWrapper { void SetDefault(Hg hgDefault); void Init(string repository); void Update(string repository); - void Update(string repository, string rev); + void Update(string repositoryPath, string rev); void BackupRepository(string repository, string backupFile); void RestoreRepository(string destination, string backupFile); string GetLastPublicRevision(string repository); string GetRepoRevision(string repositoryPath); - void MarkSharedChangeSetsPublic(string repository); + void MarkSharedChangeSetsPublic(string repositoryPath); + string[] Pull(string repositoryPath, byte[] bundle); + byte[] Bundle(string repositoryPath, params string[] baseRevisions); string RecentLogGraph(string repositoryPath); string[] GetDraftRevisions(string repositoryPath); } diff --git a/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs b/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs index 93f1a86b11c..f48b6788efc 100644 --- a/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs +++ b/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs @@ -62,9 +62,9 @@ public bool CanUserAuthenticateToPTArchives() /// Uses the a REST client to pull from the Paratext send/receive server. This overrides the base implementation /// to avoid needing the current user's Paratext registration code to get the base revision. /// - public override string[] Pull(string repository, SharedRepository pullRepo) + public override string[] Pull(string repositoryPath, SharedRepository pullRepo) { - string baseRev = _hgWrapper.GetLastPublicRevision(repository); + string baseRev = _hgWrapper.GetLastPublicRevision(repositoryPath); // Get bundle string guid = Guid.NewGuid().ToString(); @@ -92,9 +92,9 @@ public override string[] Pull(string repository, SharedRepository pullRepo) return []; // Use bundle - string[] changeSets = HgWrapper.Pull(repository, bundle); + string[] changeSets = _hgWrapper.Pull(repositoryPath, bundle); - _hgWrapper.MarkSharedChangeSetsPublic(repository); + _hgWrapper.MarkSharedChangeSetsPublic(repositoryPath); return changeSets; } @@ -102,14 +102,23 @@ public override string[] Pull(string repository, SharedRepository pullRepo) /// Uses the a REST client to push to the Paratext send/receive server. This overrides the base implementation /// to avoid needing the current user's Paratext registration code to get the base revision. /// - public override void Push(string repository, SharedRepository pushRepo) + public override void Push(string repositoryPath, SharedRepository pushRepo) { - string baseRev = _hgWrapper.GetLastPublicRevision(repository); + string baseRev = _hgWrapper.GetLastPublicRevision(repositoryPath); // Create bundle - byte[] bundle = HgWrapper.Bundle(repository, baseRev); + byte[] bundle = _hgWrapper.Bundle(repositoryPath, baseRev); if (bundle.Length == 0) + { + _logger.LogInformation($"Not pushing a 0 Byte bundle for project PT ID {pushRepo.SendReceiveId.Id}."); return; + } + + string localTip = _hgWrapper.GetRepoRevision(repositoryPath); + _logger.LogInformation( + $"Pushing bundle of {bundle.Length} Bytes to S/R server for project PT ID " + + $"{pushRepo.SendReceiveId.Id}. Base revision {baseRev ?? "(null)"}. Local tip {localTip}." + ); // Send bundle string guid = Guid.NewGuid().ToString(); @@ -128,7 +137,50 @@ public override void Push(string repository, SharedRepository pushRepo) "no" ); - _hgWrapper.MarkSharedChangeSetsPublic(repository); + (bool isRevOnServer, int serverRevCount, string? serverLastRev) = CheckIfRevisionIsOnServer(pushRepo, localTip); + if (!isRevOnServer) + { + throw new InvalidOperationException( + $"Push verification failed for project PT ID {pushRepo.SendReceiveId.Id}. " + + $"Expected revision {localTip} was not found in the server's revision history. " + + $"Server has {serverRevCount} revisions. Last server revision: {serverLastRev ?? "(null)"}." + ); + } + + _hgWrapper.MarkSharedChangeSetsPublic(repositoryPath); + } + + /// + /// Returns whether the expected revision is present on the Paratext Send/Receive server. + /// + internal (bool isRevOnServer, int serverRevCount, string? serverLastRev) CheckIfRevisionIsOnServer( + SharedRepository serverRepo, + string expectedRevision + ) + { + string projRevHistResponse = GetClient() + .Get("projrevhist", "proj", serverRepo.ScrTextName, "projid", serverRepo.SendReceiveId.Id, "all", "1"); + + JObject jsonResult = JObject.Parse(projRevHistResponse); + JArray? revisions = jsonResult["project"]?["revision_history"]?["revisions"] as JArray; + + bool isRevOnServer = + revisions?.Any(r => string.Equals(r["id"]?.ToString(), expectedRevision, StringComparison.Ordinal)) + ?? false; + int serverRevCount = revisions?.Count ?? 0; + string? serverLastRev = GetFirstElementId(revisions); + + return (isRevOnServer, serverRevCount, serverLastRev); + } + + /// + /// Returns the first element's "id" value, if possible. + /// + private static string? GetFirstElementId(JArray? revisions) + { + if (revisions?.Count > 0) + return revisions[0]["id"]?.ToString(); + return null; } /// diff --git a/src/SIL.XForge.Scripture/Services/LazyScrTextCollection.cs b/src/SIL.XForge.Scripture/Services/LazyScrTextCollection.cs index a58d48a6d03..cefdd670d08 100644 --- a/src/SIL.XForge.Scripture/Services/LazyScrTextCollection.cs +++ b/src/SIL.XForge.Scripture/Services/LazyScrTextCollection.cs @@ -32,7 +32,8 @@ public void Initialize(string projectsPath) } /// - /// Get a ScrText for a given user from the data for a paratext project with the target project ID and type. + /// Get a ScrText for a given user from the data for a paratext project with the target project ID and type. Not to + /// be confused with ParatextData ScrTextCollection.FindById, which this is not an override of. /// /// The username of the user retrieving the ScrText. /// The ID of the target project. diff --git a/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs index fdcbb00e27e..4de8d3a3734 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs @@ -18,6 +18,7 @@ public class JwtInternetSharedRepositorySourceTests private const string ProjectName = "TestProject"; private const string ProjectId = "4011111111111111111111111111111111111111"; private const string NewTipRevision = "bbb2222222222222222222222222222222222222"; + private const string BaseTipRevision = "aaa1111111111111111111111111111111111111"; [Test] public void CanUserAuthenticateToPTArchives_Works() @@ -56,6 +57,62 @@ public void CanUserAuthenticateToPTArchives_Works() ); } + [Test] + public void CheckPushRevisionOnServer_RevisionExists_ReturnsFound() + { + var env = new TestEnvironment(); + SharedRepository pushRepo = TestEnvironment.CreatePushRepo(HexId.FromStr(ProjectId), ProjectName); + env.SetProjRevHistResponse(revisionIds: [NewTipRevision, BaseTipRevision]); + + // SUT + (bool isRevOnServer, int serverRevCount, string? serverLastRev) = env.RepoSource.CheckIfRevisionIsOnServer( + pushRepo, + NewTipRevision + ); + + Assert.That(isRevOnServer, Is.True); + Assert.That(serverRevCount, Is.EqualTo(2)); + Assert.That(serverLastRev, Is.EqualTo(NewTipRevision)); + } + + [Test] + public void CheckPushRevisionOnServer_RevisionMissing_ReturnsNotFound() + { + var env = new TestEnvironment(); + SharedRepository pushRepo = TestEnvironment.CreatePushRepo(HexId.FromStr(ProjectId), ProjectName); + // Server only has the base revision, not the new one + env.SetProjRevHistResponse(revisionIds: [BaseTipRevision]); + + // SUT + (bool isRevOnServer, int serverRevCount, string? serverLastRev) = env.RepoSource.CheckIfRevisionIsOnServer( + pushRepo, + NewTipRevision + ); + + Assert.That(isRevOnServer, Is.False); + Assert.That(serverRevCount, Is.EqualTo(1)); + Assert.That(serverLastRev, Is.EqualTo(BaseTipRevision)); + } + + [Test] + public void CheckPushRevisionOnServer_EmptyServerResponse_ReturnsNotFound() + { + var env = new TestEnvironment(); + SharedRepository pushRepo = TestEnvironment.CreatePushRepo(HexId.FromStr(ProjectId), ProjectName); + // Server returns an empty revision list + env.SetProjRevHistResponse(revisionIds: []); + + // SUT + (bool isRevOnServer, int serverRevCount, string? serverLastRev) = env.RepoSource.CheckIfRevisionIsOnServer( + pushRepo, + NewTipRevision + ); + + Assert.That(isRevOnServer, Is.False); + Assert.That(serverRevCount, Is.EqualTo(0)); + Assert.That(serverLastRev, Is.Null); + } + [Test] public void GetOutgoingRevisions_ReturnsDraftRevisions() { @@ -93,6 +150,9 @@ public void GetOutgoingRevisions_NoDraftRevisions_ReturnsEmpty() Assert.That(result, Is.Empty); } + /// + /// Test environment for JwtInternetSharedRepositorySource tests. + /// private class TestEnvironment { public readonly JwtInternetSharedRepositorySource RepoSource; @@ -122,5 +182,18 @@ public TestEnvironment() public static SharedRepository CreatePushRepo(HexId ptProjectId, string projectName) => new SharedRepository { SendReceiveId = ptProjectId, ScrTextName = projectName }; + + /// + /// Sets up the projrevhist API response to return the given revision IDs. + /// + public void SetProjRevHistResponse(string[] revisionIds) + { + string revisionsJson = string.Join( + ",", + Array.ConvertAll(revisionIds, id => $"{{\"id\":\"{id}\",\"parents\":[]}}") + ); + string json = $"{{\"project\":{{\"revision_history\":{{\"revisions\":[{revisionsJson}]}}}}}}"; + MockPTArchivesClient.Configure().Get(Arg.Any(), Arg.Any()).Returns(json); + } } } From 9f04e100aaf1f93d9db671c48d08fadd227977ae Mon Sep 17 00:00:00 2001 From: MarkS Date: Tue, 7 Apr 2026 10:16:08 -0600 Subject: [PATCH 2/2] log warning on null revisions response --- .../Services/HgWrapper.cs | 19 ++++---- .../Services/IHgWrapper.cs | 10 ++-- .../JwtInternetSharedRepositorySource.cs | 47 +++++++++++++++---- .../JwtInternetSharedRepositorySourceTests.cs | 40 +++++++++++++++- 4 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/SIL.XForge.Scripture/Services/HgWrapper.cs b/src/SIL.XForge.Scripture/Services/HgWrapper.cs index 972d25bca8e..c9fe9a21452 100644 --- a/src/SIL.XForge.Scripture/Services/HgWrapper.cs +++ b/src/SIL.XForge.Scripture/Services/HgWrapper.cs @@ -19,18 +19,18 @@ public static string RunCommand(string repository, string cmd) /// /// Returns a Mercurial bundle containing changesets above the given base revisions. /// - public byte[] Bundle(string repositoryPath, params string[] baseRevisions) + public byte[] Bundle(string repository, params string[] baseRevisions) { if (Hg.Default == null) throw new InvalidOperationException("Hg default has not been set."); - return Hg.Default.Bundle(repositoryPath, baseRevisions); + return Hg.Default.Bundle(repository, baseRevisions); } - public string[] Pull(string repositoryPath, byte[] bundle) + public string[] Pull(string repository, byte[] bundle) { if (Hg.Default == null) throw new InvalidOperationException("Hg default has not been set."); - return Hg.Default.Pull(repositoryPath, bundle, true); + return Hg.Default.Pull(repository, bundle, true); } /// @@ -75,12 +75,12 @@ public string GetLastPublicRevision(string repository) /// /// Returns the currently checked out revision of an hg repository. /// - public string GetRepoRevision(string repositoryPath) + public string GetRepoRevision(string repository) { - string rev = RunCommand(repositoryPath, "log --limit 1 --rev . --template {node}"); + string rev = RunCommand(repository, "log --limit 1 --rev . --template {node}"); if (string.IsNullOrWhiteSpace(rev)) { - throw new InvalidDataException($"Unable to determine repo revision for hg repo at {repositoryPath}"); + throw new InvalidDataException($"Unable to determine repo revision for hg repo at {repository}"); } return rev; } @@ -103,7 +103,8 @@ public void RestoreRepository(string destination, string backupFile) /// /// Mark all changesets as public. /// - public void MarkSharedChangeSetsPublic(string repositoryPath) => RunCommand(repositoryPath, "phase -p -r 'tip'"); + /// The repository path. + public void MarkSharedChangeSetsPublic(string repository) => RunCommand(repository, "phase -p -r 'tip'"); /// /// Returns the ids of commits with draft phase. @@ -138,5 +139,5 @@ public void SetDefault(Hg hgDefault) /// `git checkout --force --detach COMMITTISH` /// Changes to tracked files will be discarded. Untracked files are left in place without being cleaned up. /// - public void Update(string repositoryPath, string rev) => Hg.Default.Update(repositoryPath, rev); + public void Update(string repository, string rev) => Hg.Default.Update(repository, rev); } diff --git a/src/SIL.XForge.Scripture/Services/IHgWrapper.cs b/src/SIL.XForge.Scripture/Services/IHgWrapper.cs index 8877ddf86ed..f71693a562c 100644 --- a/src/SIL.XForge.Scripture/Services/IHgWrapper.cs +++ b/src/SIL.XForge.Scripture/Services/IHgWrapper.cs @@ -8,14 +8,14 @@ public interface IHgWrapper void SetDefault(Hg hgDefault); void Init(string repository); void Update(string repository); - void Update(string repositoryPath, string rev); + void Update(string repository, string rev); void BackupRepository(string repository, string backupFile); void RestoreRepository(string destination, string backupFile); string GetLastPublicRevision(string repository); - string GetRepoRevision(string repositoryPath); - void MarkSharedChangeSetsPublic(string repositoryPath); - string[] Pull(string repositoryPath, byte[] bundle); - byte[] Bundle(string repositoryPath, params string[] baseRevisions); + string GetRepoRevision(string repository); + void MarkSharedChangeSetsPublic(string repository); + string[] Pull(string repository, byte[] bundle); + byte[] Bundle(string repository, params string[] baseRevisions); string RecentLogGraph(string repositoryPath); string[] GetDraftRevisions(string repositoryPath); } diff --git a/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs b/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs index f48b6788efc..1f1856352fa 100644 --- a/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs +++ b/src/SIL.XForge.Scripture/Services/JwtInternetSharedRepositorySource.cs @@ -13,12 +13,15 @@ namespace SIL.XForge.Scripture.Services; -/// An internet shared repository source that networks using JWT authenticated REST clients. +/// +/// An internet shared repository source that networks using JWT authenticated REST clients. +/// public class JwtInternetSharedRepositorySource : InternetSharedRepositorySource, IInternetSharedRepositorySource { private readonly JwtRestClient _registryClient; private readonly IHgWrapper _hgWrapper; private readonly ILogger _logger; + private readonly int _maxJsonLogChars = 200; public JwtInternetSharedRepositorySource( string accessToken, @@ -162,12 +165,19 @@ string expectedRevision .Get("projrevhist", "proj", serverRepo.ScrTextName, "projid", serverRepo.SendReceiveId.Id, "all", "1"); JObject jsonResult = JObject.Parse(projRevHistResponse); - JArray? revisions = jsonResult["project"]?["revision_history"]?["revisions"] as JArray; + if (jsonResult["project"]?["revision_history"]?["revisions"] is not JArray revisions) + { + string truncatedResult = FormatAndTruncate(jsonResult, _maxJsonLogChars); + _logger.LogWarning( + $"Getting projrevhist unexpectedly received null revisions for PT project ID {serverRepo.SendReceiveId.Id}. The JSON result is: {truncatedResult}" + ); + return (false, 0, null); + } - bool isRevOnServer = - revisions?.Any(r => string.Equals(r["id"]?.ToString(), expectedRevision, StringComparison.Ordinal)) - ?? false; - int serverRevCount = revisions?.Count ?? 0; + bool isRevOnServer = revisions.Any(r => + string.Equals(r["id"]?.ToString(), expectedRevision, StringComparison.Ordinal) + ); + int serverRevCount = revisions.Count; string? serverLastRev = GetFirstElementId(revisions); return (isRevOnServer, serverRevCount, serverLastRev); @@ -176,13 +186,34 @@ string expectedRevision /// /// Returns the first element's "id" value, if possible. /// - private static string? GetFirstElementId(JArray? revisions) + private static string? GetFirstElementId(JArray revisions) { - if (revisions?.Count > 0) + if (revisions.Count > 0) return revisions[0]["id"]?.ToString(); return null; } + /// + /// Formats JSON and truncates if too long. + /// + private static string FormatAndTruncate(JObject jsonResult, int maxChars) + { + string prettyJson = jsonResult.ToString(Newtonsoft.Json.Formatting.Indented); + return TruncateLogString(prettyJson, maxChars); + } + + /// + /// Truncates a string to the configured character count if needed and appends truncation details. + /// + private static string TruncateLogString(string value, int maxChars) + { + if (value.Length <= maxChars) + return value; + + int truncatedChars = value.Length - maxChars; + return value[..maxChars] + Environment.NewLine + $"... (truncated {truncatedChars} more characters)"; + } + /// /// This looks like it would be important, but it doesn't seem to matter what it returns, to synchronize. /// diff --git a/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs index 4de8d3a3734..07273526a56 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/JwtInternetSharedRepositorySourceTests.cs @@ -113,6 +113,37 @@ public void CheckPushRevisionOnServer_EmptyServerResponse_ReturnsNotFound() Assert.That(serverLastRev, Is.Null); } + [Test] + public void CheckPushRevisionOnServer_NullRevisions_LogsTruncatedJsonResult() + { + var env = new TestEnvironment(); + SharedRepository pushRepo = TestEnvironment.CreatePushRepo(HexId.FromStr(ProjectId), ProjectName); + string largeJsonValue = new string('x', 12000); + env.SetProjRevHistRawResponse($"{{\"project\":{{\"revision_history\":{{\"details\":\"{largeJsonValue}\"}}}}}}"); + + // SUT + (bool isRevOnServer, int serverRevCount, string? serverLastRev) = env.RepoSource.CheckIfRevisionIsOnServer( + pushRepo, + NewTipRevision + ); + + Assert.That(isRevOnServer, Is.False); + Assert.That(serverRevCount, Is.EqualTo(0)); + Assert.That(serverLastRev, Is.Null); + + env.Logger.AssertHasEvent( + e => + e.LogLevel == Microsoft.Extensions.Logging.LogLevel.Warning + && e.Message is not null + && e.Message.Length < 1000 + && e.Message.Contains("Getting projrevhist unexpectedly received null revisions") + && e.Message.Contains("xx") + && e.Message.Contains("truncated") + && e.Message.Contains("more characters"), + "Expected warning log with truncated JSON payload details." + ); + } + [Test] public void GetOutgoingRevisions_ReturnsDraftRevisions() { @@ -158,6 +189,7 @@ private class TestEnvironment public readonly JwtInternetSharedRepositorySource RepoSource; public readonly IRESTClient MockPTArchivesClient; public readonly IHgWrapper MockHgWrapper; + public readonly MockLogger Logger; public TestEnvironment() { @@ -168,13 +200,14 @@ public TestEnvironment() "jwtToken" ); MockHgWrapper = Substitute.For(); + Logger = new MockLogger(); RepoSource = Substitute.ForPartsOf( "access-token", mockPTRegistryClient, MockHgWrapper, ptUser, "sr-server-uri", - new MockLogger() + Logger ); MockPTArchivesClient = Substitute.For("pt-archives-server.example.com", "product-version-123"); RepoSource.Configure().GetClient().Returns(MockPTArchivesClient); @@ -193,7 +226,10 @@ public void SetProjRevHistResponse(string[] revisionIds) Array.ConvertAll(revisionIds, id => $"{{\"id\":\"{id}\",\"parents\":[]}}") ); string json = $"{{\"project\":{{\"revision_history\":{{\"revisions\":[{revisionsJson}]}}}}}}"; - MockPTArchivesClient.Configure().Get(Arg.Any(), Arg.Any()).Returns(json); + SetProjRevHistRawResponse(json); } + + public void SetProjRevHistRawResponse(string json) => + MockPTArchivesClient.Configure().Get(Arg.Any(), Arg.Any()).Returns(json); } }