diff --git a/tests/Bifrost.Web.Tests/MergeAccountsPageTests.cs b/tests/Bifrost.Web.Tests/MergeAccountsPageTests.cs index 14d6e4f..8210b8b 100644 --- a/tests/Bifrost.Web.Tests/MergeAccountsPageTests.cs +++ b/tests/Bifrost.Web.Tests/MergeAccountsPageTests.cs @@ -46,11 +46,11 @@ public sealed class MergeAccountsPageTests public MergeProofs Proofs => Host.Services.GetRequiredService(); - public static async Task Make(bool enabled = true) + public static async Task Make(bool enabled = true, string[]? otherProviders = null) { var rig = new Rig(realSessions: true, accountMerge: enabled); var me = await rig.AccountAsync("quill-9f3a", "discord"); - var other = await rig.AccountAsync(OtherName, "patreon", "google"); + var other = await rig.AccountAsync(OtherName, otherProviders ?? ["patreon", "google"]); var session = await rig.Host.SignInSessionAsync(me.HeimdallId, "discord"); var myName = await rig.InScopeAsync(async sp => (await sp.GetRequiredService().UserAccounts.AsNoTracking().SingleAsync(x => x.Id == me.Id)).DisplayName); var world = new World { Rig = rig, Me = me, Other = other, Session = session, MyName = myName }; @@ -349,6 +349,46 @@ public sealed class MergeAccountsPageTests Assert.DoesNotContain("consentSelf", page); } + [Theory] + [InlineData("self")] + [InlineData("other")] + public async Task Two_accounts_holding_a_login_of_one_provider_are_blocked_before_consent_and_the_block_names_only_the_provider(string keep) + { + using var w = await World.Make(otherProviders: ["discord", "patreon"]); + await w.ProveSelf(); + await w.ProveOther("discord"); + await w.ProveOther("patreon"); + + var page = await w.Page($"?keep={keep}"); + + Assert.Contains("Both accounts hold a Discord login", page); + Assert.DoesNotContain("consentSelf", page); + Assert.DoesNotContain("name=\"digest\"", page); + Assert.DoesNotContain("ProveRemaining", page); + var words = Regex.Match(page, "Both accounts hold a[^<]*").Value; + Assert.DoesNotContain(OtherName, words); + Assert.DoesNotContain(w.MyName, words); + Assert.DoesNotContain(w.Other.HeimdallId, words); + } + + [Fact] + public async Task A_confirm_for_accounts_holding_one_providers_login_is_Blocked_writes_nothing_and_keeps_the_proofs() + { + using var w = await World.Make(otherProviders: ["discord", "patreon"]); + await w.ProveSelf(); + await w.ProveOther("discord"); + await w.ProveOther("patreon"); + var digest = await w.Rig.DigestAsync(w.Me, w.Other); + + var response = await w.Confirm("self", digest); + + Assert.Equal(Where("self", "blocked"), MergePage.Location(response)); + Assert.Equal(0, await w.Rig.MergeCountAsync()); + Assert.NotNull(w.Proofs.Snapshot(w.Me.Id).Self); + Assert.NotNull(w.Proofs.Snapshot(w.Me.Id).Other); + Assert.Empty(w.Rig.Moves.Requests); + } + // ---- confirm ---- [Fact] diff --git a/tests/Bifrost.Web.Tests/MergePreviewTests.cs b/tests/Bifrost.Web.Tests/MergePreviewTests.cs index ae410c9..47e6b54 100644 --- a/tests/Bifrost.Web.Tests/MergePreviewTests.cs +++ b/tests/Bifrost.Web.Tests/MergePreviewTests.cs @@ -73,6 +73,12 @@ public sealed class MergePreviewTests Assert.Contains(MergePreview.Statements, x => x.Contains(statement, StringComparison.Ordinal)); } + [Fact] + public void The_statements_promise_no_passkey_because_no_page_offers_one() + { + Assert.DoesNotContain(MergePreview.Statements, x => x.Contains("passkey", StringComparison.OrdinalIgnoreCase)); + } + [Theory] [InlineData("blocked")] [InlineData("preview_stale")] diff --git a/tests/Bifrost.Web.Tests/MergeProofCompletionTests.cs b/tests/Bifrost.Web.Tests/MergeProofCompletionTests.cs index c2e364b..704b037 100644 --- a/tests/Bifrost.Web.Tests/MergeProofCompletionTests.cs +++ b/tests/Bifrost.Web.Tests/MergeProofCompletionTests.cs @@ -61,6 +61,26 @@ public sealed class MergeProofCompletionTests } } + [Theory] + [InlineData(MergeSide.Self)] + [InlineData(MergeSide.Other)] + public async Task A_completion_through_another_provider_than_the_attempts_records_no_proof(MergeSide side) + { + using var host = new HeimdallHost(realSessions: true); + var me = await SignedIn(host); + var other = await SignedIn(host); + SetList(host, "discord", "patreon"); + var proved = side == MergeSide.Self ? me : other; + var attempt = await Begin(host, me, side, via: "discord"); + + var done = await Finish(host, me, attempt, proved.Account, host.Token(proved.Account), provider: "patreon"); + + Assert.NotEqual(Page + "?result=merge_proof_recorded", Location(done)); + var held = Proofs(host).Snapshot(me.UserAccountId); + Assert.Null(held.Self); + Assert.Null(held.Other); + } + [Fact] public async Task A_self_proof_for_the_signed_in_account_is_held_and_the_browser_goes_back_with_a_closed_code() { diff --git a/tests/Bifrost.Web.Tests/MergeProofsTests.cs b/tests/Bifrost.Web.Tests/MergeProofsTests.cs index 5004bbe..f02d685 100644 --- a/tests/Bifrost.Web.Tests/MergeProofsTests.cs +++ b/tests/Bifrost.Web.Tests/MergeProofsTests.cs @@ -196,4 +196,21 @@ public sealed class MergeProofsTests Assert.DoesNotContain(_selfAccount, printed); Assert.DoesNotContain(_otherAccount, printed); } + + /// Actors with anything in memory: pruning drops a side with no proof and an actor with no side. + private int HeldProofs() => + ((System.Collections.IDictionary)typeof(MergeProofs).GetField("_held", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(_proofs)!) + .Count; + + [Fact] + public void An_expired_bearer_is_dropped_by_any_later_access_even_one_for_another_actor() + { + PutSelf("discord", Login("discord")); + Assert.Equal(1, HeldProofs()); + _clock.Advance(MergeProofs.ProofLifetime + TimeSpan.FromSeconds(1)); + + _proofs.Clear(Guid.NewGuid()); + + Assert.Equal(0, HeldProofs()); + } } diff --git a/tests/Bifrost.Web.Tests/MergeStatusTests.cs b/tests/Bifrost.Web.Tests/MergeStatusTests.cs index 4fca2b3..780c151 100644 --- a/tests/Bifrost.Web.Tests/MergeStatusTests.cs +++ b/tests/Bifrost.Web.Tests/MergeStatusTests.cs @@ -85,6 +85,42 @@ public sealed class MergeStatusTests Assert.Equal(1, Notices(await Html(w, w.SurvivorSession, "/Membership/Status"))); } + [Fact] + public async Task Past_the_grace_and_before_the_sweeper_there_is_no_notice_and_no_undo_offer() + { + var w = await Committed(); + using var rig = w.Rig; + var inside = await Html(w, w.SurvivorSession); + Assert.Equal(1, Notices(inside)); + Assert.Contains("To undo, sign in again", inside); + + rig.Clock.Advance(AccountMergeLedger.Grace + TimeSpan.FromHours(1)); + Assert.Equal(AccountMergeState.Committed, (await rig.MergeAsync(w.MergeId)).State); + var late = await Html(w, w.SurvivorSession); + + Assert.Equal(0, Notices(late)); + Assert.DoesNotContain("To undo, sign in again", late); + Assert.DoesNotContain("Undo this merge", late); + Assert.DoesNotContain("ProveForUndo", late); + Assert.DoesNotContain("can still be undone", late); + } + + [Fact] + public async Task Once_the_undo_is_requested_no_page_says_the_merge_can_still_be_undone() + { + var w = await Committed(); + using var rig = w.Rig; + await Edit(w, x => x.State = AccountMergeState.UndoRequested); + + var page = await Html(w, w.SurvivorSession); + + Assert.Equal(1, Notices(page)); + Assert.DoesNotContain("can still be undone", page); + Assert.DoesNotContain("can still undo", page); + Assert.Contains("Undo requested", page); + Assert.DoesNotContain("To undo, sign in again", page); + } + [Fact] public async Task A_strong_proof_merge_says_so_in_its_own_words() {