fix(transport): curate BundleLockedException on the management surface (register row 51)
ManagementService references Commons but not Transport, so MapFault could not see BundleLockedException and the per-bundle 3-strike lockout reached the CLI as "An internal error occurred (CorrelationId=...)" — the operator got a correlation id instead of the reason, and no hint that further attempts on that bundle would be refused. Applies the pattern register row 44 established for BundleUnlockThrottledException: - Relocate BundleLockedException from Transport/Import/ to Commons/Types/Transport/, beside BundleUnlockThrottledException. Both consumers (BundleImporter, CentralUI TransportImport) already carried the Commons.Types.Transport using, so no call-site edits were needed. - Add curated catch arms in ManagementActor.HandlePreviewBundle and HandleImportBundle that rethrow as ManagementCommandException, so the message survives MapFault's disclosure filter. - Mirror the row-44 throttle fault-mapping test for both handlers. - Document the relocation in docs/components/Transport.md and docs/requirements/Component-Transport.md. Tests: ManagementService 499 passed (+2 new), Transport 167 passed.
This commit is contained in:
@@ -3219,6 +3219,51 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("Too many bundle unlock attempts", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportBundleCommand_LockedBundle_SurfacesTheCuratedReason()
|
||||
{
|
||||
// Register row 51: the per-bundle 3-strike lockout used to reach the CLI as
|
||||
// MapFault's generic "An internal error occurred (CorrelationId=...)" because
|
||||
// BundleLockedException lived in Transport, which ManagementService does not
|
||||
// reference. It now lives in Commons alongside BundleUnlockThrottledException
|
||||
// and is curated the same way.
|
||||
var (_, importer) = AddBundleSubstitutes();
|
||||
|
||||
importer.LoadAsync(Arg.Any<Stream>(), Arg.Any<string?>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns<Commons.Types.Transport.BundleSession>(_ =>
|
||||
throw new Commons.Types.Transport.BundleLockedException("abc123", 3));
|
||||
|
||||
var payload = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 });
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new ImportBundleCommand(payload, "wrong", "skip"), "Administrator"));
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("locked after 3 failed unlock attempts", response.Error);
|
||||
Assert.DoesNotContain("An internal error occurred", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreviewBundleCommand_LockedBundle_SurfacesTheCuratedReason()
|
||||
{
|
||||
// Register row 51: preview shares the same LoadAsync fault surface as import,
|
||||
// so it needs the same curated arm.
|
||||
var (_, importer) = AddBundleSubstitutes();
|
||||
|
||||
importer.LoadAsync(Arg.Any<Stream>(), Arg.Any<string?>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns<Commons.Types.Transport.BundleSession>(_ =>
|
||||
throw new Commons.Types.Transport.BundleLockedException("abc123", 3));
|
||||
|
||||
var payload = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 });
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new PreviewBundleCommand(payload, "wrong"), "Administrator"));
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("locked after 3 failed unlock attempts", response.Error);
|
||||
Assert.DoesNotContain("An internal error occurred", response.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Native alarm source CRUD (Task 21)
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user