package oltp
import (
"path/filepath"
"testing"
"time"
)
// The ordinary path: a pending address confirms into the account's
// confirmed one, and the code is spent — a second confirm refuses.
func TestPendingEmailConfirms(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
code, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil || len(code) != 6 {
t.Fatalf("SetPendingEmail: %q %v", code, err)
}
a, _ := db.AccountByName("dan")
if a.EmailPending != "[email protected]" || a.Email != "" {
t.Fatalf("account after set = %+v", a)
}
addr, ok, err := db.ConfirmEmail("dan", code)
if err != nil || !ok || addr != "[email protected]" {
t.Fatalf("ConfirmEmail = %q %v %v", addr, ok, err)
}
a, _ = db.AccountByName("dan")
if a.Email != "[email protected]" || a.EmailPending != "" {
t.Fatalf("account after confirm = %+v", a)
}
// Spent: a second confirm of the same code refuses.
if _, ok, err := db.ConfirmEmail("dan", code); ok || err != nil {
t.Fatalf("a spent code confirmed again: ok=%v err=%v", ok, err)
}
}
// A wrong code counts an attempt and refuses without spending the real
// one; the fifth wrong attempt burns the code outright.
func TestWrongCodeAttemptsThenBurns(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
code, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
wrong := "000000"
if wrong == code {
wrong = "111111"
}
// Four wrong attempts: the code is still live afterward.
for i := 0; i < maxCodeAttempts-1; i++ {
if _, ok, err := db.ConfirmEmail("dan", wrong); ok || err != nil {
t.Fatalf("attempt %d: ok=%v err=%v", i, ok, err)
}
}
if addr, ok, err := db.ConfirmEmail("dan", code); err != nil || !ok || addr != "[email protected]" {
t.Fatalf("the right code still refused after 4 wrong attempts: addr=%q ok=%v err=%v", addr, ok, err)
}
}
// The fifth wrong attempt burns the code even though it was never
// guessed correctly: a new me.email.set is required either way.
func TestFifthWrongAttemptBurnsTheCode(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
code, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
wrong := "000000"
if wrong == code {
wrong = "111111"
}
for i := 0; i < maxCodeAttempts; i++ {
db.ConfirmEmail("dan", wrong)
}
// The code was live and correct, but the fifth wrong guess already
// burned it — even the right code now refuses.
if _, ok, err := db.ConfirmEmail("dan", code); ok || err != nil {
t.Fatalf("the code confirmed after being burned: ok=%v err=%v", ok, err)
}
}
// An expired code refuses exactly like a spent one.
func TestExpiredEmailCodeRefuses(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
code, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
// Back-date the row past its TTL directly, the same way
// beta_test.go's siblings would if invite tokens needed it — there
// is no public API for minting an already-expired one.
if _, err := db.db.Exec(`UPDATE email_token SET expires = ? WHERE account = ?`,
time.Now().UTC().Add(-time.Hour).Format(time.RFC3339), "dan"); err != nil {
t.Fatal(err)
}
if _, ok, err := db.ConfirmEmail("dan", code); ok || err != nil {
t.Fatalf("an expired code confirmed: ok=%v err=%v", ok, err)
}
a, _ := db.AccountByName("dan")
if a.Email != "" {
t.Fatalf("an expired confirm still promoted the address: %+v", a)
}
}
// A new me.email.set replaces any outstanding code: the old one no
// longer confirms even though it has not expired.
func TestNewSetReplacesTheOldCode(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
first, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
second, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
if _, ok, err := db.ConfirmEmail("dan", first); ok || err != nil {
t.Fatalf("a superseded code still confirmed: ok=%v err=%v", ok, err)
}
addr, ok, err := db.ConfirmEmail("dan", second)
if err != nil || !ok || addr != "[email protected]" {
t.Fatalf("the latest code did not confirm: addr=%q ok=%v err=%v", addr, ok, err)
}
}
// ClearEmail blanks both halves and closes the race: a code minted
// before the clear must not resurrect the address afterward.
func TestClearEmailBlanksBothAndSpendsLiveCodes(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.AddAccount("dan", "pw", false); err != nil {
t.Fatal(err)
}
if err := db.SetEmail("dan", "[email protected]"); err != nil {
t.Fatal(err)
}
code, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
if err := db.ClearEmail("dan"); err != nil {
t.Fatal(err)
}
a, _ := db.AccountByName("dan")
if a.Email != "" || a.EmailPending != "" {
t.Fatalf("account after clear = %+v", a)
}
if _, ok, err := db.ConfirmEmail("dan", code); ok || err != nil {
t.Fatalf("a code from before the clear still confirmed: ok=%v err=%v", ok, err)
}
}