Implement PSInfisicalAPI module per design spec with env-var auto-discovery

This commit is contained in:
GraceSolutions
2026-06-02 12:46:34 -04:00
parent 3c47d6ff30
commit 430e3a00c9
80 changed files with 6361 additions and 0 deletions
@@ -0,0 +1,49 @@
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class EndpointRegistryTests
{
[Fact]
public void Get_ListSecrets_Returns_Definition()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
Assert.Equal("GET", definition.Method);
Assert.Equal("v4", definition.Version);
Assert.Equal("/api/v4/secrets", definition.Template);
Assert.True(definition.RequiresAuthorization);
Assert.False(definition.ContainsSecretMaterialInRequest);
Assert.True(definition.ContainsSecretMaterialInResponse);
}
[Fact]
public void Get_RetrieveSecret_Returns_Definition()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
Assert.Equal("GET", definition.Method);
Assert.Equal("v4", definition.Version);
Assert.Equal("/api/v4/secrets/{secretName}", definition.Template);
Assert.True(definition.RequiresAuthorization);
}
[Fact]
public void Get_UniversalAuthLogin_Returns_Definition()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.UniversalAuthLogin);
Assert.Equal("POST", definition.Method);
Assert.Equal("v1", definition.Version);
Assert.Equal("/api/v1/auth/universal-auth/login", definition.Template);
Assert.False(definition.RequiresAuthorization);
Assert.True(definition.ContainsSecretMaterialInRequest);
Assert.True(definition.ContainsSecretMaterialInResponse);
}
[Fact]
public void Get_Unknown_Endpoint_Throws()
{
Assert.Throws<InfisicalConfigurationException>(() => InfisicalEndpointRegistry.Get("NotARealEndpoint"));
}
}
}
@@ -0,0 +1,63 @@
using PSInfisicalAPI.Errors;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class ErrorHandlerTests
{
[Fact]
public void BuildDetails_Preserves_Http_Status_Code()
{
InfisicalApiException exception = new InfisicalApiException("Forbidden")
{
StatusCode = 403,
ReasonPhrase = "Forbidden",
EndpointName = "ListSecrets",
RequestMethod = "GET",
ApiErrorCode = "PERMISSION_DENIED",
SanitizedBody = "[REDACTED]"
};
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails("SecretsClient", "RetrieveSecrets", exception);
Assert.Equal(403, details.StatusCode);
Assert.Equal("Forbidden", details.ReasonPhrase);
Assert.Equal("PERMISSION_DENIED", details.ApiErrorCode);
Assert.Equal("ListSecrets", details.EndpointName);
Assert.Equal("[REDACTED]", details.SanitizedBody);
Assert.Equal("SecretsClient", details.Component);
Assert.Equal("RetrieveSecrets", details.Operation);
}
[Fact]
public void BuildDetails_Preserves_Serialization_Line_Information()
{
InfisicalSerializationException exception = new InfisicalSerializationException("Bad JSON")
{
LineNumber = 12,
LinePosition = 34
};
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails("Serializer", "Deserialize", exception);
Assert.Equal(12, details.LineNumber);
Assert.Equal(34, details.LinePosition);
}
[Fact]
public void MapCategory_Maps_Auth_Exception()
{
System.Management.Automation.ErrorCategory category = InfisicalErrorHandler.MapCategory(new InfisicalAuthenticationException("Bad creds"));
Assert.Equal(System.Management.Automation.ErrorCategory.AuthenticationError, category);
}
[Fact]
public void MapCategory_Maps_Api_Exception_By_Status_Code()
{
Assert.Equal(System.Management.Automation.ErrorCategory.AuthenticationError, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 401 }));
Assert.Equal(System.Management.Automation.ErrorCategory.PermissionDenied, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 403 }));
Assert.Equal(System.Management.Automation.ErrorCategory.ObjectNotFound, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 404 }));
Assert.Equal(System.Management.Automation.ErrorCategory.ResourceUnavailable, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 503 }));
}
}
}
+154
View File
@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using PSInfisicalAPI.Exports;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class ExportTests : IDisposable
{
private readonly DirectoryInfo _tempDirectory;
public ExportTests()
{
string root = Path.Combine(Path.GetTempPath(), "PSInfisicalAPI.Tests.Export-" + Guid.NewGuid().ToString("N"));
_tempDirectory = new DirectoryInfo(root);
_tempDirectory.Create();
}
public void Dispose()
{
if (_tempDirectory.Exists)
{
_tempDirectory.Delete(true);
}
}
private static InfisicalSecret[] SampleSecrets()
{
return new[]
{
new InfisicalSecret
{
SecretName = "SqlServer",
SecretPath = "/servers",
SecretValue = SecureStringUtility.ToReadOnlySecureString("192.168.1.10"),
SecretMetadata = new[] { new InfisicalSecretMetadata { Key = "Owner", Value = "Infrastructure" } }
},
new InfisicalSecret
{
SecretName = "SqlPassword",
SecretPath = "/servers",
SecretValue = SecureStringUtility.ToReadOnlySecureString("ExamplePassword")
}
};
}
[Fact]
public void Export_Env_Writes_Key_Equals_Value()
{
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "missing", "out.env"));
new EnvInfisicalExporter().Export(new InfisicalExportRequest
{
Secrets = SampleSecrets(),
Format = InfisicalExportFormat.Env,
Path = path,
Encoding = new UTF8Encoding(false)
});
Assert.True(path.Exists || File.Exists(path.FullName));
string contents = File.ReadAllText(path.FullName);
Assert.Contains("SqlServer=192.168.1.10", contents);
Assert.Contains("SqlPassword=ExamplePassword", contents);
}
[Fact]
public void Export_Json_Creates_Directory()
{
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "deep", "nested", "out.json"));
new JsonInfisicalExporter().Export(new InfisicalExportRequest
{
Secrets = SampleSecrets(),
Format = InfisicalExportFormat.Json,
Path = path
});
Assert.True(path.Directory.Exists);
string contents = File.ReadAllText(path.FullName);
Assert.Contains("SqlServer", contents);
Assert.Contains("192.168.1.10", contents);
}
[Fact]
public void Export_Yaml_Creates_Directory()
{
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "yaml", "out.yaml"));
new YamlInfisicalExporter().Export(new InfisicalExportRequest
{
Secrets = SampleSecrets(),
Format = InfisicalExportFormat.Yaml,
Path = path
});
Assert.True(path.Directory.Exists);
string contents = File.ReadAllText(path.FullName);
Assert.Contains("Secrets:", contents);
Assert.Contains("SqlServer", contents);
}
[Fact]
public void Export_Xml_Matches_Schema()
{
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "xml", "out.xml"));
new XmlInfisicalExporter().Export(new InfisicalExportRequest
{
Secrets = SampleSecrets(),
Format = InfisicalExportFormat.Xml,
Path = path
});
string contents = File.ReadAllText(path.FullName);
Assert.Contains("<Secrets>", contents);
Assert.Contains("<Secret>", contents);
Assert.Contains("<SecretName>SqlServer</SecretName>", contents);
Assert.Contains("<SecretValue>192.168.1.10</SecretValue>", contents);
Assert.Contains("<SecretMetadata>", contents);
Assert.Contains("<Metadata>", contents);
}
[Fact]
public void Export_EnvironmentVariables_Defaults_To_Process()
{
string name = "PSInFI_TestVar_" + Guid.NewGuid().ToString("N");
InfisicalSecret[] secrets = new[]
{
new InfisicalSecret
{
SecretName = name,
SecretValue = SecureStringUtility.ToReadOnlySecureString("processed")
}
};
try
{
new EnvironmentVariableExporter().Export(new InfisicalExportRequest
{
Secrets = secrets,
Format = InfisicalExportFormat.EnvironmentVariables,
Scope = EnvironmentVariableTarget.Process
});
Assert.Equal("processed", Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process));
}
finally
{
Environment.SetEnvironmentVariable(name, null, EnvironmentVariableTarget.Process);
}
}
}
}
@@ -0,0 +1,129 @@
using System.Text.RegularExpressions;
using PSInfisicalAPI.Authentication;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class InfisicalEnvironmentPatternTests
{
[Theory]
[InlineData("INFISICAL_API_URL")]
[InlineData("INFISICAL_BASE_URL")]
[InlineData("INFISICAL_BASE_URI")]
[InlineData("INFISICAL_HOST")]
[InlineData("CLOUDINIT_INFISICAL_APIURL")]
public void BaseUriPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.BaseUriPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_ORG_ID")]
[InlineData("INFISICAL_ORGANIZATION_ID")]
[InlineData("CLOUDINIT_INFISICAL_ORGANIZATIONID")]
public void OrganizationIdPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_PROJECT_ID")]
[InlineData("INFISICAL_WORKSPACE_ID")]
[InlineData("CLOUDINIT_INFISICAL_PROJECTID")]
public void ProjectIdPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_ENVIRONMENT")]
[InlineData("INFISICAL_ENVIRONMENT_NAME")]
[InlineData("INFISICAL_ENV")]
[InlineData("INFISICAL_ENV_SLUG")]
[InlineData("CLOUDINIT_INFISICAL_ENVIRONMENT")]
public void EnvironmentPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_CLIENT_ID")]
[InlineData("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")]
[InlineData("INFISICAL_MACHINE_IDENTITY_CLIENT_ID")]
[InlineData("CLOUDINIT_INFISICAL_CLIENTID")]
[InlineData("myapp_infisical_client_id")]
public void ClientIdPatterns_Match_Standard_And_Custom_Prefixed_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ClientIdPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_CLIENT_SECRET")]
[InlineData("INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET")]
[InlineData("INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET")]
[InlineData("CLOUDINIT_INFISICAL_CLIENTSECRET")]
[InlineData("myapp_infisical_client_secret")]
public void ClientSecretPatterns_Match_Standard_And_Custom_Prefixed_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_TOKEN")]
[InlineData("INFISICAL_ACCESS_TOKEN")]
[InlineData("INFISICAL_AUTH_TOKEN")]
[InlineData("CLOUDINIT_INFISICAL_TOKEN")]
public void AccessTokenPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_SECRET_PATH")]
[InlineData("INFISICAL_DEFAULT_SECRET_PATH")]
[InlineData("CLOUDINIT_INFISICAL_SECRETPATH")]
public void SecretPathPatterns_Match_Expected_Names(string name)
{
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns), "Expected match for " + name);
}
[Theory]
[InlineData("INFISICAL_SECRET_PATH")]
[InlineData("INFISICAL_DEFAULT_SECRET_PATH")]
[InlineData("CLOUDINIT_INFISICAL_SECRETPATH")]
public void ClientSecretPatterns_Do_Not_Match_SecretPath_Variables(string name)
{
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns), "ClientSecretPatterns should NOT match " + name);
}
[Theory]
[InlineData("PATH")]
[InlineData("USERNAME")]
[InlineData("HOME")]
[InlineData("PROCESSOR_ARCHITECTURE")]
public void Patterns_Do_Not_Match_Unrelated_System_Variables(string name)
{
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientIdPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.BaseUriPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns));
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ApiVersionPatterns));
}
private static bool MatchesAny(string input, Regex[] patterns)
{
for (int i = 0; i < patterns.Length; i++)
{
if (patterns[i].IsMatch(input))
{
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Security;
using System.Text.RegularExpressions;
using PSInfisicalAPI.Authentication;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Security;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class InfisicalEnvironmentResolverTests : IDisposable
{
private const string TestPrefix = "PSINFITESTRESOLVER";
private readonly List<string> _createdVariables = new List<string>();
private readonly string _uniqueSuffix = Guid.NewGuid().ToString("N").ToUpperInvariant();
private string SetProcessVar(string token, string value)
{
string name = TestPrefix + "_" + token + "_" + _uniqueSuffix;
Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
_createdVariables.Add(name);
return name;
}
private Regex[] PatternsForThisTest(string tokenWithinName)
{
return new[]
{
new Regex("^" + TestPrefix + "_.*" + tokenWithinName + ".*" + _uniqueSuffix + "$", RegexOptions.IgnoreCase)
};
}
public void Dispose()
{
for (int i = 0; i < _createdVariables.Count; i++)
{
Environment.SetEnvironmentVariable(_createdVariables[i], null, EnvironmentVariableTarget.Process);
}
}
[Fact]
public void Resolve_Returns_NotFound_When_No_Pattern_Matches()
{
Regex[] patterns = new[] { new Regex("WILL_NEVER_MATCH_" + Guid.NewGuid().ToString("N"), RegexOptions.IgnoreCase) };
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(patterns);
Assert.False(result.Found);
}
[Fact]
public void Resolve_Returns_First_Matching_Variable_With_Value()
{
string name = SetProcessVar("CLIENTID", "client-1234");
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(PatternsForThisTest("CLIENTID"));
Assert.True(result.Found);
Assert.Equal("client-1234", result.Value);
Assert.Equal(name, result.VariableName);
Assert.Equal(EnvironmentVariableTarget.Process, result.Scope);
}
[Fact]
public void Resolve_Skips_Blank_Whitespace_Values()
{
SetProcessVar("CLIENTID_BLANK", " ");
string realName = SetProcessVar("CLIENTID_REAL", "client-xyz");
Regex[] patterns = new[]
{
new Regex("^" + TestPrefix + "_CLIENTID_(BLANK|REAL)_" + _uniqueSuffix + "$", RegexOptions.IgnoreCase)
};
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(patterns);
Assert.True(result.Found);
Assert.Equal("client-xyz", result.Value);
Assert.Equal(realName, result.VariableName);
}
[Fact]
public void ResolveString_Returns_Current_Value_When_Already_Set()
{
SetProcessVar("PROJECTID_OVERRIDE", "env-project");
string resolved = InfisicalEnvironmentResolver.ResolveString("ProjectId", PatternsForThisTest("PROJECTID"), "explicit-project", NullInfisicalLogger.Instance);
Assert.Equal("explicit-project", resolved);
}
[Fact]
public void ResolveString_Resolves_When_Current_Value_Is_Whitespace()
{
SetProcessVar("PROJECTID_FALLBACK", "env-project-123");
string resolved = InfisicalEnvironmentResolver.ResolveString("ProjectId", PatternsForThisTest("PROJECTID"), " ", NullInfisicalLogger.Instance);
Assert.Equal("env-project-123", resolved);
}
[Fact]
public void ResolveSecureString_Builds_ReadOnly_SecureString_From_Env()
{
SetProcessVar("ACCESSTOKEN_FOR_TEST", "tok-9999");
SecureString resolved = InfisicalEnvironmentResolver.ResolveSecureString("AccessToken", PatternsForThisTest("ACCESSTOKEN"), null, NullInfisicalLogger.Instance);
Assert.NotNull(resolved);
Assert.True(resolved.IsReadOnly());
string plain = SecureStringUtility.UsePlainText(resolved, p => p);
Assert.Equal("tok-9999", plain);
}
[Fact]
public void ResolveSecureString_Keeps_Existing_Populated_SecureString()
{
SetProcessVar("ACCESSTOKEN_IGNORE", "ignored");
SecureString existing = SecureStringUtility.ToReadOnlySecureString("explicit-token");
SecureString resolved = InfisicalEnvironmentResolver.ResolveSecureString("AccessToken", PatternsForThisTest("ACCESSTOKEN"), existing, NullInfisicalLogger.Instance);
Assert.Same(existing, resolved);
}
}
}
@@ -0,0 +1,50 @@
using System.Reflection;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class InfisicalSecretTests
{
[Fact]
public void ToString_Returns_SecretName_Only()
{
InfisicalSecret secret = new InfisicalSecret
{
SecretName = "MySecret",
SecretValue = SecureStringUtility.ToReadOnlySecureString("super-secret")
};
Assert.Equal("MySecret", secret.ToString());
Assert.DoesNotContain("super-secret", secret.ToString());
}
[Fact]
public void UsePlainTextValue_Scopes_Plaintext_Access()
{
InfisicalSecret secret = new InfisicalSecret
{
SecretName = "DbPassword",
SecretValue = SecureStringUtility.ToReadOnlySecureString("p@ssw0rd")
};
string observed = secret.UsePlainTextValue(value => value);
Assert.Equal("p@ssw0rd", observed);
}
[Fact]
public void Has_No_PlainText_Property()
{
PropertyInfo[] properties = typeof(InfisicalSecret).GetProperties();
foreach (PropertyInfo property in properties)
{
Assert.False(string.Equals(property.Name, "PlainTextValue", System.StringComparison.OrdinalIgnoreCase));
if (property.Name == "SecretValue")
{
Assert.Equal(typeof(System.Security.SecureString), property.PropertyType);
}
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Text.RegularExpressions;
using PSInfisicalAPI.Logging;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class LoggingTests
{
[Fact]
public void Format_Uses_Utc_Timestamp_And_Component()
{
DateTimeOffset utc = new DateTimeOffset(2026, 6, 2, 21, 44, 22, TimeSpan.Zero).AddTicks(1830000);
string result = InfisicalLogFormatter.Format(utc, InfisicalLogLevel.Information, "SecretsClient", "Attempting to retrieve Infisical secrets. Please Wait...");
Assert.Equal("[2026-06-02T21:44:22.1830000Z] - [Information] - [SecretsClient] - Attempting to retrieve Infisical secrets. Please Wait...", result);
}
[Fact]
public void Format_Includes_All_Levels()
{
DateTimeOffset utc = DateTimeOffset.UtcNow;
foreach (InfisicalLogLevel level in Enum.GetValues(typeof(InfisicalLogLevel)))
{
string result = InfisicalLogFormatter.Format(utc, level, "Component", "Message");
Assert.Matches(@"^\[[0-9TZ:\.\-]+\] - \[" + level + @"\] - \[Component\] - Message$", result);
}
}
[Fact]
public void NullLogger_Accepts_Any_Calls()
{
IInfisicalLogger logger = NullInfisicalLogger.Instance;
logger.Information("c", "m");
logger.Verbose("c", "m");
logger.Debug("c", "m");
logger.Warning("c", "m");
logger.Error("c", "m");
}
}
}
+59
View File
@@ -0,0 +1,59 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class ManifestTests
{
private static DirectoryInfo RepositoryRoot()
{
DirectoryInfo current = new DirectoryInfo(System.AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "PSInfisicalAPI.sln")))
{
return current;
}
current = current.Parent;
}
return null;
}
[Fact]
public void Assembly_Has_CommitHash_Metadata()
{
AssemblyMetadataAttribute[] attributes = (AssemblyMetadataAttribute[])typeof(PSInfisicalAPI.Connections.InfisicalConnection)
.Assembly
.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false);
bool found = false;
foreach (AssemblyMetadataAttribute attribute in attributes)
{
if (attribute.Key == "CommitHash")
{
found = true;
Assert.False(string.IsNullOrEmpty(attribute.Value));
}
}
Assert.True(found, "Assembly must contain a CommitHash metadata attribute.");
}
[Fact]
public void Psm1_Imports_Binary_From_Bin_Folder()
{
DirectoryInfo root = RepositoryRoot();
Assert.NotNull(root);
string psm1Path = Path.Combine(root.FullName, "Module", "PSInfisicalAPI", "PSInfisicalAPI.psm1");
Assert.True(File.Exists(psm1Path));
string content = File.ReadAllText(psm1Path);
Assert.Contains("Import-Module", content);
Assert.Contains("'bin'", content);
Assert.Contains("PSInfisicalAPI.dll", content);
}
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<IsPackable>false</IsPackable>
<AssemblyName>PSInfisicalAPI.Tests</AssemblyName>
<RootNamespace>PSInfisicalAPI.Tests</RootNamespace>
<NoWarn>$(NoWarn);CS1591;NU1701;NU1903</NoWarn>
<GenerateProgramFile>true</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="YamlDotNet" Version="15.1.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PSInfisicalAPI\PSInfisicalAPI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using PSInfisicalAPI.Common;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SanitizerTests
{
[Fact]
public void Sanitize_Body_Redacts_When_Contains_Secrets()
{
string body = "{\"secretValue\":\"abc\"}";
string sanitized = InfisicalSanitizer.SanitizeBody(body, true);
Assert.Equal("[REDACTED]", sanitized);
}
[Fact]
public void Sanitize_Body_Truncates_Long_NonSecret_Body()
{
string body = new string('a', 4096);
string sanitized = InfisicalSanitizer.SanitizeBody(body, false);
Assert.Contains("[truncated]", sanitized);
}
[Fact]
public void Sanitize_Header_Redacts_Authorization()
{
string sanitized = InfisicalSanitizer.SanitizeHeaderValue("Authorization", "Bearer abc.def");
Assert.Equal("[REDACTED]", sanitized);
}
[Fact]
public void Sanitize_Header_Passes_Through_Non_Sensitive()
{
string sanitized = InfisicalSanitizer.SanitizeHeaderValue("Accept", "application/json");
Assert.Equal("application/json", sanitized);
}
[Fact]
public void Sanitize_Headers_Returns_New_Map()
{
Dictionary<string, string> headers = new Dictionary<string, string>
{
{ "Authorization", "Bearer token" },
{ "Accept", "application/json" }
};
IDictionary<string, string> sanitized = InfisicalSanitizer.SanitizeHeaders(headers);
Assert.Equal("[REDACTED]", sanitized["Authorization"]);
Assert.Equal("application/json", sanitized["Accept"]);
}
}
}
@@ -0,0 +1,38 @@
using System.Reflection;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SecretMapperTests
{
[Fact]
public void Mapper_Maps_SecretKey_To_SecretName_And_SecretValue_To_SecureString()
{
System.Type mapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.Secrets.InfisicalSecretMapper", true);
System.Type dtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.Secrets.InfisicalSecretResponseDto", true);
object dto = System.Activator.CreateInstance(dtoType);
dtoType.GetProperty("SecretKey").SetValue(dto, "DatabasePassword");
dtoType.GetProperty("SecretValue").SetValue(dto, "Sup3rSecret!");
dtoType.GetProperty("SecretPath").SetValue(dto, "/prod/db");
dtoType.GetProperty("Environment").SetValue(dto, "prod");
dtoType.GetProperty("Type").SetValue(dto, "shared");
MethodInfo mapMethod = mapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
InfisicalSecret secret = (InfisicalSecret)mapMethod.Invoke(null, new[] { dto });
Assert.Equal("DatabasePassword", secret.SecretName);
Assert.NotNull(secret.SecretValue);
Assert.True(secret.SecretValue.IsReadOnly());
Assert.Equal("/prod/db", secret.SecretPath);
Assert.Equal(InfisicalSecretType.Shared, secret.Type);
string roundtripped = SecureStringUtility.UsePlainText(secret.SecretValue, plain => plain);
Assert.Equal("Sup3rSecret!", roundtripped);
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Security;
using PSInfisicalAPI.Security;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SecureStringUtilityTests
{
[Fact]
public void ToReadOnlySecureString_Returns_ReadOnly_Instance()
{
SecureString secure = SecureStringUtility.ToReadOnlySecureString("hello");
Assert.True(secure.IsReadOnly());
Assert.Equal(5, secure.Length);
}
[Fact]
public void ToReadOnlySecureString_Handles_Null_And_Empty()
{
SecureString fromNull = SecureStringUtility.ToReadOnlySecureString(null);
SecureString fromEmpty = SecureStringUtility.ToReadOnlySecureString(string.Empty);
Assert.True(fromNull.IsReadOnly());
Assert.True(fromEmpty.IsReadOnly());
Assert.Equal(0, fromNull.Length);
Assert.Equal(0, fromEmpty.Length);
}
[Fact]
public void UsePlainText_Roundtrips_Value()
{
SecureString secure = SecureStringUtility.ToReadOnlySecureString("RoundTripValue");
string captured = SecureStringUtility.UsePlainText(secure, plainText => plainText);
Assert.Equal("RoundTripValue", captured);
}
[Fact]
public void UsePlainText_Throws_For_Null_Action()
{
SecureString secure = SecureStringUtility.ToReadOnlySecureString("x");
Assert.Throws<ArgumentNullException>(() => SecureStringUtility.UsePlainText<string>(secure, null));
}
}
}
@@ -0,0 +1,42 @@
using System.IO;
using System.Text.RegularExpressions;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SourcePolicyTests
{
private static DirectoryInfo RepositoryRoot()
{
DirectoryInfo current = new DirectoryInfo(System.AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "PSInfisicalAPI.sln")))
{
return current;
}
current = current.Parent;
}
return null;
}
[Fact]
public void No_Async_Or_Await_Keywords_In_Production_Source()
{
DirectoryInfo root = RepositoryRoot();
Assert.NotNull(root);
DirectoryInfo src = new DirectoryInfo(Path.Combine(root.FullName, "src", "PSInfisicalAPI"));
Assert.True(src.Exists);
Regex asyncRegex = new Regex(@"\basync\b");
Regex awaitRegex = new Regex(@"\bawait\b");
foreach (FileInfo file in src.EnumerateFiles("*.cs", SearchOption.AllDirectories))
{
string content = File.ReadAllText(file.FullName);
Assert.DoesNotMatch(asyncRegex, content);
Assert.DoesNotMatch(awaitRegex, content);
}
}
}
}
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class UriBuilderTests
{
[Fact]
public void Build_Combines_Base_And_Endpoint()
{
Uri baseUri = new Uri("https://app.infisical.com");
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
Uri uri = InfisicalUriBuilder.Build(baseUri, definition, null, new[]
{
new KeyValuePair<string, string>("environment", "prod"),
new KeyValuePair<string, string>("secretPath", "/")
});
Assert.Equal("https://app.infisical.com/api/v4/secrets?environment=prod&secretPath=%2F", uri.ToString());
}
[Fact]
public void Build_Escapes_SecretName_Path_Segment()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", "Sql Password" } };
string resolved = InfisicalUriBuilder.ResolvePathTemplate(definition.Template, pathParameters);
Assert.Equal("/api/v4/secrets/Sql%20Password", resolved);
}
[Fact]
public void ResolvePathTemplate_Escapes_Slash_And_Colon()
{
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", "Path/With:Colon" } };
string resolved = InfisicalUriBuilder.ResolvePathTemplate("/api/v4/secrets/{secretName}", pathParameters);
Assert.Equal("/api/v4/secrets/Path%2FWith%3AColon", resolved);
}
[Fact]
public void Build_Escapes_Query_Parameters()
{
string queryString = InfisicalUriBuilder.BuildQueryString(new[]
{
new KeyValuePair<string, string>("tagSlugs", "tag a"),
new KeyValuePair<string, string>("tagSlugs", "tag/b")
});
Assert.Equal("tagSlugs=tag%20a&tagSlugs=tag%2Fb", queryString);
}
[Fact]
public void Build_Throws_When_Required_Token_Missing()
{
Uri baseUri = new Uri("https://app.infisical.com");
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
Assert.Throws<InfisicalConfigurationException>(() => InfisicalUriBuilder.Build(baseUri, definition, null, null));
}
}
}