wallet-sync-server/store/account_test.go
Daniel Krol aa691dbc09 Use verify_token=null as verified to avoid unique constraint
We were using verify_token="" to mean that the user was verified. We need a unique constraint on verify_token to prevent two users from getting the same verify link in their email. This means that if we have two verified users, they will both have verify_token="", which triggers the unique constraint. Oops. However, null is an exception to unique constraints, so we're now using that instead to mean verified.
2022-08-13 22:15:19 -04:00

442 lines
16 KiB
Go

package store
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,
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, verify_token, verify_expiration from accounts WHERE normalized_email=? AND client_salt_seed=?`,
normEmail, seed,
).Scan(&key, &salt, &email, &verifyTokenString, &verifyExpiration)
if err != nil {
t.Fatalf("Error finding account for: %s %s - %+v", normEmail, password, err)
}
match, err := password.Check(key, salt)
if err != nil {
t.Fatalf("Error checking password for: %s %s - %+v", email, password, err)
}
if !match {
t.Fatalf("Password incorrect for: %s %s", email, password)
}
if email != expectedEmail {
t.Fatalf("Email case not as expected. Want: %s Got: %s", email, expectedEmail)
}
if (verifyTokenString == nil) != (expectedVerifyTokenString == nil) {
t.Fatalf(
"Verify token string nil-ness not as expected. Want: %v Got: %v",
expectedVerifyTokenString == nil,
verifyTokenString == nil,
)
}
if expectedVerifyTokenString != nil {
if *verifyTokenString != *expectedVerifyTokenString {
t.Fatalf(
"Verify token string not as expected. Want: %s Got: %s",
*expectedVerifyTokenString,
*verifyTokenString,
)
}
}
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) {
rows, err := s.db.Query(
`SELECT 1 from accounts WHERE normalized_email=?`,
normEmail,
)
if err != nil {
t.Fatalf("Error finding account for: %s - %+v", normEmail, err)
}
defer rows.Close()
for rows.Next() {
t.Fatalf("Expected no account for: %s", normEmail)
}
// found nothing, we're good
}
// Test CreateAccount
// Try CreateAccount twice with the same email and different password, error the second time
func TestStoreCreateAccount(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")
// Get an account, come back empty
expectAccountNotExists(t, &s, normEmail)
// 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, nil); 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, nil, 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, nil); err != ErrDuplicateAccount {
t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err)
}
differentCaseEmail := auth.Email("aBC@examplE.CoM")
// Try to create a new account with the same email different capitalization.
// fail because email already exists
if err := s.CreateAccount(differentCaseEmail, password, seed, nil); 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, nil, nil)
}
// Test that I can use CreateAccount twice for different emails with no veriy token
// This is related to https://github.com/lbryio/wallet-sync-server/issues/13
func TestStoreCreateAccountTwoVerifiedSucceed(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email1, normEmail1 := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com")
password1, seed1 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
email2, normEmail2 := auth.Email("Abc2@Example.Com"), auth.NormalizedEmail("abc2@example.com")
password2, seed2 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
// Create a couple accounts. Don't care if they have the same password.
// Make them verified (i.e. no token) for the usual
// case. We'll test unverified (with token) separately.
if err := s.CreateAccount(email1, password1, seed1, nil); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
if err := s.CreateAccount(email2, password2, seed2, nil); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
// Get and confirm the accounts we just put in
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, nil, nil)
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, nil, nil)
}
// Test that I cannot use CreateAccount twice with the same verify token, but
// I can with different verify tokens
// This is related to https://github.com/lbryio/wallet-sync-server/issues/13
func TestStoreCreateAccountTwoSameVerfiyTokenFail(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email1, normEmail1 := auth.Email("Abc@Example.Com"), auth.NormalizedEmail("abc@example.com")
password1, seed1 := auth.Password("123"), auth.ClientSaltSeed("abcd1234abcd1234")
verifyToken1 := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
email2, normEmail2 := auth.Email("Abc2@Example.Com"), auth.NormalizedEmail("abc2@example.com")
password2, seed2 := auth.Password("xyz"), auth.ClientSaltSeed("abcd1234abcd1234")
verifyToken2 := auth.VerifyTokenString("00001234abcd1234abcd123400000000")
// Create the first account
if err := s.CreateAccount(email1, password1, seed1, &verifyToken1); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
// Try to create the second account with the same verify token, fail
if err := s.CreateAccount(email2, password2, seed2, &verifyToken1); err != ErrDuplicateAccount {
t.Fatalf(`CreateAccount err: wanted "%+v", got "%+v"`, ErrDuplicateAccount, err)
}
// Confirm that it didn't save
expectAccountNotExists(t, &s, normEmail2)
// Create the second account with a different verify token
if err := s.CreateAccount(email2, password2, seed2, &verifyToken2); err != nil {
t.Fatalf("Unexpected error in CreateAccount: %+v", err)
}
// Get and confirm the accounts we just put in
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, &verifyToken1, &approxVerifyExpiration)
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, &verifyToken2, &approxVerifyExpiration)
}
// 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
verifyToken := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
if err := s.CreateAccount(email, password, seed, &verifyToken); 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, &verifyToken, &approxVerifyExpiration)
}
// Test GetUserId for nonexisting email
func TestStoreGetUserIdAccountNotExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email, password := auth.Email("abc@example.com"), auth.Password("123")
if userId, err := s.GetUserId(email, password); err != ErrWrongCredentials || userId != 0 {
t.Fatalf(`GetUserId error for nonexistant account: wanted "%+v", got "%+v. userId: %v"`, ErrWrongCredentials, err, userId)
}
}
// Test GetUserId for existing account, with the correct and incorrect password
func TestStoreGetUserIdAccountExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
createdUserId, email, password, _ := makeTestUser(t, &s, nil, nil)
// Check that the userId is correct for the email, irrespective of the case of
// the characters in the email.
lowerEmail := auth.Email(strings.ToLower(string(email)))
upperEmail := auth.Email(strings.ToUpper(string(email)))
// Check that there's now a user id for the email and password
if userId, err := s.GetUserId(lowerEmail, password); err != nil || userId != createdUserId {
t.Fatalf("Unexpected error in GetUserId: err: %+v userId: %v", err, userId)
}
// Check that there's now a user id for the email and password
if userId, err := s.GetUserId(upperEmail, password); err != nil || userId != createdUserId {
t.Fatalf("Unexpected error in GetUserId: err: %+v userId: %v", err, userId)
}
// Check that it won't return if the wrong password is given
if userId, err := s.GetUserId(email, password+auth.Password("_wrong")); err != ErrWrongCredentials || userId != 0 {
t.Fatalf(`GetUserId error for wrong password: wanted "%+v", got "%+v. userId: %v"`, ErrWrongCredentials, err, userId)
}
}
// Test GetUserId for existing but unverified account
func TestStoreGetUserIdAccountUnverified(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
verifyToken := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
_, email, password, _ := makeTestUser(t, &s, &verifyToken, &time.Time{})
// Check that it won't return if the account is unverified
if userId, err := s.GetUserId(email, password); err != ErrNotVerified || userId != 0 {
t.Fatalf(`GetUserId error for unverified account: wanted "%+v", got "%+v. userId: %v"`, ErrNotVerified, err, userId)
}
}
func TestStoreAccountEmptyFields(t *testing.T) {
// Make sure expiration doesn't get set if sanitization fails
tt := []struct {
name string
email auth.Email
clientSaltSeed auth.ClientSaltSeed
password auth.Password
}{
{
name: "missing email",
email: "",
clientSaltSeed: "abcd1234abcd1234",
password: "xyz",
},
{
name: "missing client salt seed",
email: "a@example.com",
clientSaltSeed: "",
password: "xyz",
},
// Not testing empty key and salt because they get generated to something
// non-empty in the method, even if email is empty
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
var sqliteErr sqlite3.Error
err := s.CreateAccount(tc.email, tc.password, tc.clientSaltSeed, nil)
if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintCheck) {
return // We got the error we expected
}
}
t.Errorf("Expected check constraint error for empty field. Got %+v", err)
})
}
}
// Test GetClientSaltSeed for existing account
func TestStoreGetClientSaltSeedAccountExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
_, email, _, createdSeed := makeTestUser(t, &s, nil, nil)
// Check that the seed is correct for the email, irrespective of the case of
// the characters in the email.
lowerEmail := auth.Email(strings.ToLower(string(email)))
upperEmail := auth.Email(strings.ToUpper(string(email)))
if seed, err := s.GetClientSaltSeed(lowerEmail); err != nil || seed != createdSeed {
t.Fatalf("Unexpected error in GetClientSaltSeed: err: %+v seed: %v", err, seed)
}
if seed, err := s.GetClientSaltSeed(upperEmail); err != nil || seed != createdSeed {
t.Fatalf("Unexpected error in GetClientSaltSeed: err: %+v seed: %v", err, seed)
}
}
// Test GetClientSaltSeed for nonexisting email
func TestStoreGetClientSaltSeedAccountNotExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email := auth.Email("abc@example.com")
if seed, err := s.GetClientSaltSeed(email); err != ErrWrongCredentials || seed != "" {
t.Fatalf(`GetClientSaltSeed error for nonexistant account: wanted "%+v", got "%+v. seed: %v"`, ErrWrongCredentials, err, seed)
}
}
// Test UpdateVerifyTokenString for existing account
func TestUpdateVerifyTokenStringSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
verifyTokenString1 := auth.VerifyTokenString("00000000000000000000000000000000")
time1 := time.Time{}
_, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString1, &time1)
// we're not testing normalization features so we'll just use this here
normEmail := email.Normalize()
// Check that the token updates for the email, irrespective of the case of
// the characters in the email.
lowerEmail := auth.Email(strings.ToLower(string(email)))
upperEmail := auth.Email(strings.ToUpper(string(email)))
verifyTokenString2 := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
verifyTokenString3 := auth.VerifyTokenString("ef095678ef095678ef095678ef095678")
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
if err := s.UpdateVerifyTokenString(lowerEmail, verifyTokenString2); err != nil {
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
}
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString2, &approxVerifyExpiration)
if err := s.UpdateVerifyTokenString(upperEmail, verifyTokenString3); err != nil {
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
}
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString3, &approxVerifyExpiration)
}
// Test UpdateVerifyTokenString for nonexisting email
func TestStoreUpdateVerifyTokenStringAccountNotExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
email := auth.Email("abc@example.com")
if err := s.UpdateVerifyTokenString(email, "abcd1234abcd1234abcd1234abcd1234"); err != ErrWrongCredentials {
t.Fatalf(`UpdateVerifyTokenString error for nonexistant account: wanted "%+v", got "%+v."`, ErrWrongCredentials, err)
}
}
// Test UpdateVerifyTokenString for already verified account
func TestStoreUpdateVerifyTokenStringAccountVerified(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
_, email, _, _ := makeTestUser(t, &s, nil, nil)
if err := s.UpdateVerifyTokenString(email, "abcd1234abcd1234abcd1234abcd1234"); err != ErrNoTokenForUser {
t.Fatalf(`UpdateVerifyTokenString error for already verified account: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
}
}
// Test VerifyAccount for existing account
func TestUpdateVerifyAccountSuccess(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
time1 := time.Time{}
_, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString, &time1)
// we're not testing normalization features so we'll just use this here
normEmail := email.Normalize()
if err := s.VerifyAccount(verifyTokenString); err != nil {
t.Fatalf("Unexpected error in VerifyAccount: err: %+v", err)
}
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, nil, nil)
}
// Test VerifyAccount for nonexisting token
func TestStoreVerifyAccountTokenNotExists(t *testing.T) {
s, sqliteTmpFile := StoreTestInit(t)
defer StoreTestCleanup(sqliteTmpFile)
if err := s.VerifyAccount("abcd1234abcd1234abcd1234abcd1234"); err != ErrNoTokenForUser {
t.Fatalf(`VerifyAccount error for nonexistant token: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
}
}