package oltp
import (
"testing"
"time"
)
// #319: an admin who re-sends an invite holds a second link. Once the
// owner claims the account with either one, the other must not set the
// owner's password — or the admin signs in as them.
func TestAStaleInviteCannotTakeOverAClaimedAccount(t *testing.T) {
db := testDB(t)
first, err := db.InviteAccount("bob", "[email protected]", false)
if err != nil {
t.Fatal(err)
}
second, err := db.InviteAccount("bob", "[email protected]", false)
if err != nil {
t.Fatal(err)
}
if _, err := db.RedeemInvite(second, "bobs-own-pw"); err != nil {
t.Fatalf("bob could not claim his account: %v", err)
}
if _, err := db.RedeemInvite(first, "admins-pw"); err == nil {
t.Error("FAIL: the older invite set a claimed account's password")
}
if _, ok := db.Authenticate("bob", "admins-pw"); ok {
t.Error("FAIL: the admin signs in as bob")
}
if _, ok := db.Authenticate("bob", "bobs-own-pw"); !ok {
t.Error("FAIL: bob's own password stopped working")
}
}
// Re-inviting supersedes, as ResetPassword does: one live link per
// pending account, the newest.
func TestReinviteSupersedesTheOlderLink(t *testing.T) {
db := testDB(t)
first, err := db.InviteAccount("bob", "", false)
if err != nil {
t.Fatal(err)
}
second, err := db.InviteAccount("bob", "", false)
if err != nil {
t.Fatal(err)
}
if _, ok := db.InviteFor(first); ok {
t.Error("FAIL: the superseded invite is still live")
}
if name, ok := db.InviteFor(second); !ok || name != "bob" {
t.Errorf("FAIL: the newest invite is not live: %q %v", name, ok)
}
}
// mintRaw writes a token row the way a build before the supersede did,
// so a database carrying such rows across the upgrade is covered too.
func mintRaw(t *testing.T, db *DB, token, account, kind string) {
t.Helper()
now := time.Now().UTC()
if _, err := db.w.Exec(`INSERT INTO invite (token, account, created, expires, kind) VALUES (?, ?, ?, ?, ?)`,
hashToken(token), account, now.Format(time.RFC3339), now.Add(InviteTTL).Format(time.RFC3339), kind); err != nil {
t.Fatal(err)
}
}
// An invite token is only ever good while the account is unclaimed,
// whatever else is true of it.
func TestAnInviteForAClaimedAccountIsRefused(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("bob", "bobs-own-pw", false); err != nil {
t.Fatal(err)
}
mintRaw(t, db, "left-over", "bob", "invite")
if _, err := db.RedeemInvite("left-over", "admins-pw"); err == nil {
t.Error("FAIL: an invite set a claimed account's password")
}
if _, ok := db.Authenticate("bob", "bobs-own-pw"); !ok {
t.Error("FAIL: bob's own password stopped working")
}
}
// Redeeming any token spends every other unspent one the account has,
// invite or reset alike.
func TestRedeemingSpendsTheAccountsOtherTokens(t *testing.T) {
db := testDB(t)
if _, err := db.InviteAccount("bob", "", false); err != nil {
t.Fatal(err)
}
mintRaw(t, db, "inv-a", "bob", "invite")
mintRaw(t, db, "inv-b", "bob", "invite")
if _, err := db.RedeemInvite("inv-a", "bobs-own-pw"); err != nil {
t.Fatal(err)
}
if _, ok := db.InviteFor("inv-b"); ok {
t.Error("FAIL: a second invite survived the claim")
}
mintRaw(t, db, "rst-a", "bob", "reset")
mintRaw(t, db, "rst-b", "bob", "reset")
if _, err := db.RedeemInvite("rst-a", "bobs-next-pw"); err != nil {
t.Fatal(err)
}
if _, ok := db.InviteFor("rst-b"); ok {
t.Error("FAIL: a second reset survived the redemption")
}
}
// Setting a password retires an imported fossil credential: left in
// place, the old fossil password still signs in and Authenticate's
// upgrade then writes it over the password just set.
func TestSetPasswordRetiresTheLegacyCredential(t *testing.T) {
for _, self := range []bool{false, true} {
db := testDB(t)
const code, login, old = "code", "alice", "fossil-pw"
if _, _, err := db.ImportUsers("repo", code, []ImportedUser{
{Login: login, PWHash: legacySecret(code, login, old)},
}); err != nil {
t.Fatal(err)
}
set := db.SetPassword
if self {
set = func(name, pw string) error { return db.SetPasswordSelf(name, pw, "") }
}
if err := set(login, "brand-new-pw"); err != nil {
t.Fatal(err)
}
if _, _, ok := db.LegacyCredential(login); ok {
t.Errorf("FAIL (self=%v): the legacy credential survived a password change", self)
}
if _, ok := db.Authenticate(login, old); ok {
t.Errorf("FAIL (self=%v): the old fossil password still signs in", self)
}
if _, ok := db.Authenticate(login, "brand-new-pw"); !ok {
t.Errorf("FAIL (self=%v): the new password does not sign in", self)
}
}
}