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)); }