//go:build browser package contratest import ( "strings" "testing" "github.com/carlosframework/rastrillo/harness" "github.com/chromedp/chromedp" ) // Two accounts sharing a vault, in a real engine. // // The Go and node suites each prove a half. This is the only test where one // account's browser seals a vault keyset and ANOTHER account's browser opens // it, through the real API, with the real ECDH, and reads a real item out of // it. It is the walk a person actually does. // // One browser profile, two accounts. Only the SESSION changes between them — // localStorage is left alone, because the Account Key cache is keyed by // account id (`contra/v1/account-key/`) and the two coexist. That is not // a shortcut around the test; it is what a shared family machine looks like. func signOut(t *testing.T, rig *harness.Rig) { t.Helper() rig.Run( chromedp.Navigate(rig.Origin+"/vault"), chromedp.Click(`form[action="/auth/signout"] button`, chromedp.ByQuery), chromedp.WaitVisible(`body`, chromedp.ByQuery), ) } func signIn(t *testing.T, rig *harness.Rig, email, passphrase string) { t.Helper() rig.Run( chromedp.Navigate(rig.Origin+"/signin"), chromedp.SendKeys(`input[name=email]`, email, chromedp.ByQuery), chromedp.SendKeys(`input[name=passphrase]`, passphrase, chromedp.ByQuery), chromedp.Click(`button[type=submit]`, chromedp.ByQuery), chromedp.WaitVisible(`form[action="/auth/signout"]`, chromedp.ByQuery), ) } func unlockActions(rig *harness.Rig, passphrase string) []chromedp.Action { return []chromedp.Action{ chromedp.Navigate(rig.Origin + "/vault"), chromedp.WaitVisible(`[data-unlock]`, chromedp.ByQuery), chromedp.SendKeys(`[data-unlock] input[name=passphrase]`, passphrase, chromedp.ByQuery), chromedp.Click(`[data-unlock] button[type=submit]`, chromedp.ByQuery), chromedp.WaitVisible(`[data-item-form]`, chromedp.ByQuery), } } func TestBrowserTwoAccountsShareAVault(t *testing.T) { rig, box := newRig(t) const ownerEmail = "share-owner@example.invalid" const ownerPass = "a generated test passphrase 8f3a1c" const guestEmail = "share-guest@example.invalid" const guestPass = "a different generated one 4d2e9b" // The guest signs up first, so their account id exists to be shared with. signUpInBrowser(t, rig, box, guestEmail, guestPass) rig.Run(unlockActions(rig, guestPass)...) var guestID string rig.Run(chromedp.Text(`[data-my-account]`, &guestID, chromedp.ByQuery)) guestID = strings.TrimSpace(guestID) if len(guestID) != 32 { t.Fatalf("the guest's account id is not 32 hex characters: %q", guestID) } rig.Screen("[data-sharing]", "the sharing panel, before anything is shared") signOut(t, rig) // The owner signs up, adds an item, and shares with the id the guest read // off their own screen — which is the only handle there is, because there // is no directory to look anybody up in. signUpInBrowser(t, rig, box, ownerEmail, ownerPass) rig.Run(unlockActions(rig, ownerPass)...) rig.Run( chromedp.SendKeys(`[data-item-form] input[name=title]`, "Jamaica Bakery", chromedp.ByQuery), chromedp.SendKeys(`[data-item-form] input[name=password]`, "generated-not-real-8f3a", chromedp.ByQuery), chromedp.Click(`[data-item-form] [data-save]`, chromedp.ByQuery), chromedp.WaitVisible(`[data-items] li`, chromedp.ByQuery), chromedp.SendKeys(`[data-share-form] input[name=account_id]`, guestID, chromedp.ByQuery), chromedp.Click(`[data-share-form] button[type=submit]`, chromedp.ByQuery), chromedp.Poll( `document.querySelector('[data-members]').textContent.includes('`+guestID+`')`, nil), ) rig.Screen("[data-sharing]", "shared with one member") signOut(t, rig) // The guest opens the owner's item. This is the assertion the whole // feature exists for: sealed by one account's browser under one set of // keys, opened by another under theirs. signIn(t, rig, guestEmail, guestPass) rig.Run(unlockActions(rig, guestPass)...) // The guest has two vaults now — their own, and the one shared with them // — so the picker has to be used. Their own sorts first. rig.Run( chromedp.WaitVisible(`[data-vault-picker]`, chromedp.ByQuery), chromedp.SetValue(`[data-vault-picker]`, "1", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('[data-vault-picker]')`+ `.dispatchEvent(new Event('change'))`, nil), chromedp.Poll( `document.querySelector('[data-items]').textContent.includes('Jamaica')`, nil), ) var listed string rig.Run(chromedp.Text(`[data-items]`, &listed, chromedp.ByQuery)) if !strings.Contains(listed, "Jamaica Bakery") { t.Fatalf("the member cannot see the shared item: %q", listed) } rig.Screen("[data-vault]", "a vault opened by the account it was shared with") // And a member is told they cannot change who else can see it, rather // than being shown a control that fails. var panel string rig.Run(chromedp.Text(`[data-sharing]`, &panel, chromedp.ByQuery)) if !strings.Contains(panel, "shared with you by somebody else") { t.Errorf("a member is not told whose vault this is: %q", panel) } // The owner removes them, which rotates the vault, and the member loses // it. Nothing already downloaded is un-downloaded — which is exactly why // the screen says so before the button. signOut(t, rig) signIn(t, rig, ownerEmail, ownerPass) rig.Run(unlockActions(rig, ownerPass)...) rig.Run( chromedp.Click(`[data-understood]`, chromedp.ByQuery), chromedp.Click(`[data-members] [data-remove]`, chromedp.ByQuery), chromedp.Poll( `document.querySelector('[data-share-status]').textContent.includes('new key')`, nil), ) rig.Screen("[data-sharing]", "after removing a member") signOut(t, rig) signIn(t, rig, guestEmail, guestPass) rig.Run( chromedp.Navigate(rig.Origin+"/vault"), chromedp.WaitVisible(`[data-unlock]`, chromedp.ByQuery), chromedp.SendKeys(`[data-unlock] input[name=passphrase]`, guestPass, chromedp.ByQuery), chromedp.Click(`[data-unlock] button[type=submit]`, chromedp.ByQuery), chromedp.WaitVisible(`[data-item-form]`, chromedp.ByQuery), ) rig.Run(chromedp.Text(`[data-items]`, &listed, chromedp.ByQuery)) if strings.Contains(listed, "Jamaica Bakery") { t.Error("a removed member can still read the vault") } }