package oltp
import (
"path/filepath"
"testing"
"time"
)
// The ordinary path: a pending address confirms into the account's
// confirmed one, and the token 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)
}
token, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil || token == "" {
t.Fatalf("SetPendingEmail: %q %v", token, err)
}
a, _ := db.AccountByName("dan")
if a.EmailPending != "[email protected]" || a.Email != "" {
t.Fatalf("account after set = %+v", a)
}
if acct, ok := db.EmailTokenAccount(token); !ok || acct != "dan" {
t.Fatalf("EmailTokenAccount = %q %v", acct, ok)
}
acct, addr, ok, err := db.ConfirmEmail(token)
if err != nil || !ok || acct != "dan" || addr != "[email protected]" {
t.Fatalf("ConfirmEmail = %q %q %v %v", acct, 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 token refuses.
if _, _, ok, err := db.ConfirmEmail(token); ok || err != nil {
t.Fatalf("a spent token confirmed again: ok=%v err=%v", ok, err)
}
if _, ok := db.EmailTokenAccount(token); ok {
t.Fatal("a spent token still names its account")
}
}
// An expired token refuses exactly like a spent one — neither
// EmailTokenAccount nor ConfirmEmail treats it as live.
func TestExpiredEmailTokenRefuses(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)
}
token, err := db.SetPendingEmail("dan", "[email protected]")
if err != nil {
t.Fatal(err)
}
// Back-date the token 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 token = ?`,
time.Now().UTC().Add(-time.Hour).Format(time.RFC3339), hashToken(token)); err != nil {
t.Fatal(err)
}
if _, ok := db.EmailTokenAccount(token); ok {
t.Fatal("an expired token still names its account")
}
if _, _, ok, err := db.ConfirmEmail(token); ok || err != nil {
t.Fatalf("an expired token 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)
}
}
// ClearEmail blanks both halves and closes the race: a token minted
// before the clear must not resurrect the address afterward.
func TestClearEmailBlanksBothAndSpendsLiveTokens(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)
}
token, 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(token); ok || err != nil {
t.Fatalf("a token from before the clear still confirmed: ok=%v err=%v", ok, err)
}
}