using DodoSSH.Contracts;
namespace DodoSSH.Client.Storage.Tests;
///
/// The sign-in a machine may resume, and what emptying the cache does to it.
///
///
/// Two behaviours meet here for a reason: the refresh token is the one thing in this cache that is a
/// credential for the account rather than for the vault, so both halves of its life — sealed
/// while it is kept, gone when the user signs out — belong under one test class.
///
public sealed class RememberedSignInTests
{
private static CancellationToken Token => TestContext.Current.CancellationToken;
[Fact]
public async Task ARememberedTokenRoundTrips()
{
using var harness = await CacheHarness.CreateAsync();
var store = Store(harness);
(await store.ReadAsync(Token)).ShouldBeNull("nothing has been remembered yet");
await store.SaveAsync("refresh-token-1", Token);
(await store.ReadAsync(Token)).ShouldBe("refresh-token-1");
}
[Fact]
public async Task RememberingAgain_ReplacesRatherThanAdds()
{
// What a rotating provider does on every refresh. A second row would be a constraint violation;
// keeping the first would leave the next launch presenting a token the provider has retired.
using var harness = await CacheHarness.CreateAsync();
var store = Store(harness);
await store.SaveAsync("refresh-token-1", Token);
await store.SaveAsync("refresh-token-2", Token);
(await store.ReadAsync(Token)).ShouldBe("refresh-token-2");
}
[Fact]
public async Task AnotherUsersCacheKey_DoesNotOpenIt()
{
// The whole reason this is sealed rather than stored. A cache file lifted off a machine cannot be
// made to yield an account credential without the key that only an unlocked vault holds.
using var owner = await CacheHarness.CreateAsync();
using var stranger = await CacheHarness.CreateAsync();
await Store(owner).SaveAsync("refresh-token-1", Token);
var strangersView = new RememberedSignInStore(
owner.Factory, stranger.Protector, CacheHarness.UserId, TimeProvider.System);
(await strangersView.ReadAsync(Token)).ShouldBeNull();
}
[Fact]
public async Task ForgettingIt_LeavesNothingToResume()
{
using var harness = await CacheHarness.CreateAsync();
var store = Store(harness);
await store.SaveAsync("refresh-token-1", Token);
await store.ForgetAsync(Token);
(await store.ReadAsync(Token)).ShouldBeNull();
// And forgetting what is not there is not an error: it runs on a sign-out from a machine that
// never remembered one.
await store.ForgetAsync(Token);
}
[Fact]
public async Task ResettingTheCache_EmptiesEveryTableAndKeepsTheSchema()
{
// What signing out does on disk. Every row goes — the profile an unlock reads, the item mirror,
// the outbox, the remembered sign-in — and the database is immediately usable again, because the
// application has to be able to be set up afresh without being restarted.
using var harness = await CacheHarness.CreateAsync();
var entityId = Guid.CreateVersion7();
await harness.Unlock.SaveAsync(Material(), Token);
await harness.Items.SaveAsync(CacheHarness.Item(entityId), Token);
await harness.Outbox.QueueAsync(CacheHarness.Change(entityId), Token);
await Store(harness).SaveAsync("refresh-token-1", Token);
await harness.Factory.ResetAsync(Token);
(await harness.Unlock.ReadAsync(Token)).ShouldBeNull("the profile is what makes a machine enrolled");
(await harness.Items
.ListAsync(CacheHarness.VaultId, SyncEntityType.Host, includeDeleted: true, Token))
.ShouldBeEmpty();
(await harness.Outbox.ListAllAsync(CacheHarness.VaultId, Token)).ShouldBeEmpty();
(await Store(harness).ReadAsync(Token)).ShouldBeNull();
// Usable, not merely empty: writing to it again must not need a migration.
await harness.Unlock.SaveAsync(Material(), Token);
(await harness.Unlock.ReadAsync(Token)).ShouldNotBeNull();
}
private static RememberedSignInStore Store(CacheHarness harness) =>
new(harness.Factory, harness.Protector, CacheHarness.UserId, TimeProvider.System);
private static StoredUnlockMaterial Material() =>
new(
"https://dodossh.example",
CacheHarness.UserId,
"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));
}