Skip to content
quarry Artifact [c5f2fc78a8]
Login

Artifact [c5f2fc78a8]

Artifact c5f2fc78a8699ed11feea9f87b62e219ce481ae43f717607726edd04edd93f2e:


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)
	}
}