513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
207 lines
9.1 KiB
C#
207 lines
9.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
|
|
|
/// <summary>
|
|
/// Coverage for the startup-reconciliation repo methods on <see cref="DeploymentManagerRepository"/>:
|
|
/// <see cref="IDeploymentManagerRepository.GetExpectedDeploymentsForSiteAsync"/> (central's deployed
|
|
/// set for a site, excluding instances with no snapshot) and
|
|
/// <see cref="IDeploymentManagerRepository.StagePendingIfAbsentAsync"/> (insert-if-absent, used by
|
|
/// the reconcile handler to mint fresh fetch tokens for the gap without clobbering an in-flight deploy).
|
|
/// Uses the shared SQLite in-memory fixture with real FK enforcement.
|
|
/// </summary>
|
|
public class ReconcileRepositoryTests : IDisposable
|
|
{
|
|
private readonly ScadaBridgeDbContext _context;
|
|
private readonly DeploymentManagerRepository _repository;
|
|
|
|
/// <summary>Initializes the test with an in-memory database context and repository.</summary>
|
|
public ReconcileRepositoryTests()
|
|
{
|
|
_context = SqliteTestHelper.CreateInMemoryContext();
|
|
_repository = new DeploymentManagerRepository(_context);
|
|
}
|
|
|
|
/// <summary>Releases the resources held by this test class.</summary>
|
|
public void Dispose()
|
|
{
|
|
_context.Database.CloseConnection();
|
|
_context.Dispose();
|
|
}
|
|
|
|
// --- Seed helpers ---
|
|
|
|
/// <summary>Seeds a Site row and returns its generated id.</summary>
|
|
private async Task<int> SeedSiteAsync(string name)
|
|
{
|
|
var site = new Site(name, $"SI-{name}");
|
|
_context.Sites.Add(site);
|
|
await _context.SaveChangesAsync();
|
|
return site.Id;
|
|
}
|
|
|
|
/// <summary>Seeds a Template + Instance on the given site and returns the instance id.</summary>
|
|
private async Task<int> SeedInstanceAsync(string uniqueName, int siteId, InstanceState state = InstanceState.Enabled)
|
|
{
|
|
var template = new Template($"T-{uniqueName}");
|
|
_context.Templates.Add(template);
|
|
await _context.SaveChangesAsync();
|
|
|
|
var instance = new Instance(uniqueName) { SiteId = siteId, TemplateId = template.Id, State = state };
|
|
_context.Instances.Add(instance);
|
|
await _context.SaveChangesAsync();
|
|
return instance.Id;
|
|
}
|
|
|
|
/// <summary>Seeds a <see cref="DeployedConfigSnapshot"/> for the given instance and returns it.</summary>
|
|
private async Task<DeployedConfigSnapshot> SeedSnapshotAsync(int instanceId, string deploymentId, string revisionHash)
|
|
{
|
|
var snapshot = new DeployedConfigSnapshot(deploymentId, revisionHash, "{}") { InstanceId = instanceId };
|
|
_context.Set<DeployedConfigSnapshot>().Add(snapshot);
|
|
await _context.SaveChangesAsync();
|
|
return snapshot;
|
|
}
|
|
|
|
// --- GetExpectedDeploymentsForSiteAsync ---
|
|
|
|
/// <summary>Verifies that only deployed instances belonging to the site are returned as expected deployments.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task GetExpectedDeployments_ReturnsOnlyDeployedInstancesForSite()
|
|
{
|
|
// Arrange: site A has two deployed instances (one Enabled, one Disabled)
|
|
// site B has one deployed instance
|
|
// site A also has one instance WITHOUT a snapshot (must be excluded)
|
|
var siteAId = await SeedSiteAsync("SiteA");
|
|
var siteBId = await SeedSiteAsync("SiteB");
|
|
|
|
var instA1Id = await SeedInstanceAsync("A-Inst1", siteAId, InstanceState.Enabled);
|
|
var instA2Id = await SeedInstanceAsync("A-Inst2", siteAId, InstanceState.Disabled);
|
|
var instA3Id = await SeedInstanceAsync("A-Inst3-NoSnap", siteAId, InstanceState.Enabled);
|
|
var instB1Id = await SeedInstanceAsync("B-Inst1", siteBId, InstanceState.Enabled);
|
|
|
|
await SeedSnapshotAsync(instA1Id, "dep-a1", "hash-a1");
|
|
await SeedSnapshotAsync(instA2Id, "dep-a2", "hash-a2");
|
|
// instA3 intentionally has no snapshot
|
|
await SeedSnapshotAsync(instB1Id, "dep-b1", "hash-b1");
|
|
|
|
// Act
|
|
var result = await _repository.GetExpectedDeploymentsForSiteAsync(siteAId);
|
|
|
|
// Assert: exactly the two site-A instances that have snapshots
|
|
Assert.Equal(2, result.Count);
|
|
|
|
var byName = result.ToDictionary(r => r.InstanceUniqueName);
|
|
Assert.True(byName.ContainsKey("A-Inst1"), "A-Inst1 should be present");
|
|
Assert.True(byName.ContainsKey("A-Inst2"), "A-Inst2 should be present");
|
|
Assert.False(byName.ContainsKey("A-Inst3-NoSnap"), "instance without snapshot must be excluded");
|
|
Assert.False(byName.ContainsKey("B-Inst1"), "site B instance must be excluded");
|
|
|
|
// Check field correctness for A-Inst1 (Enabled)
|
|
var a1 = byName["A-Inst1"];
|
|
Assert.Equal(instA1Id, a1.InstanceId);
|
|
Assert.Equal("dep-a1", a1.DeploymentId);
|
|
Assert.Equal("hash-a1", a1.RevisionHash);
|
|
Assert.True(a1.IsEnabled);
|
|
|
|
// Check field correctness for A-Inst2 (Disabled)
|
|
var a2 = byName["A-Inst2"];
|
|
Assert.Equal(instA2Id, a2.InstanceId);
|
|
Assert.Equal("dep-a2", a2.DeploymentId);
|
|
Assert.Equal("hash-a2", a2.RevisionHash);
|
|
Assert.False(a2.IsEnabled);
|
|
}
|
|
|
|
/// <summary>Verifies that no expected deployments are returned when the site has no deployed instances.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task GetExpectedDeployments_NoDeployedInstances_ReturnsEmpty()
|
|
{
|
|
var siteId = await SeedSiteAsync("EmptySite");
|
|
// Seed an instance but no snapshot
|
|
await SeedInstanceAsync("Inst-NoSnap", siteId);
|
|
|
|
var result = await _repository.GetExpectedDeploymentsForSiteAsync(siteId);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
// --- StagePendingIfAbsentAsync ---
|
|
|
|
/// <summary>Verifies that staging a pending deployment with no prior row stages it and returns true.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task StagePendingIfAbsent_NoPriorRow_StagesAndReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var siteId = await SeedSiteAsync("SiteX");
|
|
var instanceId = await SeedInstanceAsync("InstX", siteId);
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
// Act
|
|
var staged = await _repository.StagePendingIfAbsentAsync(
|
|
instanceId,
|
|
deploymentId: "dep-new",
|
|
revisionHash: "rev-new",
|
|
configurationJson: "{\"x\":1}",
|
|
token: "tok-new",
|
|
createdAtUtc: now,
|
|
expiresAtUtc: now.AddMinutes(10));
|
|
|
|
// Assert: staged, and the row is retrievable with the correct token + config
|
|
Assert.True(staged);
|
|
var row = await _repository.GetPendingDeploymentByIdAsync("dep-new");
|
|
Assert.NotNull(row);
|
|
Assert.Equal(instanceId, row!.InstanceId);
|
|
Assert.Equal("tok-new", row.Token);
|
|
Assert.Equal("{\"x\":1}", row.ConfigurationJson);
|
|
Assert.Equal("rev-new", row.RevisionHash);
|
|
}
|
|
|
|
/// <summary>Verifies that staging a pending deployment when a row already exists returns false and does not overwrite it.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task StagePendingIfAbsent_PriorRowExists_ReturnsFalseAndDoesNotOverwrite()
|
|
{
|
|
// Arrange: an existing pending row for the instance (simulates an in-flight deploy)
|
|
var siteId = await SeedSiteAsync("SiteY");
|
|
var instanceId = await SeedInstanceAsync("InstY", siteId);
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
var existing = new PendingDeployment(
|
|
"dep-existing", instanceId, revisionHash: "rev-existing",
|
|
configurationJson: "{\"existing\":true}", token: "tok-existing",
|
|
createdAtUtc: now.AddMinutes(-5), expiresAtUtc: now.AddMinutes(5));
|
|
_context.Set<PendingDeployment>().Add(existing);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Act: try to stage a newer row for the same instance
|
|
var staged = await _repository.StagePendingIfAbsentAsync(
|
|
instanceId,
|
|
deploymentId: "dep-reconcile",
|
|
revisionHash: "rev-reconcile",
|
|
configurationJson: "{\"reconcile\":true}",
|
|
token: "tok-reconcile",
|
|
createdAtUtc: now,
|
|
expiresAtUtc: now.AddMinutes(10));
|
|
|
|
// Assert: skipped (returns false), existing row is unchanged
|
|
Assert.False(staged);
|
|
|
|
var existingRow = await _repository.GetPendingDeploymentByIdAsync("dep-existing");
|
|
Assert.NotNull(existingRow);
|
|
Assert.Equal("tok-existing", existingRow!.Token);
|
|
Assert.Equal("{\"existing\":true}", existingRow.ConfigurationJson);
|
|
|
|
// The reconcile row must not exist
|
|
Assert.Null(await _repository.GetPendingDeploymentByIdAsync("dep-reconcile"));
|
|
}
|
|
}
|