fix(deployment): read every external system's methods in one query for the artifact snapshot (review 2026-10-21 04/P7)

This commit is contained in:
Joseph Doherty
2026-10-22 06:31:30 -04:00
parent 58b1e021b2
commit 669822772e
5 changed files with 102 additions and 5 deletions
@@ -1,20 +1,22 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
/// <summary>
/// Retention purge for <see cref="SystemArtifactDeploymentRecord"/> rows (review 2026-10-21
/// Artifact-deploy queries on DeploymentManagerRepository. Retention purge for
/// <see cref="SystemArtifactDeploymentRecord"/> rows (review 2026-10-21
/// 08/G2): one row is written per Deploy Artifacts run and nothing reads them back, so they
/// must share the terminal deployment-record retention window.
/// </summary>
public sealed class SystemArtifactDeploymentPurgeTests : IDisposable
public sealed class DeploymentManagerArtifactRepositoryTests : IDisposable
{
private readonly ScadaBridgeDbContext _context = SqliteTestHelper.CreateInMemoryContext();
private readonly DeploymentManagerRepository _repository;
/// <summary>Initializes the repository over a fresh in-memory SQLite context.</summary>
public SystemArtifactDeploymentPurgeTests()
public DeploymentManagerArtifactRepositoryTests()
{
_repository = new DeploymentManagerRepository(_context);
}
@@ -49,4 +51,30 @@ public sealed class SystemArtifactDeploymentPurgeTests : IDisposable
var survivor = Assert.Single(remaining);
Assert.True(survivor.DeployedAt > cutoff);
}
/// <summary>
/// review 2026-10-21 04/P7: the artifact-deploy method read returns every system's methods
/// in one untracked query.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetAllExternalSystemMethods_ReturnsEverySystemsMethods_Untracked()
{
var a = new ExternalSystemDefinition("A", "https://a.test", "ApiKey");
var b = new ExternalSystemDefinition("B", "https://b.test", "ApiKey");
_context.AddRange(a, b);
await _context.SaveChangesAsync();
_context.AddRange(
new ExternalSystemMethod("A1", "GET", "/a1") { ExternalSystemDefinitionId = a.Id },
new ExternalSystemMethod("B1", "GET", "/b1") { ExternalSystemDefinitionId = b.Id },
new ExternalSystemMethod("B2", "POST", "/b2") { ExternalSystemDefinitionId = b.Id });
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var methods = await _repository.GetAllExternalSystemMethodsAsync();
Assert.Equal(3, methods.Count);
Assert.Equal(2, methods.Count(m => m.ExternalSystemDefinitionId == b.Id));
Assert.Empty(_context.ChangeTracker.Entries<ExternalSystemMethod>());
}
}
@@ -255,6 +255,47 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Equal(TimeSpan.FromSeconds(120), artifact.RetryDelay);
}
/// <summary>
/// review 2026-10-21 04/P7: every external system's methods come from ONE query, grouped
/// per system — not one method query per external system — and each system's artifact
/// still carries exactly its own methods.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ExternalSystemMethods_AreReadInOneQuery_AndGroupedPerSystem()
{
_externalSystemRepo.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>()).Returns(
[
new ExternalSystemDefinition("Mes", "https://mes", "ApiKey") { Id = 1 },
new ExternalSystemDefinition("Erp", "https://erp", "ApiKey") { Id = 2 },
new ExternalSystemDefinition("Idle", "https://idle", "ApiKey") { Id = 3 },
]);
_deploymentRepo.GetAllExternalSystemMethodsAsync(Arg.Any<CancellationToken>()).Returns(
[
new ExternalSystemMethod("MoveOut", "POST", "/move") { Id = 10, ExternalSystemDefinitionId = 1 },
new ExternalSystemMethod("GetOrder", "GET", "/order") { Id = 11, ExternalSystemDefinitionId = 2 },
new ExternalSystemMethod("PostOrder", "POST", "/order") { Id = 12, ExternalSystemDefinitionId = 2 },
]);
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
var service = CreateServiceWithCommActor(probe);
var result = await service.RetryForSiteAsync(1, "retry-site", "admin");
Assert.True(result.IsSuccess);
await _deploymentRepo.Received(1).GetAllExternalSystemMethodsAsync(Arg.Any<CancellationToken>());
await _externalSystemRepo.DidNotReceive().GetMethodsByExternalSystemIdAsync(
Arg.Any<int>(), Arg.Any<CancellationToken>());
var artifacts = Assert.Single(recorder.Received).ExternalSystems!.ToDictionary(a => a.Name);
Assert.Contains("MoveOut", artifacts["Mes"].MethodDefinitionsJson);
Assert.DoesNotContain("Order", artifacts["Mes"].MethodDefinitionsJson);
Assert.Contains("GetOrder", artifacts["Erp"].MethodDefinitionsJson);
Assert.Contains("PostOrder", artifacts["Erp"].MethodDefinitionsJson);
Assert.Null(artifacts["Idle"].MethodDefinitionsJson);
}
/// <summary>Verifies that retrying a single site still runs the global artifact queries exactly once.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]