chatdesk-ui/auth_v2.169.0/internal/models/user_test.go

468 lines
14 KiB
Go

package models
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/storage/test"
"golang.org/x/crypto/bcrypt"
)
const modelsTestConfig = "../../hack/test.env"
func init() {
crypto.PasswordHashCost = crypto.QuickHashCost
}
type UserTestSuite struct {
suite.Suite
db *storage.Connection
}
func (ts *UserTestSuite) SetupTest() {
TruncateAll(ts.db)
}
func TestUser(t *testing.T) {
globalConfig, err := conf.LoadGlobal(modelsTestConfig)
require.NoError(t, err)
conn, err := test.SetupDBConnection(globalConfig)
require.NoError(t, err)
ts := &UserTestSuite{
db: conn,
}
defer ts.db.Close()
suite.Run(t, ts)
}
func (ts *UserTestSuite) TestUpdateAppMetadata() {
u, err := NewUser("", "", "", "", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), u.UpdateAppMetaData(ts.db, make(map[string]interface{})))
require.NotNil(ts.T(), u.AppMetaData)
require.NoError(ts.T(), u.UpdateAppMetaData(ts.db, map[string]interface{}{
"foo": "bar",
}))
require.Equal(ts.T(), "bar", u.AppMetaData["foo"])
require.NoError(ts.T(), u.UpdateAppMetaData(ts.db, map[string]interface{}{
"foo": nil,
}))
require.Len(ts.T(), u.AppMetaData, 0)
require.Equal(ts.T(), nil, u.AppMetaData["foo"])
}
func (ts *UserTestSuite) TestUpdateUserMetadata() {
u, err := NewUser("", "", "", "", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), u.UpdateUserMetaData(ts.db, make(map[string]interface{})))
require.NotNil(ts.T(), u.UserMetaData)
require.NoError(ts.T(), u.UpdateUserMetaData(ts.db, map[string]interface{}{
"foo": "bar",
}))
require.Equal(ts.T(), "bar", u.UserMetaData["foo"])
require.NoError(ts.T(), u.UpdateUserMetaData(ts.db, map[string]interface{}{
"foo": nil,
}))
require.Len(ts.T(), u.UserMetaData, 0)
require.Equal(ts.T(), nil, u.UserMetaData["foo"])
}
func (ts *UserTestSuite) TestFindUserByConfirmationToken() {
u := ts.createUser()
tokenHash := "test_confirmation_token"
require.NoError(ts.T(), CreateOneTimeToken(ts.db, u.ID, "relates_to not used", tokenHash, ConfirmationToken))
n, err := FindUserByConfirmationToken(ts.db, tokenHash)
require.NoError(ts.T(), err)
require.Equal(ts.T(), u.ID, n.ID)
}
func (ts *UserTestSuite) TestFindUserByEmailAndAudience() {
u := ts.createUser()
n, err := FindUserByEmailAndAudience(ts.db, u.GetEmail(), "test")
require.NoError(ts.T(), err)
require.Equal(ts.T(), u.ID, n.ID)
_, err = FindUserByEmailAndAudience(ts.db, u.GetEmail(), "invalid")
require.EqualError(ts.T(), err, UserNotFoundError{}.Error())
}
func (ts *UserTestSuite) TestFindUsersInAudience() {
u := ts.createUser()
n, err := FindUsersInAudience(ts.db, u.Aud, nil, nil, "")
require.NoError(ts.T(), err)
require.Len(ts.T(), n, 1)
p := Pagination{
Page: 1,
PerPage: 50,
}
n, err = FindUsersInAudience(ts.db, u.Aud, &p, nil, "")
require.NoError(ts.T(), err)
require.Len(ts.T(), n, 1)
assert.Equal(ts.T(), uint64(1), p.Count)
sp := &SortParams{
Fields: []SortField{
{Name: "created_at", Dir: Descending},
},
}
n, err = FindUsersInAudience(ts.db, u.Aud, nil, sp, "")
require.NoError(ts.T(), err)
require.Len(ts.T(), n, 1)
}
func (ts *UserTestSuite) TestFindUserByID() {
u := ts.createUser()
n, err := FindUserByID(ts.db, u.ID)
require.NoError(ts.T(), err)
require.Equal(ts.T(), u.ID, n.ID)
}
func (ts *UserTestSuite) TestFindUserByRecoveryToken() {
u := ts.createUser()
tokenHash := "test_recovery_token"
require.NoError(ts.T(), CreateOneTimeToken(ts.db, u.ID, "relates_to not used", tokenHash, RecoveryToken))
n, err := FindUserByRecoveryToken(ts.db, tokenHash)
require.NoError(ts.T(), err)
require.Equal(ts.T(), u.ID, n.ID)
}
func (ts *UserTestSuite) TestFindUserWithRefreshToken() {
u := ts.createUser()
r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
require.NoError(ts.T(), err)
n, nr, s, err := FindUserWithRefreshToken(ts.db, r.Token, true /* forUpdate */)
require.NoError(ts.T(), err)
require.Equal(ts.T(), r.ID, nr.ID)
require.Equal(ts.T(), u.ID, n.ID)
require.NotNil(ts.T(), s)
require.Equal(ts.T(), *r.SessionId, s.ID)
}
func (ts *UserTestSuite) TestIsDuplicatedEmail() {
_ = ts.createUserWithEmail("david.calavera@netlify.com")
e, err := IsDuplicatedEmail(ts.db, "david.calavera@netlify.com", "test", nil)
require.NoError(ts.T(), err)
require.NotNil(ts.T(), e, "expected email to be duplicated")
e, err = IsDuplicatedEmail(ts.db, "davidcalavera@netlify.com", "test", nil)
require.NoError(ts.T(), err)
require.Nil(ts.T(), e, "expected email to not be duplicated", nil)
e, err = IsDuplicatedEmail(ts.db, "david@netlify.com", "test", nil)
require.NoError(ts.T(), err)
require.Nil(ts.T(), e, "expected same email to not be duplicated", nil)
e, err = IsDuplicatedEmail(ts.db, "david.calavera@netlify.com", "other-aud", nil)
require.NoError(ts.T(), err)
require.Nil(ts.T(), e, "expected same email to not be duplicated")
}
func (ts *UserTestSuite) createUser() *User {
return ts.createUserWithEmail("david@netlify.com")
}
func (ts *UserTestSuite) createUserWithEmail(email string) *User {
user, err := NewUser("", email, "secret", "test", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(user))
identity, err := NewIdentity(user, "email", map[string]interface{}{
"sub": user.ID.String(),
"email": email,
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(identity))
return user
}
func (ts *UserTestSuite) TestRemoveUnconfirmedIdentities() {
user, err := NewUser("+29382983298", "someone@example.com", "abcdefgh", "authenticated", nil)
require.NoError(ts.T(), err)
user.AppMetaData = map[string]interface{}{
"provider": "email",
"providers": []string{"email", "phone", "twitter"},
}
require.NoError(ts.T(), ts.db.Create(user))
idEmail, err := NewIdentity(user, "email", map[string]interface{}{
"sub": "someone@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(idEmail))
idPhone, err := NewIdentity(user, "phone", map[string]interface{}{
"sub": "+29382983298",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(idPhone))
idTwitter, err := NewIdentity(user, "twitter", map[string]interface{}{
"sub": "test_twitter_user_id",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(idTwitter))
user.Identities = append(user.Identities, *idEmail, *idPhone, *idTwitter)
// reload the user
require.NoError(ts.T(), ts.db.Load(user))
require.False(ts.T(), user.IsConfirmed(), "user's email must not be confirmed")
require.NoError(ts.T(), user.RemoveUnconfirmedIdentities(ts.db, idTwitter))
// reload the user to check that identities are deleted from the db too
require.NoError(ts.T(), ts.db.Load(user))
require.Empty(ts.T(), user.EncryptedPassword, "password still remains in user")
require.Len(ts.T(), user.Identities, 1, "only one identity must be remaining")
require.Equal(ts.T(), idTwitter.ID, user.Identities[0].ID, "remaining identity is not the expected one")
require.NotNil(ts.T(), user.AppMetaData)
require.Equal(ts.T(), user.AppMetaData["provider"], "twitter")
require.Equal(ts.T(), user.AppMetaData["providers"], []string{"twitter"})
}
func (ts *UserTestSuite) TestConfirmEmailChange() {
user, err := NewUser("", "test@example.com", "", "authenticated", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(user))
identity, err := NewIdentity(user, "email", map[string]interface{}{
"sub": user.ID.String(),
"email": "test@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(identity))
user.EmailChange = "new@example.com"
require.NoError(ts.T(), ts.db.UpdateOnly(user, "email_change"))
require.NoError(ts.T(), user.ConfirmEmailChange(ts.db, 0))
require.NoError(ts.T(), ts.db.Eager().Load(user))
identity, err = FindIdentityByIdAndProvider(ts.db, user.ID.String(), "email")
require.NoError(ts.T(), err)
require.Equal(ts.T(), user.Email, storage.NullString("new@example.com"))
require.Equal(ts.T(), user.EmailChange, "")
require.NotNil(ts.T(), identity.IdentityData)
require.Equal(ts.T(), identity.IdentityData["email"], "new@example.com")
}
func (ts *UserTestSuite) TestConfirmPhoneChange() {
user, err := NewUser("123456789", "", "", "authenticated", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(user))
identity, err := NewIdentity(user, "phone", map[string]interface{}{
"sub": user.ID.String(),
"phone": "123456789",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(identity))
user.PhoneChange = "987654321"
require.NoError(ts.T(), ts.db.UpdateOnly(user, "phone_change"))
require.NoError(ts.T(), user.ConfirmPhoneChange(ts.db))
require.NoError(ts.T(), ts.db.Eager().Load(user))
identity, err = FindIdentityByIdAndProvider(ts.db, user.ID.String(), "phone")
require.NoError(ts.T(), err)
require.Equal(ts.T(), user.Phone, storage.NullString("987654321"))
require.Equal(ts.T(), user.PhoneChange, "")
require.NotNil(ts.T(), identity.IdentityData)
require.Equal(ts.T(), identity.IdentityData["phone"], "987654321")
}
func (ts *UserTestSuite) TestUpdateUserEmailSuccess() {
userA, err := NewUser("", "foo@example.com", "", "authenticated", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(userA))
primaryIdentity, err := NewIdentity(userA, "email", map[string]interface{}{
"sub": userA.ID.String(),
"email": "foo@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(primaryIdentity))
secondaryIdentity, err := NewIdentity(userA, "google", map[string]interface{}{
"sub": userA.ID.String(),
"email": "bar@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(secondaryIdentity))
// UpdateUserEmail should not do anything and the user's email should still use the primaryIdentity
require.NoError(ts.T(), userA.UpdateUserEmailFromIdentities(ts.db))
require.Equal(ts.T(), primaryIdentity.GetEmail(), userA.GetEmail())
// remove primary identity
require.NoError(ts.T(), ts.db.Destroy(primaryIdentity))
// UpdateUserEmail should update the user to use the secondary identity's email
require.NoError(ts.T(), userA.UpdateUserEmailFromIdentities(ts.db))
require.Equal(ts.T(), secondaryIdentity.GetEmail(), userA.GetEmail())
}
func (ts *UserTestSuite) TestUpdateUserEmailFailure() {
userA, err := NewUser("", "foo@example.com", "", "authenticated", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(userA))
primaryIdentity, err := NewIdentity(userA, "email", map[string]interface{}{
"sub": userA.ID.String(),
"email": "foo@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(primaryIdentity))
secondaryIdentity, err := NewIdentity(userA, "google", map[string]interface{}{
"sub": userA.ID.String(),
"email": "bar@example.com",
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(secondaryIdentity))
userB, err := NewUser("", "bar@example.com", "", "authenticated", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(userB))
// remove primary identity
require.NoError(ts.T(), ts.db.Destroy(primaryIdentity))
// UpdateUserEmail should fail with the email unique constraint violation error
// since userB is using the secondary identity's email
require.ErrorIs(ts.T(), userA.UpdateUserEmailFromIdentities(ts.db), UserEmailUniqueConflictError{})
require.Equal(ts.T(), primaryIdentity.GetEmail(), userA.GetEmail())
}
func (ts *UserTestSuite) TestNewUserWithPasswordHashSuccess() {
cases := []struct {
desc string
hash string
}{
{
desc: "Valid bcrypt hash",
hash: "$2y$10$SXEz2HeT8PUIGQXo9yeUIem8KzNxgG0d7o/.eGj2rj8KbRgAuRVlq",
},
{
desc: "Valid argon2i hash",
hash: "$argon2i$v=19$m=16,t=2,p=1$bGJRWThNOHJJTVBSdHl2dQ$NfEnUOuUpb7F2fQkgFUG4g",
},
{
desc: "Valid argon2id hash",
hash: "$argon2id$v=19$m=32,t=3,p=2$SFVpOWJ0eXhjRzVkdGN1RQ$RXnb8rh7LaDcn07xsssqqulZYXOM/EUCEFMVcAcyYVk",
},
{
desc: "Valid Firebase scrypt hash",
hash: "$fbscrypt$v=1,n=14,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$C0sHCg9ek77hsg==$ZGlmZmVyZW50aGFzaA==",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
u, err := NewUserWithPasswordHash("", "", c.hash, "", nil)
require.NoError(ts.T(), err)
require.NotNil(ts.T(), u)
})
}
}
func (ts *UserTestSuite) TestNewUserWithPasswordHashFailure() {
cases := []struct {
desc string
hash string
}{
{
desc: "Invalid argon2i hash",
hash: "$argon2id$test",
},
{
desc: "Invalid bcrypt hash",
hash: "plaintest_password",
},
{
desc: "Invalid scrypt hash",
hash: "$fbscrypt$invalid",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
u, err := NewUserWithPasswordHash("", "", c.hash, "", nil)
require.Error(ts.T(), err)
require.Nil(ts.T(), u)
})
}
}
func (ts *UserTestSuite) TestAuthenticate() {
// every case uses "test" as the password
cases := []struct {
desc string
hash string
expectedHashCost int
}{
{
desc: "Invalid bcrypt hash cost of 11",
hash: "$2y$11$4lH57PU7bGATpRcx93vIoObH3qDmft/pytbOzDG9/1WsyNmN5u4di",
expectedHashCost: bcrypt.MinCost,
},
{
desc: "Valid bcrypt hash cost of 10",
hash: "$2y$10$va66S4MxFrH6G6L7BzYl0.QgcYgvSr/F92gc.3botlz7bG4p/g/1i",
expectedHashCost: bcrypt.DefaultCost,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
u, err := NewUserWithPasswordHash("", "", c.hash, "", nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.db.Create(u))
require.NotNil(ts.T(), u)
isAuthenticated, _, err := u.Authenticate(context.Background(), ts.db, "test", nil, false, "")
require.NoError(ts.T(), err)
require.True(ts.T(), isAuthenticated)
// check hash cost
hashCost, err := bcrypt.Cost([]byte(*u.EncryptedPassword))
require.NoError(ts.T(), err)
require.Equal(ts.T(), c.expectedHashCost, hashCost)
})
}
}