using DodoSSH.Contracts;
using DodoSSH.Crypto;
namespace DodoSSH.Client.Storage.Tests;
///
/// The cache as it is actually deployed: a file on disk.
///
///
/// Every other suite here uses an in-memory database because it is faster and isolated. That leaves the
/// production path — , a real migration against a file that does
/// not exist yet, and data surviving the process that wrote it — untested, which is exactly the shape of
/// bug that only appears on a user's first launch.
///
public sealed class FileBackedCacheTests : IDisposable
{
private readonly string directory =
Path.Combine(Path.GetTempPath(), $"dodossh-cache-{Guid.CreateVersion7():N}");
///
public void Dispose()
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public async Task AMigrationCreatesTheFileAndTheDataOutlivesTheFactory()
{
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, "cache.db");
var material = Material();
using (var first = ClientCacheFactory.ForFile(path))
{
await first.MigrateAsync(Token);
File.Exists(path).ShouldBeTrue("the migration should have created the database");
await new UnlockStore(first, TimeProvider.System).SaveAsync(material, Token);
}
// A second factory over the same file, as a later launch of the application is.
using var second = ClientCacheFactory.ForFile(path);
// Migrating again is what every launch does, and it has to be a no-op rather than an error.
await second.MigrateAsync(Token);
var read = await new UnlockStore(second, TimeProvider.System).ReadAsync(Token);
read.ShouldNotBeNull();
read.UserId.ShouldBe(material.UserId);
read.WrappedPrivateKey.ShouldBe(material.WrappedPrivateKey);
read.KdfParameters.Salt.ShouldBe(material.KdfParameters.Salt);
}
[Fact]
public async Task AMissingDirectory_FailsClearlyRatherThanSilently()
{
// The application creates the profile directory before opening the cache. If that order were ever
// reversed, this is the error it would produce — worth pinning so the failure stays diagnosable
// instead of turning into an empty vault.
using var factory = ClientCacheFactory.ForFile(
Path.Combine(directory, "missing", "cache.db"));
await Should.ThrowAsync(
async () => await factory.MigrateAsync(Token));
}
private static CancellationToken Token => TestContext.Current.CancellationToken;
private static StoredUnlockMaterial Material() =>
new(
"https://dodossh.example",
Guid.CreateVersion7(),
"https://idp.example",
"alice",
"alice@example.com",
"Alice",
KeyGeneration: 1,
WrappedPrivateKey: [1, 2, 3, 4],
new KdfParameters("argon2id", [5, 6, 7, 8], 262144, 4, 1),
DateTimeOffset.FromUnixTimeSeconds(1_750_000_000));
}