package oltp
import (
"path/filepath"
"testing"
)
func resetDB(t *testing.T) *DB {
t.Helper()
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
return db
}
// The whole point: somebody who has forgotten their password chooses a
// new one themselves, and no operator ever holds it.
func TestAResetLetsSomebodyChooseTheirOwnPassword(t *testing.T) {
db := resetDB(t)
if err := db.AddAccount("jo", "the-old-password", false); err != nil {
t.Fatal(err)
}
token, err := db.ResetPassword("jo")
if err != nil {
t.Fatalf("FAIL: could not mint a reset: %v", err)
}
if name, ok := db.InviteFor(token); !ok || name != "jo" {
t.Fatalf("FAIL: the token names %q ok=%v", name, ok)
}
if k := db.TokenKind(token); k != "reset" {
t.Errorf("FAIL: the token reads as %q, so the page says the wrong thing", k)
} else {
t.Log("ok: the token knows it is a reset")
}
// The old password still works until the link is spent: a reset
// that locked the account would make a lost link worse than the
// problem it was sent to solve.
if !db.VerifyPassword("jo", "the-old-password") {
t.Error("FAIL: minting a reset locked the account out")
} else {
t.Log("ok: the old password holds until the link is used")
}
if _, err := db.RedeemInvite(token, "a-brand-new-password"); err != nil {
t.Fatalf("FAIL: redeeming: %v", err)
}
if !db.VerifyPassword("jo", "a-brand-new-password") {
t.Error("FAIL: the new password does not work")
} else {
t.Log("ok: the new password works")
}
if db.VerifyPassword("jo", "the-old-password") {
t.Error("FAIL: the old password still works after the reset")
} else {
t.Log("ok: the old password is gone")
}
// Spent, and spent once.
if _, ok := db.InviteFor(token); ok {
t.Error("FAIL: the link can be used twice")
} else {
t.Log("ok: the link is one-time")
}
}
// Minting again supersedes, so "send it again" cannot leave two live
// credentials behind where the operator believes there is one.
func TestASecondResetKillsTheFirst(t *testing.T) {
db := resetDB(t)
if err := db.AddAccount("jo", "the-old-password", false); err != nil {
t.Fatal(err)
}
first, err := db.ResetPassword("jo")
if err != nil {
t.Fatal(err)
}
second, err := db.ResetPassword("jo")
if err != nil {
t.Fatal(err)
}
if _, ok := db.InviteFor(first); ok {
t.Error("FAIL: the superseded link is still live")
} else {
t.Log("ok: the older link is dead")
}
if _, ok := db.InviteFor(second); !ok {
t.Error("FAIL: the new link is not live")
} else {
t.Log("ok: the newer link works")
}
}
// An account that has never been claimed wants the invite it is already
// waiting on, not a reset of a password it does not have.
func TestAPendingAccountIsToldToUseItsInvite(t *testing.T) {
db := resetDB(t)
if _, err := db.InviteAccount("newbie", "", false); err != nil {
t.Fatal(err)
}
if _, err := db.ResetPassword("newbie"); err == nil {
t.Error("FAIL: a pending account was given a reset")
} else {
t.Logf("ok: refused — %v", err)
}
if _, err := db.ResetPassword("nobody"); err == nil {
t.Error("FAIL: an account that does not exist was given a reset")
} else {
t.Logf("ok: refused — %v", err)
}
}
// An invite is the way into an account that has no password. Once the
// account has one, an invite minted earlier (a re-sent link keeps the
// old one alive) must not be a second way to overwrite it.
func TestAnInviteCannotOverwriteAPasswordOnceTheAccountIsClaimed(t *testing.T) {
db := resetDB(t)
first, err := db.InviteAccount("jo", "[email protected]", false)
if err != nil {
t.Fatal(err)
}
second, err := db.InviteAccount("jo", "[email protected]", false)
if err != nil {
t.Fatal(err)
}
if _, err := db.RedeemInvite(first, "the-real-password"); err != nil {
t.Fatalf("FAIL: redeeming the first invite: %v", err)
}
if _, ok := db.InviteFor(second); ok {
t.Error("FAIL: a leftover invite still names a claimed account")
}
if _, err := db.RedeemInvite(second, "attacker-password"); err == nil {
t.Error("FAIL: a leftover invite overwrote a claimed account's password")
}
if !db.VerifyPassword("jo", "the-real-password") {
t.Error("FAIL: the claimed account's password was replaced")
}
}
// A reset is for an account that has a password; a token of that kind
// is refused for an account that is waiting on its invite.
func TestAResetTokenIsRefusedForAnAccountWithNoPassword(t *testing.T) {
db := resetDB(t)
if err := db.AddAccount("jo", "the-old-password", false); err != nil {
t.Fatal(err)
}
token, err := db.ResetPassword("jo")
if err != nil {
t.Fatal(err)
}
if _, err := db.w.Exec(`UPDATE account SET pwhash = '' WHERE name = 'jo'`); err != nil {
t.Fatal(err)
}
if _, ok := db.InviteFor(token); ok {
t.Error("FAIL: a reset token is live for a passwordless account")
}
if _, err := db.RedeemInvite(token, "x-new-password-1"); err == nil {
t.Error("FAIL: a reset token claimed a passwordless account")
}
}