Implement PSInfisicalAPI module per design spec with env-var auto-discovery
This commit is contained in:
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user