M9: bulk + duplicate + connection inheritance

- Bulk parameter sets on New-/Update-/Remove-InfisicalSecret via v3/secrets/batch/raw.
- Copy-InfisicalSecret cmdlet wrapping v4/secrets/duplicate.
- InfisicalCmdletBase.Resolve{ProjectId,Environment,SecretPath,ApiVersion,OrganizationId} with verbose inheritance logging.
- All resource cmdlets refactored to use the resolution helpers.
- InfisicalBulkSecretConverter for flexible Hashtable -> DTO mapping.
- 22 new unit tests covering registry, DTOs, converter, and inheritance helpers. Total: 161 passing.
This commit is contained in:
GraceSolutions
2026-06-03 19:59:11 -04:00
parent 09c3d5c68b
commit e0a6ef02df
40 changed files with 1282 additions and 65 deletions
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Reflection;
using PSInfisicalAPI.Cmdlets;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class CmdletBaseInheritanceTests
{
private sealed class RecordingLogger : IInfisicalLogger
{
public List<string> VerboseEntries { get; } = new List<string>();
public void Information(string component, string message) { }
public void Verbose(string component, string message) { VerboseEntries.Add(message); }
public void Debug(string component, string message) { }
public void Warning(string component, string message) { }
public void Error(string component, string message) { }
}
[Cmdlet(VerbsCommon.Get, "TestCmdlet")]
private sealed class TestCmdlet : InfisicalCmdletBase
{
public string CallResolveProjectId(InfisicalConnection connection, string explicitValue)
{
return ResolveProjectId(connection, explicitValue);
}
public string CallResolveEnvironment(InfisicalConnection connection, string explicitValue)
{
return ResolveEnvironment(connection, explicitValue);
}
public string CallResolveSecretPath(InfisicalConnection connection, string explicitValue)
{
return ResolveSecretPath(connection, explicitValue);
}
public string CallResolveApiVersion(InfisicalConnection connection, string explicitValue)
{
return ResolveApiVersion(connection, explicitValue);
}
public string CallResolveOrganizationId(InfisicalConnection connection, string explicitValue)
{
return ResolveOrganizationId(connection, explicitValue);
}
}
private static TestCmdlet CreateCmdletWith(RecordingLogger logger)
{
TestCmdlet cmdlet = new TestCmdlet();
FieldInfo field = typeof(InfisicalCmdletBase).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(cmdlet, logger);
return cmdlet;
}
private static InfisicalConnection ConnectionWithDefaults()
{
return new InfisicalConnection
{
BaseUri = new Uri("https://app.example.com"),
ProjectId = "proj-conn",
Environment = "prod-conn",
DefaultSecretPath = "/db",
OrganizationId = "org-conn",
PinnedApiVersion = "v3"
};
}
[Fact]
public void Explicit_Value_Overrides_Connection_And_Does_Not_Log()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveProjectId(ConnectionWithDefaults(), "explicit-proj");
Assert.Equal("explicit-proj", resolved);
Assert.Empty(logger.VerboseEntries);
}
[Fact]
public void Missing_Value_Inherits_From_Connection_And_Logs()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveProjectId(ConnectionWithDefaults(), null);
Assert.Equal("proj-conn", resolved);
Assert.Single(logger.VerboseEntries);
Assert.Contains("Inherited ProjectId", logger.VerboseEntries[0]);
Assert.Contains("proj-conn", logger.VerboseEntries[0]);
}
[Fact]
public void ResolveSecretPath_Defaults_To_Root_When_Connection_Has_No_Default()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
InfisicalConnection bareConnection = new InfisicalConnection { BaseUri = new Uri("https://app.example.com") };
string resolved = cmdlet.CallResolveSecretPath(bareConnection, null);
Assert.Equal("/", resolved);
}
[Fact]
public void ResolveSecretPath_Inherits_From_Connection_When_Set()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveSecretPath(ConnectionWithDefaults(), null);
Assert.Equal("/db", resolved);
Assert.Contains(logger.VerboseEntries, v => v.Contains("SecretPath") && v.Contains("/db"));
}
[Fact]
public void ResolveApiVersion_Prefers_PinnedApiVersion_From_Connection()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveApiVersion(ConnectionWithDefaults(), null);
Assert.Equal("v3", resolved);
}
[Fact]
public void ResolveEnvironment_And_ResolveOrganizationId_Inherit()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
Assert.Equal("prod-conn", cmdlet.CallResolveEnvironment(ConnectionWithDefaults(), null));
Assert.Equal("org-conn", cmdlet.CallResolveOrganizationId(ConnectionWithDefaults(), null));
Assert.Equal(2, logger.VerboseEntries.Count);
}
}
}