feat(management): additive Skip/Take paging on template/instance lists (arch-review P2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -151,6 +151,41 @@ public class CommandTreeTests
|
||||
Assert.Contains("clear", subNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TemplateList_HasSkipAndTakePagingOptions()
|
||||
{
|
||||
// arch-review P2: `template list` gained additive --skip/--take offset paging.
|
||||
var template = TemplateCommands.Build(Url, Format, Username, Password);
|
||||
var list = template.Subcommands.Single(c => c.Name == "list");
|
||||
var optionNames = list.Options.Select(o => o.Name).ToList();
|
||||
Assert.Contains("--skip", optionNames);
|
||||
Assert.Contains("--take", optionNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceList_HasSkipAndTakePagingOptions()
|
||||
{
|
||||
// arch-review P2: `instance list` gained additive --skip/--take offset paging.
|
||||
var instance = InstanceCommands.Build(Url, Format, Username, Password);
|
||||
var list = instance.Subcommands.Single(c => c.Name == "list");
|
||||
var optionNames = list.Options.Select(o => o.Name).ToList();
|
||||
Assert.Contains("--skip", optionNames);
|
||||
Assert.Contains("--take", optionNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceList_ParsesTakeOptionValue()
|
||||
{
|
||||
// Parse-level check that --take binds to an int value on the list command.
|
||||
var instance = InstanceCommands.Build(Url, Format, Username, Password);
|
||||
var list = instance.Subcommands.Single(c => c.Name == "list");
|
||||
var takeOption = list.Options.Single(o => o.Name == "--take");
|
||||
|
||||
var parse = list.Parse(new[] { "--skip", "10", "--take", "5" });
|
||||
Assert.Empty(parse.Errors);
|
||||
Assert.Equal(5, parse.GetValue((Option<int?>)takeOption));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(GetInstanceCommand))]
|
||||
[InlineData(typeof(ListSitesCommand))]
|
||||
|
||||
@@ -1378,6 +1378,78 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
_artifactService.DidNotReceiveWithAnyArgs().DeployToAllSitesAsync(default!, default);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Additive Skip/Take paging on template/instance lists (arch-review P2)
|
||||
//
|
||||
// Take = null preserves the historical unlimited behaviour. When set, paging
|
||||
// applies AFTER any in-memory site-scope filtering, so a scoped user never sees
|
||||
// an out-of-scope entity surface into their page window.
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ListTemplates_SkipTake_ReturnsRequestedWindow()
|
||||
{
|
||||
var templates = Enumerable.Range(1, 30)
|
||||
.Select(i => new Template($"T-{i:D3}") { Id = i })
|
||||
.ToList();
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>()).Returns(templates);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new ListTemplatesCommand(Skip: 10, Take: 5)));
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
// Items 11..15 (1-based) present; the pages on either side absent.
|
||||
Assert.Contains("T-011", response.JsonData);
|
||||
Assert.Contains("T-015", response.JsonData);
|
||||
Assert.DoesNotContain("T-010", response.JsonData);
|
||||
Assert.DoesNotContain("T-016", response.JsonData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListTemplates_DefaultTake_ReturnsAll()
|
||||
{
|
||||
var templates = Enumerable.Range(1, 30)
|
||||
.Select(i => new Template($"T-{i:D3}") { Id = i })
|
||||
.ToList();
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>()).Returns(templates);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new ListTemplatesCommand())); // Take = null => unlimited
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("T-001", response.JsonData);
|
||||
Assert.Contains("T-030", response.JsonData);
|
||||
}
|
||||
|
||||
[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>
|
||||
{
|
||||
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 },
|
||||
});
|
||||
_services.AddScoped(_ => uiRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(ScopedEnvelope(new ListInstancesCommand(Take: 2), new[] { "1" }, "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
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SetInstanceOverrides atomicity (finding -015)
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user