diff --git a/store/account_test.go b/store/account_test.go index 2c980cf..d623089 100644 --- a/store/account_test.go +++ b/store/account_test.go @@ -4,21 +4,33 @@ import ( "errors" "strings" "testing" + "time" "github.com/mattn/go-sqlite3" "lbryio/lbry-id/auth" ) -func expectAccountMatch(t *testing.T, s *Store, normEmail auth.NormalizedEmail, expectedEmail auth.Email, password auth.Password, seed auth.ClientSaltSeed) { +func expectAccountMatch( + t *testing.T, + s *Store, + normEmail auth.NormalizedEmail, + expectedEmail auth.Email, + password auth.Password, + seed auth.ClientSaltSeed, + expectedVerifyTokenString auth.VerifyTokenString, + approxVerifyExpiration *time.Time, +) { var key auth.KDFKey var salt auth.ServerSalt var email auth.Email + var verifyExpiration *time.Time + var verifyTokenString auth.VerifyTokenString err := s.db.QueryRow( - `SELECT key, server_salt, email from accounts WHERE normalized_email=? AND client_salt_seed=?`, + `SELECT key, server_salt, email, verify_token, verify_expiration from accounts WHERE normalized_email=? AND client_salt_seed=?`, normEmail, seed, - ).Scan(&key, &salt, &email) + ).Scan(&key, &salt, &email, &verifyTokenString, &verifyExpiration) if err != nil { t.Fatalf("Error finding account for: %s %s - %+v", normEmail, password, err) } @@ -34,6 +46,31 @@ func expectAccountMatch(t *testing.T, s *Store, normEmail auth.NormalizedEmail, if email != expectedEmail { t.Fatalf("Email case not as expected. Want: %s Got: %s", email, expectedEmail) } + + if verifyTokenString != expectedVerifyTokenString { + t.Fatalf( + "Verify token string not as expected. Want: %s Got: %s", + verifyTokenString, + expectedVerifyTokenString, + ) + } + + if approxVerifyExpiration != nil { + if verifyExpiration == nil { + t.Fatalf("Expected verify expiration to not be nil") + } + expDiff := approxVerifyExpiration.Sub(*verifyExpiration) + if time.Second < expDiff || expDiff < -time.Second { + t.Fatalf( + "Verify expiration not as expected. Want approximately: %s Got: %s", + verifyExpiration, + approxVerifyExpiration, + ) + } + } + if approxVerifyExpiration == nil && verifyExpiration != nil { + t.Fatalf("Expected verify expiration to be nil. Got: %+v", verifyExpiration) + } } func expectAccountNotExists(t *testing.T, s *Store, normEmail auth.NormalizedEmail) { @@ -65,19 +102,20 @@ func TestStoreCreateAccount(t *testing.T) { // Get an account, come back empty expectAccountNotExists(t, &s, normEmail) - // Create an account - if err := s.CreateAccount(email, password, seed); err != nil { + // Create an account. Make it verified (i.e. no token) for the usual + // case. We'll test unverified (with token) separately. + if err := s.CreateAccount(email, password, seed, ""); err != nil { t.Fatalf("Unexpected error in CreateAccount: %+v", err) } // Get and confirm the account we just put in - expectAccountMatch(t, &s, normEmail, email, password, seed) + expectAccountMatch(t, &s, normEmail, email, password, seed, "", nil) newPassword := auth.Password("xyz") // Try to create a new account with the same email and different password, // fail because email already exists - if err := s.CreateAccount(email, newPassword, seed); err != ErrDuplicateAccount { + if err := s.CreateAccount(email, newPassword, seed, ""); err != ErrDuplicateAccount { t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err) } @@ -85,12 +123,30 @@ func TestStoreCreateAccount(t *testing.T) { // Try to create a new account with the same email different capitalization. // fail because email already exists - if err := s.CreateAccount(differentCaseEmail, password, seed); err != ErrDuplicateAccount { + if err := s.CreateAccount(differentCaseEmail, password, seed, ""); err != ErrDuplicateAccount { t.Fatalf(`CreateAccount err (for case insensitivity check): wanted "%+v", got "%+v"`, ErrDuplicateAccount, err) } // Get the email and same *first* password we successfully put in - expectAccountMatch(t, &s, normEmail, email, password, seed) + expectAccountMatch(t, &s, normEmail, email, password, seed, "", nil) +} + +// Try CreateAccount with a verification string, thus unverified +func TestStoreCreateAccountUnverified(t *testing.T) { + s, sqliteTmpFile := StoreTestInit(t) + defer StoreTestCleanup(sqliteTmpFile) + + email, normEmail := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com") + password, seed := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234") + + // Create an account + if err := s.CreateAccount(email, password, seed, "abcd1234abcd1234abcd1234abcd1234"); err != nil { + t.Fatalf("Unexpected error in CreateAccount: %+v", err) + } + + // Get and confirm the account we just put in + approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC() + expectAccountMatch(t, &s, normEmail, email, password, seed, "abcd1234abcd1234abcd1234abcd1234", &approxVerifyExpiration) } // Test GetUserId for nonexisting email @@ -165,7 +221,7 @@ func TestStoreAccountEmptyFields(t *testing.T) { var sqliteErr sqlite3.Error - err := s.CreateAccount(tc.email, tc.password, tc.clientSaltSeed) + err := s.CreateAccount(tc.email, tc.password, tc.clientSaltSeed, "") if errors.As(err, &sqliteErr) { if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintCheck) { return // We got the error we expected diff --git a/store/password_test.go b/store/password_test.go index 7df6af9..8a66abd 100644 --- a/store/password_test.go +++ b/store/password_test.go @@ -47,7 +47,7 @@ func TestStoreChangePasswordSuccess(t *testing.T) { t.Errorf("ChangePasswordWithWallet (lower case email): unexpected error: %+v", err) } - expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed) + expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, "", nil) expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac) expectTokenNotExists(t, &s, token) @@ -63,7 +63,7 @@ func TestStoreChangePasswordSuccess(t *testing.T) { t.Errorf("ChangePasswordWithWallet (upper case email): unexpected error: %+v", err) } - expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed) + expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, "", nil) } func TestStoreChangePasswordErrors(t *testing.T) { @@ -161,7 +161,7 @@ func TestStoreChangePasswordErrors(t *testing.T) { // This tests the transaction rollbacks in particular, given the errors // that are at a couple different stages of the txn, triggered by these // tests. - expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed) + expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, "", nil) if tc.hasWallet { expectWalletExists(t, &s, userId, oldEncryptedWallet, oldSequence, oldHmac) } else { @@ -196,7 +196,7 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) { t.Errorf("ChangePasswordNoWallet (lower case email): unexpected error: %+v", err) } - expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed) + expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, "", nil) expectWalletNotExists(t, &s, userId) expectTokenNotExists(t, &s, token) @@ -209,7 +209,7 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) { t.Errorf("ChangePasswordNoWallet (upper case email): unexpected error: %+v", err) } - expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed) + expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, "", nil) } func TestStoreChangePasswordNoWalletErrors(t *testing.T) { @@ -292,7 +292,7 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) { // deleted. This tests the transaction rollbacks in particular, given the // errors that are at a couple different stages of the txn, triggered by // these tests. - expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed) + expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, "", nil) if tc.hasWallet { expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac) } else { diff --git a/store/store.go b/store/store.go index 6778e9c..ce20599 100644 --- a/store/store.go +++ b/store/store.go @@ -120,6 +120,8 @@ func (s *Store) Migrate() error { key TEXT NOT NULL, client_salt_seed TEXT NOT NULL, server_salt TEXT NOT NULL, + verify_token TEXT NOT NULL, + verify_expiration DATETIME, user_id INTEGER PRIMARY KEY AUTOINCREMENT, CHECK ( email <> '' AND @@ -369,10 +371,16 @@ func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed aut return } + var verifyExpiration *time.Time + if len(verifyToken) > 0 { + verifyExpiration = new(time.Time) + *verifyExpiration = time.Now().UTC().Add(time.Hour * 24 * 2) + } + // userId auto-increments _, err = s.db.Exec( - "INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed) VALUES(?,?,?,?,?)", - email.Normalize(), email, key, salt, seed, + "INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token, verify_expiration) VALUES(?,?,?,?,?,?,?)", + email.Normalize(), email, key, salt, seed, verifyToken, verifyExpiration, ) var sqliteErr sqlite3.Error if errors.As(err, &sqliteErr) { diff --git a/store/store_test.go b/store/store_test.go index 2b56d92..094b52c 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -45,8 +45,8 @@ func makeTestUser(t *testing.T, s *Store) (userId auth.UserId, email auth.Email, seed = auth.ClientSaltSeed("abcd1234abcd1234") rows, err := s.db.Query( - "INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed) values(?,?,?,?,?) returning user_id", - normEmail, email, key, salt, seed, + "INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token) values(?,?,?,?,?,?) returning user_id", + normEmail, email, key, salt, seed, "", ) if err != nil { t.Fatalf("Error setting up account: %+v", err)