feat(managementservice): MgmtDeployArea — Deployer-only, scoped by the area's owning site (fail-closed on an unknown area), long-running like MgmtDeploySite (Gitea #40)
This commit is contained in:
@@ -1391,6 +1391,64 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Gitea #40: MgmtDeployArea is scoped by the area's OWNING site, and an
|
||||
// unresolvable area fails closed for a scoped caller (register row 98).
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that deploy area, when the area is out of scope for a site scoped user, returns unauthorized.</summary>
|
||||
[Fact]
|
||||
public void DeployArea_OutOfScopeForSiteScopedUser_ReturnsUnauthorized()
|
||||
{
|
||||
// Area 12 belongs to site 2; user is scoped to site 1.
|
||||
_templateRepo.GetAreaByIdAsync(12, Arg.Any<CancellationToken>())
|
||||
.Returns(new Area("Line") { Id = 12, SiteId = 2 });
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = ScopedEnvelope(new MgmtDeployAreaCommand(12), new[] { "1" }, "Deployer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deploy area, when the area does not exist and the caller is site scoped, returns unauthorized without an existence oracle.</summary>
|
||||
[Fact]
|
||||
public void DeployArea_NonexistentAreaForSiteScopedUser_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = ScopedEnvelope(new MgmtDeployAreaCommand(999), new[] { "1" }, "Deployer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.DoesNotContain("not found", response.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("exist", response.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deploy area, when the area does not exist and the caller is system wide, returns a clean not-found error.</summary>
|
||||
[Fact]
|
||||
public void DeployArea_NonexistentAreaForSystemWideUser_ReturnsNotFoundError()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new MgmtDeployAreaCommand(999), "Deployer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("not found", response.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deploy area is refused for a Designer.</summary>
|
||||
[Fact]
|
||||
public void DeployArea_WithDesignerRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new MgmtDeployAreaCommand(12), "Designer"));
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that get instance, when in scope for site scoped user, returns success.</summary>
|
||||
[Fact]
|
||||
public void GetInstance_InScopeForSiteScopedUser_ReturnsSuccess()
|
||||
|
||||
@@ -48,6 +48,25 @@ public class ManagementEndpointsTests
|
||||
Assert.Equal(expectedForce, command.Force);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>MgmtDeployArea</c> (Gitea #40) parses with the same safe <c>Force</c>
|
||||
/// default as the site deploy: absent means skip unchanged instances.
|
||||
/// </summary>
|
||||
/// <param name="json">The management request body to parse.</param>
|
||||
/// <param name="expectedForce">The expected value of the command's <c>Force</c> flag.</param>
|
||||
[Theory]
|
||||
[InlineData("""{ "command": "MgmtDeployArea", "payload": { "areaId": 12 } }""", false)]
|
||||
[InlineData("""{ "command": "MgmtDeployArea", "payload": { "areaId": 12, "force": true } }""", true)]
|
||||
public void ParseCommand_DeployArea_ForceDefaultsToTheNonDestructiveBehaviour(string json, bool expectedForce)
|
||||
{
|
||||
var result = ManagementEndpoints.ParseCommand(json);
|
||||
|
||||
Assert.True(result.Success);
|
||||
var command = Assert.IsType<MgmtDeployAreaCommand>(result.Command);
|
||||
Assert.Equal(12, command.AreaId);
|
||||
Assert.Equal(expectedForce, command.Force);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a request with no payload deserializes a parameterless command.</summary>
|
||||
[Fact]
|
||||
public void ParseCommand_WithMissingPayload_DeserializesParameterlessCommand()
|
||||
@@ -168,4 +187,16 @@ public class ManagementEndpointsTests
|
||||
var options = new ManagementServiceOptions { LongRunningCommandTimeout = TimeSpan.FromMinutes(2) };
|
||||
Assert.Equal(TimeSpan.FromMinutes(3), ManagementEndpoints.ResolveAskTimeout(options, typeof(MgmtDeploySiteCommand)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The area-subtree bulk deploy (Gitea #40) is a bulk deploy too: same
|
||||
/// long-running deadline plus the same cleanup grace as the site deploy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BulkAsk_AreaDeploy_GetsTheSameGraceAsSiteDeploy()
|
||||
{
|
||||
var options = new ManagementServiceOptions { LongRunningCommandTimeout = TimeSpan.FromMinutes(2) };
|
||||
Assert.Equal(TimeSpan.FromMinutes(3), ManagementEndpoints.ResolveAskTimeout(options, typeof(MgmtDeployAreaCommand)));
|
||||
Assert.Equal(TimeSpan.FromMinutes(6), ManagementEndpoints.ResolveAskTimeout(null, typeof(MgmtDeployAreaCommand)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,9 @@ public class RequiredRoleMatrixTests
|
||||
// Bulk site deploy. Same authority as the single-instance deploy
|
||||
// it batches — it is N of those, not a new class of privilege.
|
||||
["MgmtDeploySite"] = [Roles.Deployer],
|
||||
// Area-subtree bulk deploy (Gitea #40): the site deploy narrowed to one
|
||||
// area subtree — same authority, never a wider one.
|
||||
["MgmtDeployArea"] = [Roles.Deployer],
|
||||
["MgmtEnableInstance"] = [Roles.Deployer],
|
||||
["MgmtDisableInstance"] = [Roles.Deployer],
|
||||
["MgmtDeleteInstance"] = [Roles.Deployer],
|
||||
|
||||
Reference in New Issue
Block a user