Tests: roll forward to latest major .NET runtime #3
+7
-1
@@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 2026.06.04.0020
|
||||||
|
|
||||||
|
- Build produced from commit 211fbcf34dbb.
|
||||||
|
|
||||||
|
## Unreleased (carried forward)
|
||||||
|
|
||||||
## 2026.06.04.0005
|
## 2026.06.04.0005
|
||||||
|
|
||||||
- Build produced from commit e0a6ef02df3e.
|
- Build produced from commit e0a6ef02df3e.
|
||||||
@@ -18,7 +24,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
|
|||||||
## 2026.06.03.2207
|
## 2026.06.03.2207
|
||||||
|
|
||||||
- Build produced from commit 09c3d5c68bbc.
|
- Build produced from commit 09c3d5c68bbc.
|
||||||
- **M9 — Bulk, Duplicate & Inheritance**:
|
- **M9  Bulk, Duplicate & Inheritance**:
|
||||||
- **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`.
|
- **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`.
|
||||||
- **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles.
|
- **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles.
|
||||||
- **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line.
|
- **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = 'PSInfisicalAPI.psm1'
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
ModuleVersion = '2026.06.04.0005'
|
ModuleVersion = '2026.06.04.0020'
|
||||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
Author = 'Grace Solutions'
|
Author = 'Grace Solutions'
|
||||||
CompanyName = 'Grace Solutions'
|
CompanyName = 'Grace Solutions'
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
||||||
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
||||||
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
||||||
CommitHash = 'e0a6ef02df3e'
|
CommitHash = '211fbcf34dbb'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1,4 +1,8 @@
|
|||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Cmdlets;
|
||||||
using PSInfisicalAPI.Errors;
|
using PSInfisicalAPI.Errors;
|
||||||
using PSInfisicalAPI.Secrets;
|
using PSInfisicalAPI.Secrets;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -90,4 +94,116 @@ namespace PSInfisicalAPI.Tests
|
|||||||
Assert.Empty(items);
|
Assert.Empty(items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BulkSecretsTransformationAttributeTests
|
||||||
|
{
|
||||||
|
private static IDictionary<string, string>[] Transform(object input)
|
||||||
|
{
|
||||||
|
BulkSecretsTransformationAttribute attribute = new BulkSecretsTransformationAttribute();
|
||||||
|
object result = attribute.Transform(null, input);
|
||||||
|
return (IDictionary<string, string>[])result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Single_Hashtable_Wraps_Into_Array()
|
||||||
|
{
|
||||||
|
Hashtable input = new Hashtable { { "SecretName", "API_KEY" }, { "SecretValue", "abc" } };
|
||||||
|
|
||||||
|
IDictionary<string, string>[] result = Transform(input);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("API_KEY", result[0]["SecretName"]);
|
||||||
|
Assert.Equal("abc", result[0]["SecretValue"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Hashtable_Array_Preserves_Order()
|
||||||
|
{
|
||||||
|
Hashtable a = new Hashtable { { "SecretName", "A" }, { "SecretValue", "1" } };
|
||||||
|
Hashtable b = new Hashtable { { "SecretName", "B" }, { "SecretValue", "2" } };
|
||||||
|
|
||||||
|
IDictionary<string, string>[] result = Transform(new object[] { a, b });
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("A", result[0]["SecretName"]);
|
||||||
|
Assert.Equal("B", result[1]["SecretName"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_OrderedDictionary_Converts_To_StringString()
|
||||||
|
{
|
||||||
|
OrderedDictionary input = new OrderedDictionary();
|
||||||
|
input.Add("SecretName", "API_KEY");
|
||||||
|
input.Add("SkipMultilineEncoding", true);
|
||||||
|
|
||||||
|
IDictionary<string, string>[] result = Transform(input);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("API_KEY", result[0]["SecretName"]);
|
||||||
|
Assert.Equal("true", result[0]["SkipMultilineEncoding"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Array_Values_Are_Joined_Comma_Separated()
|
||||||
|
{
|
||||||
|
Hashtable input = new Hashtable
|
||||||
|
{
|
||||||
|
{ "SecretName", "API_KEY" },
|
||||||
|
{ "TagIds", new[] { "tag-1", "tag-2" } }
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string>[] result = Transform(input);
|
||||||
|
|
||||||
|
Assert.Equal("tag-1,tag-2", result[0]["TagIds"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Already_Typed_Array_Passes_Through()
|
||||||
|
{
|
||||||
|
IDictionary<string, string>[] input = new IDictionary<string, string>[]
|
||||||
|
{
|
||||||
|
new Dictionary<string, string> { { "SecretName", "A" }, { "SecretValue", "1" } }
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string>[] result = Transform(input);
|
||||||
|
|
||||||
|
Assert.Same(input, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Null_Input_Returns_Null()
|
||||||
|
{
|
||||||
|
BulkSecretsTransformationAttribute attribute = new BulkSecretsTransformationAttribute();
|
||||||
|
Assert.Null(attribute.Transform(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_Invalid_Element_Throws_ArgumentTransformationMetadataException()
|
||||||
|
{
|
||||||
|
BulkSecretsTransformationAttribute attribute = new BulkSecretsTransformationAttribute();
|
||||||
|
Assert.Throws<ArgumentTransformationMetadataException>(() =>
|
||||||
|
attribute.Transform(null, new object[] { "not-a-dictionary" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Transform_End_To_End_With_Converter_Produces_Bulk_Items()
|
||||||
|
{
|
||||||
|
Hashtable entry = new Hashtable
|
||||||
|
{
|
||||||
|
{ "SecretName", "API_KEY" },
|
||||||
|
{ "SecretValue", "abc" },
|
||||||
|
{ "TagIds", new[] { "tag-1", "tag-2" } },
|
||||||
|
{ "SkipMultilineEncoding", true }
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string>[] transformed = Transform(new object[] { entry });
|
||||||
|
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(transformed);
|
||||||
|
|
||||||
|
Assert.Single(items);
|
||||||
|
Assert.Equal("API_KEY", items[0].SecretName);
|
||||||
|
Assert.Equal("abc", items[0].SecretValue);
|
||||||
|
Assert.Equal(new[] { "tag-1", "tag-2" }, items[0].TagIds);
|
||||||
|
Assert.True(items[0].SkipMultilineEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||||
|
public sealed class BulkSecretsTransformationAttribute : ArgumentTransformationAttribute
|
||||||
|
{
|
||||||
|
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
|
||||||
|
{
|
||||||
|
if (inputData == null) { return null; }
|
||||||
|
|
||||||
|
object unwrapped = Unwrap(inputData);
|
||||||
|
|
||||||
|
if (unwrapped is IDictionary<string, string>[] strongArray) { return strongArray; }
|
||||||
|
|
||||||
|
if (unwrapped is IDictionary singleDict)
|
||||||
|
{
|
||||||
|
return new IDictionary<string, string>[] { Convert(singleDict) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unwrapped is IEnumerable enumerable && !(unwrapped is string))
|
||||||
|
{
|
||||||
|
List<IDictionary<string, string>> result = new List<IDictionary<string, string>>();
|
||||||
|
foreach (object element in enumerable)
|
||||||
|
{
|
||||||
|
if (element == null) { continue; }
|
||||||
|
object e = Unwrap(element);
|
||||||
|
if (e is IDictionary dict)
|
||||||
|
{
|
||||||
|
result.Add(Convert(dict));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentTransformationMetadataException(
|
||||||
|
"Each element of -Secrets must be a dictionary (Hashtable, OrderedDictionary, Dictionary<string,string>, etc.).");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentTransformationMetadataException(
|
||||||
|
"-Secrets must be a dictionary or an array of dictionaries.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object Unwrap(object value)
|
||||||
|
{
|
||||||
|
PSObject pso = value as PSObject;
|
||||||
|
return pso != null ? pso.BaseObject : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, string> Convert(IDictionary source)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (DictionaryEntry entry in source)
|
||||||
|
{
|
||||||
|
if (entry.Key == null) { continue; }
|
||||||
|
string key = entry.Key.ToString();
|
||||||
|
dict[key] = Stringify(entry.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Stringify(object value)
|
||||||
|
{
|
||||||
|
object v = Unwrap(value);
|
||||||
|
if (v == null) { return null; }
|
||||||
|
if (v is string s) { return s; }
|
||||||
|
if (v is bool b) { return b ? "true" : "false"; }
|
||||||
|
if (v is System.Security.SecureString secure)
|
||||||
|
{
|
||||||
|
return SecureStringUtility.UsePlainText(secure, plain => plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v is IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
List<string> parts = new List<string>();
|
||||||
|
foreach (object item in enumerable)
|
||||||
|
{
|
||||||
|
if (item == null) { continue; }
|
||||||
|
parts.Add(Unwrap(item).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(",", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
public SecureString SecureSecretValue { get; set; }
|
public SecureString SecureSecretValue { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
||||||
|
[BulkSecretsTransformation]
|
||||||
public IDictionary<string, string>[] Secrets { get; set; }
|
public IDictionary<string, string>[] Secrets { get; set; }
|
||||||
|
|
||||||
[Parameter] public string SecretComment { get; set; }
|
[Parameter] public string SecretComment { get; set; }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Parameter(ParameterSetName = "SecureString")] public SecureString SecureSecretValue { get; set; }
|
[Parameter(ParameterSetName = "SecureString")] public SecureString SecureSecretValue { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
||||||
|
[BulkSecretsTransformation]
|
||||||
public IDictionary<string, string>[] Secrets { get; set; }
|
public IDictionary<string, string>[] Secrets { get; set; }
|
||||||
|
|
||||||
[Parameter] public string NewSecretName { get; set; }
|
[Parameter] public string NewSecretName { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user