using AcDream.Headless.Configuration; using AcDream.Headless.Credentials; namespace AcDream.Headless.Tests; public sealed class HeadlessCredentialResolverTests { [Fact] public void EnvironmentSecretIsRedactedAndErasable() { const string secretValue = "test-secret-value"; var environment = new FixtureCredentialEnvironment( isLinux: false, new Dictionary { ["BOT_PASSWORD"] = secretValue, }); var resolver = new HeadlessCredentialResolver( TextReader.Null, Environment.CurrentDirectory, environment); HeadlessCredentialSecret secret = resolver.Resolve( "bot", new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.Environment, Reference = "BOT_PASSWORD", }); Assert.Equal(secretValue, secret.Reveal()); Assert.DoesNotContain(secretValue, secret.ToString()); secret.Dispose(); Assert.True(secret.IsDisposed); Assert.Throws(secret.Reveal); } [Fact] public void StandardInputConsumesOneSecretWithoutEchoingIt() { const string secretValue = "stdin-secret"; var resolver = new HeadlessCredentialResolver( new StringReader(secretValue + Environment.NewLine), Environment.CurrentDirectory, new FixtureCredentialEnvironment( false, new Dictionary())); using HeadlessCredentialSecret secret = resolver.Resolve( "bot", new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.StandardInput, Reference = "bot-stdin", }); Assert.Equal(secretValue, secret.Reveal()); Assert.DoesNotContain(secretValue, secret.ToString()); } [Fact] public void CredentialFileIsResolvedRelativeToConfiguredDirectory() { string directory = Path.Combine( Path.GetTempPath(), $"acdream-credentials-{Guid.NewGuid():N}"); Directory.CreateDirectory(directory); string path = Path.Combine(directory, "bot.pass"); File.WriteAllText(path, "file-secret" + Environment.NewLine); try { var resolver = new HeadlessCredentialResolver( TextReader.Null, directory, new FixtureCredentialEnvironment( false, new Dictionary())); using HeadlessCredentialSecret secret = resolver.Resolve( "bot", new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.File, Reference = "bot.pass", }); Assert.Equal("file-secret", secret.Reveal()); } finally { File.Delete(path); Directory.Delete(directory); } } [Fact] public void MissingSecretErrorNeverContainsAnotherSecret() { const string unrelatedSecret = "must-not-leak"; var resolver = new HeadlessCredentialResolver( new StringReader(string.Empty), Environment.CurrentDirectory, new FixtureCredentialEnvironment( false, new Dictionary { ["OTHER"] = unrelatedSecret, })); HeadlessCredentialException error = Assert.Throws(() => resolver.Resolve( "bot", new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.Environment, Reference = "MISSING", })); Assert.DoesNotContain(unrelatedSecret, error.ToString()); } [Fact] public void LinuxRejectsGroupOrOtherCredentialPermissions() { if (!OperatingSystem.IsLinux()) return; string path = Path.Combine( Path.GetTempPath(), $"acdream-credential-{Guid.NewGuid():N}"); File.WriteAllText(path, "linux-secret"); File.SetUnixFileMode( path, UnixFileMode.UserRead | UnixFileMode.GroupRead); try { var resolver = new HeadlessCredentialResolver( TextReader.Null, Path.GetDirectoryName(path)!, new FixtureCredentialEnvironment( true, new Dictionary())); Assert.Throws(() => resolver.Resolve( "bot", new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.File, Reference = Path.GetFileName(path), })); } finally { File.Delete(path); } } private sealed class FixtureCredentialEnvironment( bool isLinux, IReadOnlyDictionary values) : IHeadlessCredentialEnvironment { public bool IsLinux { get; } = isLinux; public string? GetEnvironmentVariable(string name) => values.TryGetValue(name, out string? value) ? value : null; } }