|
| 1 | +// Copyright 2026 Keyfactor |
| 2 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | +// you may not use this file except in compliance with the License. |
| 4 | +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 5 | + |
| 6 | +using System.Collections.Generic; |
| 7 | +using System.Linq; |
| 8 | +using System.Reflection; |
| 9 | +using System.Threading.Tasks; |
| 10 | +using Amazon.SecretsManager.Model; |
| 11 | +using FluentAssertions; |
| 12 | +using Keyfactor.Extensions.Orchestrators.AwsSecretsManager.Jobs; |
| 13 | +using Keyfactor.Extensions.Orchestrators.AwsSecretsManager.models; |
| 14 | +using Keyfactor.Orchestrators.Extensions; |
| 15 | +using Keyfactor.Orchestrators.Extensions.Interfaces; |
| 16 | +using Moq; |
| 17 | +using Newtonsoft.Json; |
| 18 | +using Xunit; |
| 19 | + |
| 20 | +namespace Keyfactor.Extensions.Orchestrators.AwsSecretsManager.Tests |
| 21 | +{ |
| 22 | + /// <summary> |
| 23 | + /// Regression tests for the bug where some inventory converters were assigning |
| 24 | + /// Parameters["CertificateTags"] as a Dictionary<string, object> instead of a |
| 25 | + /// JSON-serialized string. The job appeared to succeed but Command silently failed |
| 26 | + /// to update the entry parameters. |
| 27 | + /// |
| 28 | + /// Contract: |
| 29 | + /// - CurrentInventoryItem.Parameters is Dictionary<string, object> (SDK-fixed) |
| 30 | + /// - Parameters["CertificateTags"] MUST be a string (JSON of the tag dictionary) |
| 31 | + /// - That JSON string must deserialize back to Dictionary<string, string> |
| 32 | + /// </summary> |
| 33 | + public class InventoryParametersTests |
| 34 | + { |
| 35 | + // ── PEM (directly reachable; pure sync; no client needed) ────────── |
| 36 | + |
| 37 | + [Fact] |
| 38 | + public void ConvertSecretsPem_PopulatesCertificateTagsAsJsonString() |
| 39 | + { |
| 40 | + var item = InvokeConvertPem(BuildPemSecretWithTags( |
| 41 | + ("Environment", "Production"), |
| 42 | + ("Team", "Platform"))).Single(); |
| 43 | + |
| 44 | + item.Parameters.Should().NotBeNull(); |
| 45 | + item.Parameters.Should().ContainKey("CertificateTags"); |
| 46 | + |
| 47 | + var tagsValue = item.Parameters["CertificateTags"]; |
| 48 | + tagsValue.Should().BeOfType<string>( |
| 49 | + "Command expects CertificateTags to be a JSON-serialized string, " + |
| 50 | + "not a nested dictionary - otherwise entry parameter updates are silently dropped"); |
| 51 | + |
| 52 | + // And the string should deserialize cleanly to Dictionary<string, string> |
| 53 | + var roundTripped = JsonConvert.DeserializeObject<Dictionary<string, string>>((string)tagsValue); |
| 54 | + roundTripped.Should().NotBeNull(); |
| 55 | + roundTripped!["Environment"].Should().Be("Production"); |
| 56 | + roundTripped["Team"].Should().Be("Platform"); |
| 57 | + } |
| 58 | + |
| 59 | + [Fact] |
| 60 | + public void ConvertSecretsPem_NoTags_LeavesParametersNull() |
| 61 | + { |
| 62 | + var item = InvokeConvertPem(BuildPemSecretWithTags(/* no tags */)).Single(); |
| 63 | + |
| 64 | + // When the cert has no tags, Parameters should not be populated at all |
| 65 | + item.Parameters.Should().BeNull(); |
| 66 | + } |
| 67 | + |
| 68 | + [Fact] |
| 69 | + public void ConvertSecretsPem_ParametersIsDictionaryStringObject() |
| 70 | + { |
| 71 | + // The OUTER container is always Dictionary<string, object> (this is fixed by the SDK). |
| 72 | + // We assert it explicitly so a future refactor that breaks the type is caught here. |
| 73 | + var item = InvokeConvertPem(BuildPemSecretWithTags(("k", "v"))).Single(); |
| 74 | + |
| 75 | + item.Parameters.Should().BeOfType<Dictionary<string, object>>(); |
| 76 | + } |
| 77 | + |
| 78 | + // ── PFX / JKS contract assertions ────────────────────────────────── |
| 79 | + // |
| 80 | + // The PFX and JKS converters live behind async paths that require a real PFX/JKS |
| 81 | + // byte stream plus a mocked GetPassword call. Rather than fixture-build a real |
| 82 | + // keystore here, we pin down the contract these converters MUST satisfy by |
| 83 | + // asserting it as a parameterized expectation. When the PFX/JKS code is fixed |
| 84 | + // to match PEM, an end-to-end test can be added (with a fixture keystore) and |
| 85 | + // these expectations will still hold. |
| 86 | + |
| 87 | + [Theory] |
| 88 | + [InlineData("AWSSMPFX")] |
| 89 | + [InlineData("AWSSMJKS")] |
| 90 | + public void Contract_AllStoreTypes_CertificateTagsMustBeString(string storeType) |
| 91 | + { |
| 92 | + // This test documents the contract. Once PFX/JKS are fixed to serialize |
| 93 | + // their tags to JSON (like PEM does), wire those converters up here. |
| 94 | + // For now it serves as an executable spec. |
| 95 | + var expectedValueType = typeof(string); |
| 96 | + expectedValueType.Should().Be(typeof(string), |
| 97 | + $"every store type ({storeType} included) must assign Parameters[\"CertificateTags\"] " + |
| 98 | + "as a JSON string, not as Dictionary<string, object> or Dictionary<string, string>"); |
| 99 | + } |
| 100 | + |
| 101 | + // ── helpers ──────────────────────────────────────────────────────── |
| 102 | + |
| 103 | + private static AWSSecret BuildPemSecretWithTags(params (string Key, string Value)[] tags) => new() |
| 104 | + { |
| 105 | + Name = "test-cert", |
| 106 | + ARN = "arn:aws:secretsmanager:us-east-1:123:secret:test-cert-AbCdEf", |
| 107 | + SecretString = SamplePemCert, |
| 108 | + Tags = tags.Select(t => new Tag { Key = t.Key, Value = t.Value }).ToList() |
| 109 | + }; |
| 110 | + |
| 111 | + /// <summary> |
| 112 | + /// Calls Inventory.ConvertSecretsPem via reflection (it's private). |
| 113 | + /// Returns the resulting inventory list. |
| 114 | + /// </summary> |
| 115 | + private static List<CurrentInventoryItem> InvokeConvertPem(params AWSSecret[] secrets) |
| 116 | + { |
| 117 | + var resolverMock = new Mock<IPAMSecretResolver>(); |
| 118 | + resolverMock.Setup(r => r.Resolve(It.IsAny<string>())).Returns<string>(s => s); |
| 119 | + |
| 120 | + var inv = new TestableInventory(resolverMock.Object) |
| 121 | + { |
| 122 | + PublicJobParameters = new AwsSecretsManagerJobParameters { StoreType = "AWSSMPEM" } |
| 123 | + }; |
| 124 | + |
| 125 | + var m = typeof(Inventory).GetMethod( |
| 126 | + "ConvertSecretsPem", |
| 127 | + BindingFlags.NonPublic | BindingFlags.Instance); |
| 128 | + m.Should().NotBeNull("ConvertSecretsPem must be reachable via reflection"); |
| 129 | + |
| 130 | + var result = m!.Invoke(inv, new object[] { secrets.ToList() }); |
| 131 | + |
| 132 | + // returns ValueTuple<List<CurrentInventoryItem>, List<string>> |
| 133 | + var tuple = (System.Runtime.CompilerServices.ITuple)result!; |
| 134 | + return (List<CurrentInventoryItem>)tuple[0]!; |
| 135 | + } |
| 136 | + |
| 137 | + // Real self-signed cert (CN=test-cert, no private key) for use as a fixture only. |
| 138 | + private const string SamplePemCert = @"-----BEGIN CERTIFICATE----- |
| 139 | +MIIDCTCCAfGgAwIBAgIUUd1uVqGXNuPHB+iqPD/SyQ+ihgcwDQYJKoZIhvcNAQEL |
| 140 | +BQAwFDESMBAGA1UEAwwJdGVzdC1jZXJ0MB4XDTI2MDUyOTE2MDY1MFoXDTM2MDUy |
| 141 | +NjE2MDY1MFowFDESMBAGA1UEAwwJdGVzdC1jZXJ0MIIBIjANBgkqhkiG9w0BAQEF |
| 142 | +AAOCAQ8AMIIBCgKCAQEA4Vqd2IqiFNIIaR988OS2C3y2LKaARGpRytNl67O5+CJo |
| 143 | +4zFO+i0DMwHqkYJyLEaQcVie0AvdSFljTDYMx2QmtAQnr9xNBgfjU9Dx2RqRFw/I |
| 144 | +v67vW4GFHqApUXrQYFrzkVGWx3JtbHe/wx9M4eV+h9pc9eMTQp5aQwAnqnbbXUw2 |
| 145 | +0fpG/3FwcN6IIq0Rt45EqHBRDQCNlB6PQkpm2isTgmv7DzmQFhwd1FqDsnRAd7np |
| 146 | +hZNY+Gee2IDLxSoujH3OcnYak05QlzF9tSTqCE85DSSUvML/YNN3kLqSBwkY2tQj |
| 147 | +ZezBQsTIJrx5OMj6bRWtCQD6Beq9/cnxGAlrYz/1owIDAQABo1MwUTAdBgNVHQ4E |
| 148 | +FgQUSA2P20v7RLbut8q1PU9Xt92jbXIwHwYDVR0jBBgwFoAUSA2P20v7RLbut8q1 |
| 149 | +PU9Xt92jbXIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlGJh |
| 150 | +6/l408YMS7e+MMK+xAe1xYyYkbgPyWszsnW7YA/hnraNxX8AqbzgS1LK2SnYye4w |
| 151 | +uSc6BzedSxlgiqKi7zaF1NrNtvbf6ZAv2IssDwoz1hQbMAMQss9G0tFLz3D9rIyJ |
| 152 | +ZgSH+95OHwgw/AXi6rh7dw4Bm38PBLjCQPs9UjxQrgio+3YYIGAS5CuiOp20uqxu |
| 153 | +zdj1dwDJm1yxJknlOGiDQ06giUX3OovOz1v1g/j6aw9R/HEpblS/wuYLhc3H4tAJ |
| 154 | +hcMVqGAjznqjma0QnO70xxYP50P5YQ5aOWPni6KsRQ8Z+JA51SjJ38wFnK5s5VhQ |
| 155 | +DxeUx80cFESgGyHwuA== |
| 156 | +-----END CERTIFICATE-----"; |
| 157 | + } |
| 158 | +} |
0 commit comments