fix(auth): C4 review polish — document backward-compat JSON tolerance, shared BundleJsonOptions, PreviewAsync legacy-bundle test, doc fix (review I-2/I-3/M-1/M-2; I-1 intentionally skipped)

This commit is contained in:
Joseph Doherty
2026-06-02 05:15:50 -04:00
parent 731cfd3bfc
commit b13d7b3d28
5 changed files with 134 additions and 15 deletions
@@ -839,4 +839,88 @@ public sealed class BundleImporterApplyTests : IDisposable
Assert.Equal(1, result.Added); // the API method
Assert.NotEqual(Guid.Empty, result.BundleImportId);
}
// ─────────────────────────────────────────────────────────────────────
// Fix I-3: PreviewAsync must also tolerate a legacy bundle that carries
// an ApiKeys section without surfacing those keys as importable preview
// rows. Mirror the ApplyAsync_ignores_legacy_api_keys_in_bundle_without_failing
// setup: hand-pack a legacy bundle with a populated ApiKeys section plus
// one ApiMethod, then assert that PreviewAsync completes without fault,
// surfaces no ApiKey/key preview items, and DOES surface the ApiMethod
// preview item.
// ─────────────────────────────────────────────────────────────────────
[Fact]
public async Task PreviewAsync_on_legacy_bundle_does_not_surface_key_items()
{
// Arrange: hand-pack a legacy bundle whose content JSON contains an
// ApiKeys array plus one API method. New exports never emit ApiKeys,
// so we build the BundleContentDto directly with a populated (non-null)
// legacy ApiKeys list to faithfully simulate a pre-C4 file.
var legacyContent = new BundleContentDto(
TemplateFolders: Array.Empty<TemplateFolderDto>(),
Templates: Array.Empty<TemplateDto>(),
SharedScripts: Array.Empty<SharedScriptDto>(),
ExternalSystems: Array.Empty<ExternalSystemDto>(),
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
NotificationLists: Array.Empty<NotificationListDto>(),
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
ApiMethods: new[]
{
new ApiMethodDto("GetStatus", "return 0;",
ParameterDefinitions: null, ReturnDefinition: null, TimeoutSeconds: 30),
},
ApiKeys: new[]
{
new ApiKeyDto("legacy-key-x", "hash-x", IsEnabled: true, Secrets: null),
});
Guid sessionId;
await using (var scope = _provider.CreateAsyncScope())
{
var serializer = scope.ServiceProvider.GetRequiredService<BundleSerializer>();
var manifestBuilder = scope.ServiceProvider.GetRequiredService<ManifestBuilder>();
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
var contentBytes = serializer.SerializeContentBytes(legacyContent);
// Sanity: the packed content really does carry an ApiKeys section,
// so the test is exercising the legacy-ignore path rather than a no-op.
var contentJson = System.Text.Encoding.UTF8.GetString(contentBytes);
Assert.Contains("legacy-key-x", contentJson);
var manifest = manifestBuilder.Build(
sourceEnvironment: "legacy-env",
exportedBy: "alice",
scadaBridgeVersion: "0.9.0",
encryption: null,
summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 1),
contents: Array.Empty<ManifestContentEntry>(),
contentBytes: contentBytes);
await using var packed = serializer.Pack(legacyContent, manifest, passphrase: null, encryptor: null);
using var ms = new MemoryStream();
await packed.CopyToAsync(ms);
ms.Position = 0;
var session = await importer.LoadAsync(ms, passphrase: null);
sessionId = session.SessionId;
}
// Act — call PreviewAsync; must not throw.
ImportPreview preview;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
preview = await importer.PreviewAsync(sessionId);
}
// Assert — no ApiKey preview rows surfaced (keys are not transported).
Assert.DoesNotContain(preview.Items, item =>
item.EntityType.Contains("Key", StringComparison.OrdinalIgnoreCase) ||
item.Name.Contains("legacy-key", StringComparison.OrdinalIgnoreCase));
// The one ApiMethod IS surfaced as a New item (method does not exist in
// the target DB, so ConflictKind.New is expected).
Assert.Contains(preview.Items, item =>
item.EntityType == "ApiMethod" && item.Name == "GetStatus");
}
}