package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // The setting accounts (novox/hq ADR 0316 §10–§16, ADR 0317 §9 for betterbird_accounts): declared without // credentials, written under the module's own keys, the account list written into and never owned, refused // rather than duplicated, and said by count. const personal = `{"accounts": { "personal": {"address": "me@mail.example.org", "from-name": "Me", "incoming": {"kind": "imap", "host": "imap.example.org", "security": "tls", "sign-in": "password", "user": "me"}, "outgoing": {"host": "smtp.example.org", "port": 587, "security": "starttls", "sign-in": "password", "user": "me"}} }}` // profileWithAccounts is a profile holding an account of the person's at another address, Owl's work account, // local folders, and an account list in that order. func (f *fake) profileWithAccounts() { f.betterbird() f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+ `user_pref("mail.account.account1.server", "server1");`+"\n"+ `user_pref("mail.account.account1.identities", "id1");`+"\n"+ `user_pref("mail.account.account2.server", "server2");`+"\n"+ `user_pref("mail.account.account3.server", "server3");`+"\n"+ `user_pref("mail.account.account3.identities", "id3");`+"\n"+ `user_pref("mail.accountmanager.accounts", "account1,account3,account2");`+"\n"+ `user_pref("mail.identity.id1.useremail", "other@mail.example.org");`+"\n"+ `user_pref("mail.identity.id1.fullName", "Other");`+"\n"+ `user_pref("mail.identity.id1.smtpServer", "smtp1");`+"\n"+ `user_pref("mail.identity.id3.useremail", "work@corp.example.com");`+"\n"+ `user_pref("mail.server.server1.type", "imap");`+"\n"+ `user_pref("mail.server.server1.hostname", "imap.example.org");`+"\n"+ `user_pref("mail.server.server1.socketType", 3);`+"\n"+ `user_pref("mail.server.server1.authMethod", 10);`+"\n"+ `user_pref("mail.server.server1.userName", "other");`+"\n"+ `user_pref("mail.server.server1.directory", "/home/operator/.thunderbird/x/ImapMail/imap.example.org");`+"\n"+ `user_pref("mail.server.server2.type", "none");`+"\n"+ `user_pref("mail.server.server3.type", "owl-ews");`+"\n"+ `user_pref("mail.smtpserver.smtp1.hostname", "smtp.example.org");`+"\n"+ `user_pref("mail.smtpserver.smtp1.try_ssl", 3);`+"\n"+ `user_pref("mail.smtpserver.smtp1.authMethod", 10);`+"\n"+ `user_pref("mail.smtpserver.smtp1.username", "other");`+"\n"+ `user_pref("mail.smtpservers", "smtp1");`+"\n"+ `user_pref("mailnews.tags.version", 2);`+"\n") } func (f *fake) declareAccounts(body string) { f.write(testHome+"/"+accountsFile, body) } func (f *fake) read(path string) string { f.t.Helper() raw, err := os.ReadFile(filepath.Join(f.Root, path)) if err != nil { f.t.Fatal(err) } return string(raw) } func TestAnAccountsValueCarryingAPasswordLikeFieldIsRefusedWithoutItsValue(t *testing.T) { for _, body := range []string{ `{"accounts": {"personal": {"address": "me@mail.example.org", "password": "hunter2"}}}`, `{"accounts": {"personal": {"incoming": {"oauth_refresh": "1//hunter2"}}}}`, `{"token": "hunter2", "accounts": {}}`, } { _, err := parseAccounts([]byte(body)) if err == nil || !strings.Contains(err.Error(), "shaped like a credential") || strings.Contains(err.Error(), "hunter2") { t.Errorf("%s: %v", body, err) } } // An unknown field is refused too, and a value that cannot be declared, said by the account's place. if _, err := parseAccounts([]byte(`{"accounts": {"personal": {"address": "me@mail.example.org", "keychain": 1}}}`)); err == nil { t.Error("an unknown field was taken") } _, err := parseAccounts([]byte(strings.Replace(personal, `"security": "tls"`, `"security": "maybe"`, 1))) if err == nil || !strings.Contains(err.Error(), "declared account 1 of 1") || strings.Contains(err.Error(), "personal") || strings.Contains(err.Error(), "example.org") { t.Errorf("a bad value is said by the account's place, never its name or a value: %v", err) } } func TestTheSettingPrefsRefusesAnAccountsPreferences(t *testing.T) { for _, name := range []string{"mail.account.account1.server", "mail.identity.id1.useremail", "mail.server.server1.hostname", "mail.smtpserver.smtp1.hostname", "mail.accountmanager.accounts", "mail.smtpservers"} { err := validUserJS(mark + ")\nuser_pref(\"" + name + "\", \"x\");\n") if err == nil || !strings.Contains(err.Error(), "setting accounts") { t.Errorf("%s was taken in prefs: %v", name, err) } } if err := validUserJS(mark + ")\nuser_pref(\"calendar.alarms.show\", true);\n"); err != nil { t.Fatal(err) } } // noPackage answers pacman as a machine without the community package. func (f *fake) noPackage() { f.answer = func(name string, args []string) Output { if name == "pacman" { return Output{Code: 1} } return Output{} } } // userPrefLines are the user_pref lines of a user.js, by preference name. func userPrefLines(t *testing.T, text string) map[string]string { t.Helper() out := map[string]string{} for _, line := range strings.Split(text, "\n") { if !strings.HasPrefix(line, "user_pref(") { continue } if !statement.MatchString(line) { t.Errorf("not a statement Betterbird reads as one: %s", line) } out[line[len(`user_pref("`):strings.Index(line, `",`)]] = line } return out } func TestADeclaredAccountTheProfileDoesNotHoldIsCreatedUnderTheModulesKeysAndListed(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") f.declareAccounts(personal) // Betterbird is running: the user.js is placed, the list waits for its next start. u, err := f.userJS(true) if err != nil { t.Fatal(err) } if !strings.HasPrefix(u.Lists, "not written") || !strings.Contains(u.Lists, "running") { t.Fatalf("the list is never written while Betterbird runs: %+v", u) } if len(u.Each) != 1 || u.Each[0].How != created || u.Each[0].Keys != (keySet{"account_mesh_personal", "id_mesh_personal", "server_mesh_personal", "smtp_mesh_personal"}) { t.Fatalf("%+v", u.Each) } placed := f.read(profileDir + "/user.js") for name := range userPrefLines(t, placed) { if key := strings.Split(name, ".")[2]; !strings.HasSuffix(key, "mesh_personal") { t.Errorf("a created account's preference outside the module's keys: %s", name) } if strings.Contains(strings.ToLower(name), "password") || strings.Contains(name, "directory") { t.Errorf("the module writes no credential and none of Betterbird's own preferences: %s", name) } } for _, want := range []string{`user_pref("mail.server.server_mesh_personal.port", 993);`, `user_pref("mail.server.server_mesh_personal.socketType", 3);`, `user_pref("mail.smtpserver.smtp_mesh_personal.try_ssl", 2);`, `user_pref("mail.identity.id_mesh_personal.useremail", "me@mail.example.org");`} { if !strings.Contains(placed, want) { t.Errorf("the user.js lacks %s:\n%s", want, placed) } } // Quit: the list gains the module's keys at its end, every other line as it was. before := f.read(profileDir + "/prefs.js") os.RemoveAll(filepath.Join(f.Root, "proc", "4000")) if err := f.Prepare(); err != nil { t.Fatal(err) } after := f.read(profileDir + "/prefs.js") if !strings.Contains(after, `user_pref("mail.accountmanager.accounts", "account1,account3,account2,account_mesh_personal");`) || !strings.Contains(after, `user_pref("mail.smtpservers", "smtp1,smtp_mesh_personal");`) { t.Fatalf("the lists:\n%s", after) } bl, al := strings.Split(before, "\n"), strings.Split(after, "\n") if len(bl) != len(al) { t.Fatalf("lines were added or lost: %d then %d", len(bl), len(al)) } for i := range bl { if bl[i] != al[i] && !strings.Contains(bl[i], "mail.accountmanager.accounts") && !strings.Contains(bl[i], "mail.smtpservers") { t.Errorf("line %d changed: %q to %q", i+1, bl[i], al[i]) } } // The record lost: an account under the module's own keys is still its creation, never adopted. os.Remove(filepath.Join(f.Root, testHome, recordFile)) u, err = f.userJS(true) if err != nil || u.Each[0].How != created { t.Fatalf("without the record, the module's keys still say created: %+v %v", u.Each, err) } // The account leaves the setting: only the module's keys go, the mail stays in the profile. f.declareAccounts(`{"accounts": {}}`) if err := f.Prepare(); err != nil { t.Fatal(err) } if got := f.read(profileDir + "/prefs.js"); got != before { t.Fatalf("removing the module's keys left the file otherwise as it was:\n%s", got) } } // adoptable is a declaration of the profile's account1 (other@mail.example.org): equal but for its outgoing // port, said explicitly, and its incoming user. const adoptable = `{"accounts": { "mail": {"address": "Other@mail.example.org", "incoming": {"kind": "imap", "host": "imap.example.org", "security": "tls", "sign-in": "oauth", "user": "other2"}, "outgoing": {"host": "smtp.example.org", "port": 465, "security": "tls", "sign-in": "oauth", "user": "other"}}}}` func TestADeclaredAccountTheProfileHoldsIsAdoptedUnderItsOwnKeysAndTheListIsLeftAlone(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") f.declareAccounts(adoptable) os.RemoveAll(filepath.Join(f.Root, "proc", "4000")) before := f.read(profileDir + "/prefs.js") u, err := f.userJS(true) if err != nil { t.Fatal(err) } if len(u.Each) != 1 || u.Each[0].How != adopted || u.Each[0].Keys != (keySet{"account1", "id1", "server1", "smtp1"}) { t.Fatalf("adopted under the profile's keys: %+v", u.Each) } // The address's case and the user name differ; the default port declared explicitly is no change. Names // only, no value. if strings.Join(u.Each[0].Changed, ",") != "mail.identity.id1.useremail,mail.server.server1.userName" { t.Fatalf("the changed preferences, by name: %v", u.Each[0].Changed) } if f.read(profileDir+"/prefs.js") != before || u.Lists != "unchanged" { t.Fatalf("an adopted account's keys are in the lists already: nothing written to prefs.js (%s)", u.Lists) } lines := userPrefLines(t, f.read(profileDir+"/user.js")) for name := range lines { key := strings.Split(name, ".")[2] if key != "account1" && key != "id1" && key != "server1" && key != "smtp1" { t.Errorf("an adopted account's preference outside its keys: %s", name) } for _, never := range []string{"directory", ".type", "smtpServer", "identities", ".server"} { if strings.HasSuffix(name, never) { t.Errorf("the module wrote %s, which is Betterbird's or the account's own", name) } } } if !strings.Contains(lines["mail.server.server1.userName"], `"other2"`) { t.Fatalf("the declared value wins at every start: %v", lines) } // The status says it by place and keys, never by address, host or name. s, err := f.Status() if err != nil { t.Fatal(err) } raw, _ := json.Marshal(s.Accounts) if s.Accounts.InList != 1 || s.Accounts.Others != 1 || strings.Contains(string(raw), "example") || strings.Contains(string(raw), `"mail"`) || !strings.Contains(string(raw), `"account1"`) { t.Fatalf("%s", raw) } // Leaving the setting releases it: its lines and its record go, the account and the lists stay. f.declareAccounts(`{"accounts": {}}`) if err := f.Prepare(); err != nil { t.Fatal(err) } if f.read(profileDir+"/prefs.js") != before || strings.Contains(f.read(profileDir+"/user.js"), "server1") || strings.Contains(f.read(testHome+"/"+recordFile), "account1") { t.Fatal("a released account stays the person's, with no line or record of the module's") } } func TestAnAmbiguousOwlsOrOtherKindOfMatchIsRefusedForThatAccountAlone(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") os.RemoveAll(filepath.Join(f.Root, "proc", "4000")) // A second account with account1's address. prefs := f.read(profileDir + "/prefs.js") prefs = strings.Replace(prefs, `"account1,account3,account2"`, `"account1,account3,account2,account4"`, 1) + `user_pref("mail.account.account4.server", "server4");` + "\n" + `user_pref("mail.account.account4.identities", "id4");` + "\n" + `user_pref("mail.identity.id4.useremail", "other@mail.example.org");` + "\n" + `user_pref("mail.identity.id4.smtpServer", "smtp4");` + "\n" + `user_pref("mail.smtpserver.smtp4.hostname", "smtp.example.org");` + "\n" + `user_pref("mail.server.server4.type", "imap");` + "\n" f.write(profileDir+"/prefs.js", prefs) both := strings.Replace(adoptable, `{"accounts": {`, `{"accounts": { "zz": {"address": "new@mail.example.org", "incoming": {"kind": "imap", "host": "imap.example.org", "security": "tls", "sign-in": "password"}, "outgoing": {"host": "smtp.example.org", "security": "tls", "sign-in": "password"}},`, 1) f.declareAccounts(both) err := f.Prepare() if err == nil || !strings.Contains(err.Error(), "declared account 1 of 2: 2 accounts in the profile have its address (account1, account4)") || !strings.Contains(err.Error(), "key") || strings.Contains(err.Error(), "example.org") { t.Fatalf("refused by place, with the matches' keys and the remedy, never the address: %v", err) } u, _ := f.userJS(false) if u.Each[0].How != refused || u.Each[1].How != created { t.Fatalf("the other declared account goes ahead: %+v", u.Each) } if strings.Contains(f.read(profileDir+"/user.js"), "server1") || strings.Contains(f.read(profileDir+"/user.js"), "server4") { t.Fatal("a refused account gets no line") } // The node's layer names the key: that one is adopted. f.declareAccounts(strings.Replace(both, `"address": "Other@mail.example.org",`, `"address": "Other@mail.example.org", "key": "account4",`, 1)) if err := f.Prepare(); err != nil { t.Fatalf("%v", err) } if u, _ := f.userJS(false); u.Each[0].How != adopted || u.Each[0].Keys.Account != "account4" { t.Fatalf("%+v", u.Each) } os.Remove(filepath.Join(f.Root, testHome, recordFile)) for key, want := range map[string]string{"account9": "not in the profile's account list", "account3": "Owl's", "account2": "another address"} { f.declareAccounts(strings.Replace(adoptable, `"address": "Other@mail.example.org",`, `"address": "Other@mail.example.org", "key": "`+key+`",`, 1)) if err := f.Prepare(); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("key %s: %v", key, err) } } // Owl's account at the address, and an account of another kind than declared. f.declareAccounts(`{"accounts": {"work": {"address": "work@corp.example.com", "incoming": {"kind": "imap", "host": "outlook.example.com", "security": "tls", "sign-in": "oauth"}, "outgoing": {"host": "smtp.example.com", "security": "tls", "sign-in": "oauth"}}}}`) if err := f.Prepare(); err == nil || !strings.Contains(err.Error(), "Owl's") { t.Errorf("Owl's account is never adopted: %v", err) } f.write(profileDir+"/prefs.js", strings.Replace(prefs, `"mail.server.server4.type", "imap"`, `"mail.server.server4.type", "pop3"`, 1)) f.declareAccounts(strings.Replace(adoptable, `"address": "Other@mail.example.org",`, `"address": "Other@mail.example.org", "key": "account4",`, 1)) if err := f.Prepare(); err == nil || !strings.Contains(err.Error(), "never converts") { t.Errorf("an account of another kind is refused, not converted: %v", err) } } func TestDeclaredAccountsWithoutASignInAreCountedFromTheServerNamesAlone(t *testing.T) { f := newFake(t) f.profileWithAccounts() links(t, nil) f.declareAccounts(`{"accounts": { "personal": {"address": "me@mail.example.org", "incoming": {"kind": "imap", "host": "imap.example.org", "security": "tls", "sign-in": "password"}, "outgoing": {"host": "smtp.example.org", "security": "tls", "sign-in": "password"}}, "google": {"address": "me@gmail.com", "incoming": {"kind": "imap", "host": "imap.gmail.com", "security": "tls", "sign-in": "oauth"}, "outgoing": {"host": "smtp.gmail.com", "security": "tls", "sign-in": "oauth"}}, "lan": {"address": "me@lan.example.org", "incoming": {"kind": "pop3", "host": "pop.example.org", "security": "tls", "sign-in": "kerberos"}, "outgoing": {"host": "smtp.example.org", "security": "tls", "sign-in": "none"}}}}`) // The username and password fields are encrypted and never read; a fixture holding junk there proves it. f.write(profileDir+"/logins.json", `{"logins": [ {"hostname": "imap://imap.example.org", "httpRealm": "imap://imap.example.org", "encryptedUsername": "MEIE...", "encryptedPassword": "MDoE..."}, {"hostname": "smtp://smtp.example.org", "encryptedUsername": "x", "encryptedPassword": "y"}]}`) c := f.countAccounts(filepath.Join(f.Root, profileDir), "~/.thunderbird/60016jsp.default-default") if c.Declared != 3 || c.WithoutSignIn != 1 || c.Others != 2 || c.InList != 0 { t.Fatalf("three declared, the OAuth one not signed in, the person's and Owl's other (local folders not counted): %+v", c) } raw, _ := json.Marshal(c) if strings.Contains(string(raw), "example") || strings.Contains(string(raw), "gmail") { t.Fatalf("a count names nothing: %s", raw) } } // ADR 0329 §1, §10, §12, §13: betterbird_accounts lists the accounts with their values, Owl's by name and address // and marked; its propose carries the mesh-wide layer as it stands with accounts in it, and answers the value. func TestBetterbirdAccountsListsTheValuesAndProposesThemInTheLayerAsItStands(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.write(profileDir+"/logins.json", `{"logins":[{"hostname":"imap://imap.example.org","encryptedUsername":"MDoEEPgAAAAA","encryptedPassword":"hunter2secret"}]}`) links(t, nil) a, err := f.ProfileAccounts(false) if err != nil { t.Fatal(err) } if a.Declarable["imap"] != 1 || a.KeptByOwl != 1 || a.NotDeclarable["local folders"] != 1 || a.Proposed != "" { t.Fatalf("%+v", a) } raw, _ := json.Marshal(credentialChecked(a)) for _, shown := range []string{"other@mail.example.org", "imap.example.org", "smtp.example.org", `"port":993`, `"work@corp.example.com"`} { if !strings.Contains(string(raw), shown) { t.Errorf("the answer shows the operator's own %s (ADR 0329 §1): %s", shown, raw) } } for _, never := range []string{"hunter2", "MDoEEP"} { if strings.Contains(string(raw), never) { t.Errorf("the answer carries the login store's %q", never) } } for _, l := range a.Accounts { if l.Key == "account3" && (!l.Owl || l.Value != nil || l.Address != "work@corp.example.com") { t.Errorf("Owl's account is listed by name and address, marked, never as a value to declare: %+v", l) } } var asked []map[string]any asker = func(key string, body any) (json.RawMessage, error) { if key != "seat:mesh-controller.settings" { t.Fatalf("asked %s", key) } args := body.(map[string]any) asked = append(asked, args) if args["propose"] == nil { return json.RawMessage(`{"ok": true, "output": "{\n \"tags\": {\"$label1\": {\"name\": \"Important\"}}\n}\n approved by the operator\nbetterbird on the whole mesh, every value"}`), nil } return json.RawMessage(`{"ok": true, "output": "proposed p-17 for betterbird on the whole mesh: accounts (other@mail.example.org); fingerprint ab12"}`), nil } t.Cleanup(func() { asker = nil }) a, err = f.ProfileAccounts(true) if err != nil { t.Fatal(err) } if len(asked) != 2 || asked[1]["module"] != "betterbird" || asked[1]["propose"] != "true" || asked[1]["node"] != nil || asked[1]["replace"] != nil { t.Fatalf("the layer read, then proposed for the whole mesh, never set, never removing: %v", asked) } var layer map[string]json.RawMessage if err := json.Unmarshal([]byte(asked[1]["values"].(string)), &layer); err != nil || string(layer["tags"]) == "" { t.Fatalf("the proposal carries the layer as it stands: %v %s", err, asked[1]["values"]) } value, err := parseAccounts([]byte(asked[1]["values"].(string))) if err != nil { t.Fatalf("the proposed value is one the module takes: %v", err) } other, ok := value["mail"] if len(value) != 1 || !ok || other.Address != "other@mail.example.org" || other.Incoming.SignIn != "oauth" || other.Incoming.Port != 993 || other.Outgoing.Security != "tls" || other.Outgoing.Port != 465 { t.Fatalf("the value holds the person's account and not Owl's: %+v", value) } if a.Derived["mail"].Address != "other@mail.example.org" { t.Fatalf("the answer shows the value it proposed (ADR 0329 §10): %+v", a.Derived) } if strings.Contains(a.Proposed, "other@") || !strings.Contains(a.Proposed, "p-17") { t.Fatalf("the controller's words come back with every address masked: %s", a.Proposed) } } func TestAnAdoptedAccountNeverWritesIntoAnOutgoingServerAnotherIdentityUses(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") os.RemoveAll(filepath.Join(f.Root, "proc", "4000")) // A second account of the person's, not declared, sends through smtp1 too. prefs := strings.Replace(f.read(profileDir+"/prefs.js"), `"account1,account3,account2"`, `"account1,account3,account2,account5"`, 1) + `user_pref("mail.account.account5.server", "server5");` + "\n" + `user_pref("mail.account.account5.identities", "id5");` + "\n" + `user_pref("mail.identity.id5.useremail", "fifth@mail.example.org");` + "\n" + `user_pref("mail.identity.id5.smtpServer", "smtp1");` + "\n" + `user_pref("mail.server.server5.type", "imap");` + "\n" f.write(profileDir+"/prefs.js", prefs) f.declareAccounts(adoptable) err := f.Prepare() if err == nil || !strings.Contains(err.Error(), "also used by identity id5") || strings.Contains(err.Error(), "example.org") { t.Fatalf("refused with the remedy, by keys: %v", err) } if user, _ := os.ReadFile(filepath.Join(f.Root, profileDir, "user.js")); strings.Contains(string(user), "smtpserver.smtp1") { t.Fatal("the shared outgoing server was written") } // Recorded adoption, then the person moves the identity to a server of its own: the module follows it. f.write(profileDir+"/prefs.js", strings.Replace(prefs, `"mail.identity.id5.smtpServer", "smtp1"`, `"mail.identity.id5.smtpServer", "smtp5"`, 1)) if err := f.Prepare(); err != nil { t.Fatal(err) } moved := strings.Replace(f.read(profileDir+"/prefs.js"), `"mail.identity.id1.smtpServer", "smtp1"`, `"mail.identity.id1.smtpServer", "smtp9"`, 1) + `user_pref("mail.smtpserver.smtp9.hostname", "smtp.example.org");` + "\n" f.write(profileDir+"/prefs.js", moved) u, err := f.userJS(true) if err != nil || u.Each[0].How != adopted || u.Each[0].Keys.SMTP != "smtp9" { t.Fatalf("the identity's current outgoing server is followed: %+v %v", u.Each, err) } user := f.read(profileDir + "/user.js") if strings.Contains(user, "smtpserver.smtp1.") || !strings.Contains(user, "smtpserver.smtp9.hostname") { t.Fatalf("never the old one:\n%s", user) } // And the move back onto a shared server is refused again. f.write(profileDir+"/prefs.js", strings.Replace(moved, `"mail.identity.id5.smtpServer", "smtp5"`, `"mail.identity.id5.smtpServer", "smtp9"`, 1)) if err := f.Prepare(); err == nil || !strings.Contains(err.Error(), "also used by identity id5") { t.Fatalf("%v", err) } } func TestTheUserJSAnswerWithholdsTheAccountsValues(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare(`user_pref("mail.compose.default_to", "x");`) f.declareAccounts(personal) u, err := f.userJS(false) if err != nil { t.Fatal(err) } for _, never := range []string{"example.org", `"me"`, `"Me"`} { if strings.Contains(u.Content, never) { t.Errorf("the answer carries %s:\n%s", never, u.Content) } } if !strings.Contains(u.Content, `user_pref("mail.server.server_mesh_personal.hostname", "");`) { t.Fatalf("an account's preference is named and its value withheld:\n%s", u.Content) } // What is placed in the profile keeps the values: only the answer withholds them. if f.read(testHome+"/"+accountsFile) == "" { t.Fatal("no declaration") } } func TestACredentialRefusalNamesNoAccount(t *testing.T) { _, err := parseAccounts([]byte(`{"accounts": {"personal": {"incoming": {"password": "hunter2"}}}}`)) if err == nil || strings.Contains(err.Error(), "personal") || !strings.Contains(err.Error(), "accounts..incoming.password") { t.Fatalf("%v", err) } } func TestAnAccountsOwnAliasesAndNonMailIdentitiesDoNotShareItsOutgoingServer(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") os.RemoveAll(filepath.Join(f.Root, "proc", "4000")) // account1 has an alias identity on smtp1; smtp1 is the default server; Owl's account and local folders // have identities that name no outgoing server. prefs := strings.Replace(f.read(profileDir+"/prefs.js"), `"mail.account.account1.identities", "id1"`, `"mail.account.account1.identities", "id1,id1b"`, 1) prefs = strings.Replace(prefs, `"mail.account.account2.server", "server2");`, `"mail.account.account2.server", "server2");`+"\n"+ `user_pref("mail.account.account2.identities", "id2");`, 1) + `user_pref("mail.identity.id1b.useremail", "alias@mail.example.org");` + "\n" + `user_pref("mail.identity.id1b.smtpServer", "smtp1");` + "\n" + `user_pref("mail.identity.id2.useremail", "local@mail.example.org");` + "\n" + `user_pref("mail.smtp.defaultserver", "smtp1");` + "\n" f.write(profileDir+"/prefs.js", prefs) f.declareAccounts(adoptable) if err := f.Prepare(); err != nil { t.Fatalf("an account's own alias, and identities that do not send by SMTP, are no sharing: %v", err) } // An identity of another mail account naming no server does use the default one. f.write(profileDir+"/prefs.js", strings.Replace(prefs, `"account1,account3,account2"`, `"account1,account3,account2,account6"`, 1)+ `user_pref("mail.account.account6.server", "server6");`+"\n"+ `user_pref("mail.account.account6.identities", "id6");`+"\n"+ `user_pref("mail.identity.id6.useremail", "six@mail.example.org");`+"\n"+ `user_pref("mail.server.server6.type", "pop3");`+"\n") os.Remove(filepath.Join(f.Root, testHome, recordFile)) if err := f.Prepare(); err == nil || !strings.Contains(err.Error(), "also used by identity id6") { t.Fatalf("%v", err) } } // Review 121: the account lists wait for Betterbird to close by its profile's lock too, not only by its process. func TestTheAccountListsWaitWhileTheProfileIsLocked(t *testing.T) { f := newFake(t) f.profileWithAccounts() f.noPackage() links(t, nil) f.declare("// none") f.declareAccounts(personal) stopBetterbird(t, f) f.write(profileDir+"/lock", "") before := f.read(profileDir + "/prefs.js") u, err := f.userJS(true) if err != nil { t.Fatal(err) } if !strings.HasPrefix(u.Lists, "not written") || !strings.Contains(u.Lists, "locked") || f.read(profileDir+"/prefs.js") != before { t.Fatalf("with the profile locked and no Betterbird running, prefs.js is unchanged: %q", u.Lists) } os.Remove(filepath.Join(f.Root, profileDir, "lock")) if u, err = f.userJS(true); err != nil || !strings.HasPrefix(u.Lists, "written") { t.Fatalf("once unlocked the list is written: %q %v", u.Lists, err) } }