The LA3 Opus review process note was right: the contract both sides implement lived only in orchestrator prompts, which is exactly the drift mode the pin exists to prevent (and it produced the paths-key CRITICAL). The schema, field rules, probe-mode discriminator, and status vocabulary are now a binding plan section; amendments change this text first, implementations second. Ledger: LA3 fix round dispatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
167 lines
5.5 KiB
C#
167 lines
5.5 KiB
C#
using AcDream.App.Configuration;
|
|
using AcDream.App.Credentials;
|
|
|
|
namespace AcDream.App.Tests.Credentials;
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1: <see cref="AppCredentialResolver"/> is a minimal
|
|
/// port of <c>AcDream.Headless.Credentials.HeadlessCredentialResolver</c>
|
|
/// scoped to the App session-config credential shape — see that file's own
|
|
/// doc for why it is an independent port rather than a shared reference.
|
|
/// Mirrors <c>HeadlessCredentialResolverTests</c>'s coverage.
|
|
/// </summary>
|
|
public sealed class AppCredentialResolverTests
|
|
{
|
|
[Fact]
|
|
public void EnvironmentSecretIsRedactedAndErasable()
|
|
{
|
|
const string variable = "ACDREAM_LA1_TEST_ENV_SECRET";
|
|
const string secretValue = "test-secret-value";
|
|
Environment.SetEnvironmentVariable(variable, secretValue);
|
|
try
|
|
{
|
|
var resolver = new AppCredentialResolver(
|
|
TextReader.Null,
|
|
Environment.CurrentDirectory,
|
|
isLinux: false);
|
|
|
|
AppCredentialSecret secret = resolver.Resolve(
|
|
"session",
|
|
new SessionCredentialDescriptor
|
|
{
|
|
Provider = SessionCredentialProviderKind.Environment,
|
|
Reference = variable,
|
|
});
|
|
|
|
Assert.Equal(secretValue, secret.Reveal());
|
|
Assert.DoesNotContain(secretValue, secret.ToString());
|
|
secret.Dispose();
|
|
Assert.True(secret.IsDisposed);
|
|
Assert.Throws<ObjectDisposedException>(secret.Reveal);
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable(variable, null);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void StandardInputConsumesOneSecretWithoutEchoingIt()
|
|
{
|
|
const string secretValue = "stdin-secret";
|
|
var resolver = new AppCredentialResolver(
|
|
new StringReader(secretValue + Environment.NewLine),
|
|
Environment.CurrentDirectory,
|
|
isLinux: false);
|
|
|
|
using AppCredentialSecret secret = resolver.Resolve(
|
|
"session",
|
|
new SessionCredentialDescriptor
|
|
{
|
|
Provider = SessionCredentialProviderKind.StandardInput,
|
|
Reference = "session-stdin",
|
|
});
|
|
|
|
Assert.Equal(secretValue, secret.Reveal());
|
|
Assert.DoesNotContain(secretValue, secret.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void CredentialFileIsResolvedRelativeToConfiguredDirectory()
|
|
{
|
|
string directory = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"acdream-app-credentials-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(directory);
|
|
string path = Path.Combine(directory, "session.pass");
|
|
File.WriteAllText(path, "file-secret" + Environment.NewLine);
|
|
try
|
|
{
|
|
var resolver = new AppCredentialResolver(
|
|
TextReader.Null,
|
|
directory,
|
|
isLinux: false);
|
|
|
|
using AppCredentialSecret secret = resolver.Resolve(
|
|
"session",
|
|
new SessionCredentialDescriptor
|
|
{
|
|
Provider = SessionCredentialProviderKind.File,
|
|
Reference = "session.pass",
|
|
});
|
|
|
|
Assert.Equal("file-secret", secret.Reveal());
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(path);
|
|
Directory.Delete(directory);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingSecretErrorNeverContainsAnotherSecret()
|
|
{
|
|
const string variable = "ACDREAM_LA1_TEST_OTHER_SECRET";
|
|
const string unrelatedSecret = "must-not-leak";
|
|
Environment.SetEnvironmentVariable(variable, unrelatedSecret);
|
|
try
|
|
{
|
|
var resolver = new AppCredentialResolver(
|
|
new StringReader(string.Empty),
|
|
Environment.CurrentDirectory,
|
|
isLinux: false);
|
|
|
|
AppCredentialException error =
|
|
Assert.Throws<AppCredentialException>(() =>
|
|
resolver.Resolve(
|
|
"session",
|
|
new SessionCredentialDescriptor
|
|
{
|
|
Provider = SessionCredentialProviderKind.Environment,
|
|
Reference = "ACDREAM_LA1_TEST_DOES_NOT_EXIST",
|
|
}));
|
|
|
|
Assert.DoesNotContain(unrelatedSecret, error.ToString());
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable(variable, null);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LinuxRejectsGroupOrOtherCredentialPermissions()
|
|
{
|
|
if (!OperatingSystem.IsLinux())
|
|
return;
|
|
|
|
string path = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"acdream-app-credential-{Guid.NewGuid():N}");
|
|
File.WriteAllText(path, "linux-secret");
|
|
File.SetUnixFileMode(
|
|
path,
|
|
UnixFileMode.UserRead | UnixFileMode.GroupRead);
|
|
try
|
|
{
|
|
var resolver = new AppCredentialResolver(
|
|
TextReader.Null,
|
|
Path.GetDirectoryName(path)!,
|
|
isLinux: true);
|
|
|
|
Assert.Throws<AppCredentialException>(() =>
|
|
resolver.Resolve(
|
|
"session",
|
|
new SessionCredentialDescriptor
|
|
{
|
|
Provider = SessionCredentialProviderKind.File,
|
|
Reference = Path.GetFileName(path),
|
|
}));
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
}
|