5cfa7ca460
There are multiple entries in the users table with the same email address. This is because in the past users were able to register multiple times if the email was not verified. This is no longer the case. If a user tries to register with an unverified email already in the DB, we send a verification email instead of creating another entry. However, since these old entries in the table with duplicate emails were never cleaned up, the email reminder chore will send out email verification reminders to them. A single person will get one separate email per entry in the DB with their email and where status = 0. Since the multiple entries with the same email problem was solved a while ago, just add a constraint to GetUnverifiedNeedingReminder to only select users created after a cutoff. Once the DB is migrated to remove the duplicate emails, we can remove the cutoff. github issue: https://github.com/storj/storj/issues/4853 Change-Id: I07d77d43109bcacc8909df61d4fb49165a99527c
405 lines
13 KiB
Go
405 lines
13 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package console_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/memory"
|
|
"storj.io/common/testcontext"
|
|
"storj.io/common/testrand"
|
|
"storj.io/storj/private/testplanet"
|
|
"storj.io/storj/satellite"
|
|
"storj.io/storj/satellite/console"
|
|
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
|
)
|
|
|
|
const (
|
|
lastName = "lastName"
|
|
email = "email@mail.test"
|
|
passValid = "123456"
|
|
name = "name"
|
|
newName = "newName"
|
|
newLastName = "newLastName"
|
|
newEmail = "newEmail@mail.test"
|
|
newPass = "newPass1234567890123456789012345"
|
|
position = "position"
|
|
companyName = "companyName"
|
|
employeeCount = "0"
|
|
workingOn = "workingOn"
|
|
isProfessional = true
|
|
mfaSecretKey = "mfaSecretKey"
|
|
signupPromoCode = "STORJ50"
|
|
)
|
|
|
|
func TestUserRepository(t *testing.T) {
|
|
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
|
repository := db.Console().Users()
|
|
partnerID := testrand.UUID()
|
|
|
|
// Test with and without partnerID
|
|
user := &console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: name,
|
|
ShortName: lastName,
|
|
Email: email,
|
|
PartnerID: partnerID,
|
|
PasswordHash: []byte(passValid),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
testUsers(ctx, t, repository, user)
|
|
|
|
user = &console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: name,
|
|
ShortName: lastName,
|
|
Email: email,
|
|
PasswordHash: []byte(passValid),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
testUsers(ctx, t, repository, user)
|
|
|
|
// test professional user
|
|
user = &console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: name,
|
|
ShortName: lastName,
|
|
Email: email,
|
|
PasswordHash: []byte(passValid),
|
|
CreatedAt: time.Now(),
|
|
IsProfessional: isProfessional,
|
|
Position: position,
|
|
CompanyName: companyName,
|
|
EmployeeCount: employeeCount,
|
|
WorkingOn: workingOn,
|
|
SignupPromoCode: signupPromoCode,
|
|
}
|
|
testUsers(ctx, t, repository, user)
|
|
})
|
|
}
|
|
|
|
func TestUserEmailCase(t *testing.T) {
|
|
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
|
for _, testCase := range []struct {
|
|
email string
|
|
}{
|
|
{email: "prettyandsimple@example.com"},
|
|
{email: "firstname.lastname@domain.com "},
|
|
{email: "email@subdomain.domain.com "},
|
|
{email: "firstname+lastname@domain.com "},
|
|
{email: "email@[123.123.123.123] "},
|
|
{email: "\"email\"@domain.com"},
|
|
{email: "_______@domain.com "},
|
|
} {
|
|
newUser := &console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: newName,
|
|
ShortName: newLastName,
|
|
Email: testCase.email,
|
|
Status: console.Active,
|
|
PasswordHash: []byte(newPass),
|
|
}
|
|
|
|
createdUser, err := db.Console().Users().Insert(ctx, newUser)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, testCase.email, createdUser.Email)
|
|
|
|
createdUser.Status = console.Active
|
|
|
|
err = db.Console().Users().Update(ctx, createdUser)
|
|
assert.NoError(t, err)
|
|
|
|
retrievedUser, err := db.Console().Users().GetByEmail(ctx, testCase.email)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, testCase.email, retrievedUser.Email)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserUpdatePaidTier(t *testing.T) {
|
|
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
|
email := "testemail@mail.test"
|
|
fullName := "first name last name"
|
|
shortName := "short name"
|
|
password := "password"
|
|
projectBandwidthLimit := memory.Size(50000000000)
|
|
storageStorageLimit := memory.Size(50000000000)
|
|
projectLimit := 3
|
|
segmentLimit := int64(100)
|
|
newUser := &console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: fullName,
|
|
ShortName: shortName,
|
|
Email: email,
|
|
Status: console.Active,
|
|
PasswordHash: []byte(password),
|
|
}
|
|
|
|
createdUser, err := db.Console().Users().Insert(ctx, newUser)
|
|
require.NoError(t, err)
|
|
require.Equal(t, email, createdUser.Email)
|
|
require.Equal(t, fullName, createdUser.FullName)
|
|
require.Equal(t, shortName, createdUser.ShortName)
|
|
require.False(t, createdUser.PaidTier)
|
|
|
|
err = db.Console().Users().UpdatePaidTier(ctx, createdUser.ID, true, projectBandwidthLimit, storageStorageLimit, segmentLimit, projectLimit)
|
|
require.NoError(t, err)
|
|
|
|
retrievedUser, err := db.Console().Users().Get(ctx, createdUser.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, email, retrievedUser.Email)
|
|
require.Equal(t, fullName, retrievedUser.FullName)
|
|
require.Equal(t, shortName, retrievedUser.ShortName)
|
|
require.True(t, retrievedUser.PaidTier)
|
|
|
|
err = db.Console().Users().UpdatePaidTier(ctx, createdUser.ID, false, projectBandwidthLimit, storageStorageLimit, segmentLimit, projectLimit)
|
|
require.NoError(t, err)
|
|
|
|
retrievedUser, err = db.Console().Users().Get(ctx, createdUser.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, retrievedUser.PaidTier)
|
|
})
|
|
}
|
|
|
|
func testUsers(ctx context.Context, t *testing.T, repository console.Users, user *console.User) {
|
|
|
|
t.Run("User insertion success", func(t *testing.T) {
|
|
|
|
insertedUser, err := repository.Insert(ctx, user)
|
|
assert.NoError(t, err)
|
|
|
|
insertedUser.Status = console.Active
|
|
|
|
err = repository.Update(ctx, insertedUser)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Get user success", func(t *testing.T) {
|
|
userByEmail, err := repository.GetByEmail(ctx, email)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, name, userByEmail.FullName)
|
|
assert.Equal(t, lastName, userByEmail.ShortName)
|
|
assert.Equal(t, user.PartnerID, userByEmail.PartnerID)
|
|
assert.Equal(t, user.SignupPromoCode, userByEmail.SignupPromoCode)
|
|
assert.False(t, user.PaidTier)
|
|
assert.False(t, user.MFAEnabled)
|
|
assert.Empty(t, user.MFASecretKey)
|
|
assert.Empty(t, user.MFARecoveryCodes)
|
|
assert.Empty(t, user.LastVerificationReminder)
|
|
|
|
if user.IsProfessional {
|
|
assert.Equal(t, workingOn, userByEmail.WorkingOn)
|
|
assert.Equal(t, position, userByEmail.Position)
|
|
assert.Equal(t, companyName, userByEmail.CompanyName)
|
|
assert.Equal(t, employeeCount, userByEmail.EmployeeCount)
|
|
} else {
|
|
assert.Equal(t, "", userByEmail.WorkingOn)
|
|
assert.Equal(t, "", userByEmail.Position)
|
|
assert.Equal(t, "", userByEmail.CompanyName)
|
|
assert.Equal(t, "", userByEmail.EmployeeCount)
|
|
}
|
|
|
|
userByID, err := repository.Get(ctx, userByEmail.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, name, userByID.FullName)
|
|
assert.Equal(t, lastName, userByID.ShortName)
|
|
assert.Equal(t, user.PartnerID, userByID.PartnerID)
|
|
assert.Equal(t, user.SignupPromoCode, userByID.SignupPromoCode)
|
|
assert.False(t, user.MFAEnabled)
|
|
assert.Empty(t, user.MFASecretKey)
|
|
assert.Empty(t, user.MFARecoveryCodes)
|
|
assert.Empty(t, user.LastVerificationReminder)
|
|
|
|
if user.IsProfessional {
|
|
assert.Equal(t, workingOn, userByID.WorkingOn)
|
|
assert.Equal(t, position, userByID.Position)
|
|
assert.Equal(t, companyName, userByID.CompanyName)
|
|
assert.Equal(t, employeeCount, userByID.EmployeeCount)
|
|
} else {
|
|
assert.Equal(t, "", userByID.WorkingOn)
|
|
assert.Equal(t, "", userByID.Position)
|
|
assert.Equal(t, "", userByID.CompanyName)
|
|
assert.Equal(t, "", userByID.EmployeeCount)
|
|
}
|
|
|
|
assert.Equal(t, userByID.ID, userByEmail.ID)
|
|
assert.Equal(t, userByID.FullName, userByEmail.FullName)
|
|
assert.Equal(t, userByID.ShortName, userByEmail.ShortName)
|
|
assert.Equal(t, userByID.Email, userByEmail.Email)
|
|
assert.Equal(t, userByID.PasswordHash, userByEmail.PasswordHash)
|
|
assert.Equal(t, userByID.PartnerID, userByEmail.PartnerID)
|
|
assert.Equal(t, userByID.CreatedAt, userByEmail.CreatedAt)
|
|
assert.Equal(t, userByID.IsProfessional, userByEmail.IsProfessional)
|
|
assert.Equal(t, userByID.WorkingOn, userByEmail.WorkingOn)
|
|
assert.Equal(t, userByID.Position, userByEmail.Position)
|
|
assert.Equal(t, userByID.CompanyName, userByEmail.CompanyName)
|
|
assert.Equal(t, userByID.EmployeeCount, userByEmail.EmployeeCount)
|
|
assert.Equal(t, userByID.SignupPromoCode, userByEmail.SignupPromoCode)
|
|
})
|
|
|
|
t.Run("Update user success", func(t *testing.T) {
|
|
oldUser, err := repository.GetByEmail(ctx, email)
|
|
assert.NoError(t, err)
|
|
|
|
d := (60 * time.Second)
|
|
date := time.Now().Add(-24 * 365 * time.Hour).Truncate(d)
|
|
|
|
newUserInfo := &console.User{
|
|
ID: oldUser.ID,
|
|
FullName: newName,
|
|
ShortName: newLastName,
|
|
Email: newEmail,
|
|
Status: console.Active,
|
|
PaidTier: true,
|
|
MFAEnabled: true,
|
|
MFASecretKey: mfaSecretKey,
|
|
MFARecoveryCodes: []string{"1", "2"},
|
|
PasswordHash: []byte(newPass),
|
|
LastVerificationReminder: date,
|
|
}
|
|
|
|
err = repository.Update(ctx, newUserInfo)
|
|
assert.NoError(t, err)
|
|
|
|
newUser, err := repository.Get(ctx, oldUser.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, oldUser.ID, newUser.ID)
|
|
assert.Equal(t, newName, newUser.FullName)
|
|
assert.Equal(t, newLastName, newUser.ShortName)
|
|
assert.Equal(t, newEmail, newUser.Email)
|
|
assert.Equal(t, []byte(newPass), newUser.PasswordHash)
|
|
assert.True(t, newUser.PaidTier)
|
|
assert.True(t, newUser.MFAEnabled)
|
|
assert.Equal(t, mfaSecretKey, newUser.MFASecretKey)
|
|
assert.Equal(t, newUserInfo.MFARecoveryCodes, newUser.MFARecoveryCodes)
|
|
assert.Equal(t, newUserInfo.LastVerificationReminder, newUser.LastVerificationReminder)
|
|
// PartnerID should not change
|
|
assert.Equal(t, user.PartnerID, newUser.PartnerID)
|
|
assert.Equal(t, oldUser.CreatedAt, newUser.CreatedAt)
|
|
})
|
|
|
|
t.Run("Delete user success", func(t *testing.T) {
|
|
oldUser, err := repository.GetByEmail(ctx, newEmail)
|
|
assert.NoError(t, err)
|
|
|
|
err = repository.Delete(ctx, oldUser.ID)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = repository.Get(ctx, oldUser.ID)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestGetUserByEmail(t *testing.T) {
|
|
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
|
|
usersRepo := db.Console().Users()
|
|
email := "test@mail.test"
|
|
|
|
inactiveUser := console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: "Inactive User",
|
|
Email: email,
|
|
PasswordHash: []byte("123a123"),
|
|
}
|
|
|
|
_, err := usersRepo.Insert(ctx, &inactiveUser)
|
|
require.NoError(t, err)
|
|
|
|
_, err = usersRepo.GetByEmail(ctx, email)
|
|
require.ErrorIs(t, sql.ErrNoRows, err)
|
|
|
|
verified, unverified, err := usersRepo.GetByEmailWithUnverified(ctx, email)
|
|
require.NoError(t, err)
|
|
require.Nil(t, verified)
|
|
require.Equal(t, inactiveUser.ID, unverified[0].ID)
|
|
|
|
activeUser := console.User{
|
|
ID: testrand.UUID(),
|
|
FullName: "Active User",
|
|
Email: email,
|
|
Status: console.Active,
|
|
PasswordHash: []byte("123a123"),
|
|
}
|
|
|
|
_, err = usersRepo.Insert(ctx, &activeUser)
|
|
require.NoError(t, err)
|
|
|
|
// Required to set the active status.
|
|
err = usersRepo.Update(ctx, &activeUser)
|
|
require.NoError(t, err)
|
|
|
|
dbUser, err := usersRepo.GetByEmail(ctx, email)
|
|
require.NoError(t, err)
|
|
require.Equal(t, activeUser.ID, dbUser.ID)
|
|
})
|
|
}
|
|
|
|
func TestGetUnverifiedNeedingReminder(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.EmailReminders.FirstVerificationReminder = 24 * time.Hour
|
|
config.EmailReminders.SecondVerificationReminder = 120 * time.Hour
|
|
},
|
|
},
|
|
SatelliteCount: 1,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
var sentFirstReminder bool
|
|
var sentSecondReminder bool
|
|
|
|
config := planet.Satellites[0].Config.EmailReminders
|
|
db := planet.Satellites[0].DB.Console().Users()
|
|
|
|
id := testrand.UUID()
|
|
_, err := db.Insert(ctx, &console.User{
|
|
ID: id,
|
|
FullName: "unverified user one",
|
|
Email: "userone@mail.test",
|
|
PasswordHash: []byte("123a123"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
|
|
// We expect two reminders in total - one after a day of account creation,
|
|
// and one after five. This test will check to ensure that both reminders occur.
|
|
// Each iteration advances time by i*24 hours from `now`.
|
|
for i := 0; i <= 6; i++ {
|
|
u, err := db.Get(ctx, id)
|
|
require.NoError(t, err)
|
|
|
|
// Intuitively it would be better to test this by setting `created_at` to some point in the past.
|
|
// Since we have no control over `created_at` (it's autoinserted) we will instead pass in a future time
|
|
// as the `now` argument to `GetUnverifiedNeedingReminder`
|
|
futureTime := now.Add(time.Duration(i*24) * time.Hour)
|
|
needReminder, err := db.GetUnverifiedNeedingReminder(ctx, futureTime.Add(-config.FirstVerificationReminder), futureTime.Add(-config.SecondVerificationReminder), now.Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
// These are the conditions in the SQL query which selects users needing reminder
|
|
if u.VerificationReminders == 0 && u.CreatedAt.Before(futureTime.Add(-config.FirstVerificationReminder)) {
|
|
require.NotEmpty(t, needReminder)
|
|
require.Equal(t, u.ID, needReminder[0].ID)
|
|
require.NoError(t, db.UpdateVerificationReminders(ctx, u.ID))
|
|
sentFirstReminder = true
|
|
} else if u.VerificationReminders == 1 && u.CreatedAt.Before(futureTime.Add(-config.SecondVerificationReminder)) {
|
|
require.NotEmpty(t, needReminder)
|
|
require.Equal(t, u.ID, needReminder[0].ID)
|
|
require.NoError(t, db.UpdateVerificationReminders(ctx, u.ID))
|
|
sentSecondReminder = true
|
|
} else {
|
|
require.Empty(t, needReminder)
|
|
}
|
|
}
|
|
require.True(t, sentFirstReminder)
|
|
require.True(t, sentSecondReminder)
|
|
})
|
|
}
|