fix(management): ListInstances pages, site-scopes and prefix-searches in SQL (review 2026-10-21 08/P2)
This commit is contained in:
@@ -2490,34 +2490,67 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("T-030", response.JsonData);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list instances: paging applies after site scope filter.</summary>
|
||||
/// <summary>
|
||||
/// Verifies that list instances pages and site-scopes IN SQL (review 2026-10-21 08/P2): a
|
||||
/// scoped user's permitted sites are passed as the repository's site filter, so paging
|
||||
/// can never surface an out-of-scope instance, and Skip/Take/search reach the query.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ListInstances_PagingAppliesAfterSiteScopeFilter()
|
||||
{
|
||||
// Scoped user permitted to site 1 only. The repo returns 3 in-scope (site 1)
|
||||
// instances plus 2 out-of-scope (site 2). Take:2 must yield 2 of THEIR
|
||||
// instances and never an out-of-scope one.
|
||||
var uiRepo = Substitute.For<ICentralUiRepository>();
|
||||
uiRepo.GetInstancesFilteredAsync(Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Instance>
|
||||
IReadOnlyCollection<int>? passedSites = null;
|
||||
uiRepo.GetInstancesPagedAsync(
|
||||
Arg.Do<IReadOnlyCollection<int>?>(s => passedSites = s), null, "I-", 0, 2, Arg.Any<CancellationToken>())
|
||||
.Returns((new List<Instance>
|
||||
{
|
||||
new("I-001") { Id = 1, SiteId = 1 },
|
||||
new("I-002") { Id = 2, SiteId = 1 },
|
||||
new("I-003") { Id = 3, SiteId = 1 },
|
||||
new("I-101") { Id = 101, SiteId = 2 },
|
||||
new("I-102") { Id = 102, SiteId = 2 },
|
||||
});
|
||||
}, 3));
|
||||
_services.AddScoped(_ => uiRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(ScopedEnvelope(new ListInstancesCommand(Take: 2), new[] { "1" }, "Deployer"));
|
||||
actor.Tell(ScopedEnvelope(new ListInstancesCommand(SearchTerm: "I-", Take: 2), new[] { "1", "not-an-int" }, "Deployer"));
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("I-001", response.JsonData);
|
||||
Assert.Contains("I-002", response.JsonData);
|
||||
Assert.DoesNotContain("I-003", response.JsonData); // paged out
|
||||
Assert.DoesNotContain("I-101", response.JsonData); // out of scope
|
||||
Assert.DoesNotContain("I-102", response.JsonData); // out of scope
|
||||
Assert.StartsWith("[", response.JsonData.TrimStart()); // response shape unchanged: a bare array
|
||||
Assert.Equal(new[] { 1 }, passedSites); // non-integer grant dropped, never widened
|
||||
uiRepo.DidNotReceive().GetInstancesFilteredAsync(
|
||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A scoped user's explicit SiteId is intersected with their grant; an out-of-grant site
|
||||
/// yields an empty filter (nothing), and an unscoped caller's SiteId passes through.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ListInstances_SiteIdIsIntersectedWithTheScope_AndUnlimitedTakeIsUnbounded()
|
||||
{
|
||||
var uiRepo = Substitute.For<ICentralUiRepository>();
|
||||
var calls = new List<(IReadOnlyCollection<int>? Sites, int Take)>();
|
||||
uiRepo.GetInstancesPagedAsync(Arg.Any<IReadOnlyCollection<int>?>(), Arg.Any<int?>(), Arg.Any<string?>(),
|
||||
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
calls.Add(((IReadOnlyCollection<int>?)ci[0], (int)ci[4]));
|
||||
return (Array.Empty<Instance>(), 0);
|
||||
});
|
||||
_services.AddScoped(_ => uiRepo);
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(ScopedEnvelope(new ListInstancesCommand(SiteId: 2), new[] { "1" }, "Deployer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
actor.Tell(Envelope(new ListInstancesCommand(SiteId: 2), "Deployer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
actor.Tell(Envelope(new ListInstancesCommand(), "Deployer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.Empty(calls[0].Sites!);
|
||||
Assert.Equal(new[] { 2 }, calls[1].Sites);
|
||||
Assert.Null(calls[2].Sites);
|
||||
Assert.Equal(int.MaxValue, calls[2].Take);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user