rwadurian/backend/mpc-system/tests/unit/account/domain/account_test.go

415 lines
13 KiB
Go

package domain_test
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/rwadurian/mpc-system/services/account/domain/entities"
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
)
func TestNewAccount(t *testing.T) {
t.Run("should create account with valid data", func(t *testing.T) {
publicKey := []byte("test-public-key")
keygenSessionID := uuid.New()
account := entities.NewAccount(
"testuser",
"test@example.com",
publicKey,
keygenSessionID,
3, // thresholdN
2, // thresholdT
)
assert.NotNil(t, account)
assert.False(t, account.ID.IsZero())
assert.Equal(t, "testuser", account.Username)
assert.Equal(t, "test@example.com", account.Email)
assert.Equal(t, publicKey, account.PublicKey)
assert.Equal(t, keygenSessionID, account.KeygenSessionID)
assert.Equal(t, 3, account.ThresholdN)
assert.Equal(t, 2, account.ThresholdT)
assert.Equal(t, value_objects.AccountStatusActive, account.Status)
assert.True(t, account.CreatedAt.Before(time.Now().Add(time.Second)))
})
}
func TestAccount_SetPhone(t *testing.T) {
t.Run("should set phone number", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.SetPhone("+1234567890")
assert.NotNil(t, account.Phone)
assert.Equal(t, "+1234567890", *account.Phone)
})
}
func TestAccount_UpdateLastLogin(t *testing.T) {
t.Run("should update last login timestamp", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
assert.Nil(t, account.LastLoginAt)
account.UpdateLastLogin()
assert.NotNil(t, account.LastLoginAt)
assert.True(t, account.LastLoginAt.After(account.CreatedAt.Add(-time.Second)))
})
}
func TestAccount_Suspend(t *testing.T) {
t.Run("should suspend active account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
err := account.Suspend()
require.NoError(t, err)
assert.Equal(t, value_objects.AccountStatusSuspended, account.Status)
})
t.Run("should fail to suspend recovering account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusRecovering
err := account.Suspend()
assert.Error(t, err)
assert.Equal(t, entities.ErrAccountInRecovery, err)
})
}
func TestAccount_Lock(t *testing.T) {
t.Run("should lock active account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
err := account.Lock()
require.NoError(t, err)
assert.Equal(t, value_objects.AccountStatusLocked, account.Status)
})
t.Run("should fail to lock recovering account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusRecovering
err := account.Lock()
assert.Error(t, err)
})
}
func TestAccount_Activate(t *testing.T) {
t.Run("should activate suspended account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusSuspended
account.Activate()
assert.Equal(t, value_objects.AccountStatusActive, account.Status)
})
}
func TestAccount_StartRecovery(t *testing.T) {
t.Run("should start recovery for active account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
err := account.StartRecovery()
require.NoError(t, err)
assert.Equal(t, value_objects.AccountStatusRecovering, account.Status)
})
t.Run("should start recovery for locked account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusLocked
err := account.StartRecovery()
require.NoError(t, err)
assert.Equal(t, value_objects.AccountStatusRecovering, account.Status)
})
t.Run("should fail to start recovery for suspended account", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusSuspended
err := account.StartRecovery()
assert.Error(t, err)
})
}
func TestAccount_CompleteRecovery(t *testing.T) {
t.Run("should complete recovery with new public key", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("old-key"), uuid.New(), 3, 2)
account.Status = value_objects.AccountStatusRecovering
newPublicKey := []byte("new-public-key")
newKeygenSessionID := uuid.New()
account.CompleteRecovery(newPublicKey, newKeygenSessionID)
assert.Equal(t, value_objects.AccountStatusActive, account.Status)
assert.Equal(t, newPublicKey, account.PublicKey)
assert.Equal(t, newKeygenSessionID, account.KeygenSessionID)
})
}
func TestAccount_CanLogin(t *testing.T) {
testCases := []struct {
name string
status value_objects.AccountStatus
canLogin bool
}{
{"active account can login", value_objects.AccountStatusActive, true},
{"suspended account cannot login", value_objects.AccountStatusSuspended, false},
{"locked account cannot login", value_objects.AccountStatusLocked, false},
{"recovering account cannot login", value_objects.AccountStatusRecovering, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
account.Status = tc.status
assert.Equal(t, tc.canLogin, account.CanLogin())
})
}
}
func TestAccount_Validate(t *testing.T) {
t.Run("should pass validation with valid data", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 3, 2)
err := account.Validate()
assert.NoError(t, err)
})
t.Run("should fail validation with empty username", func(t *testing.T) {
account := entities.NewAccount("", "user@test.com", []byte("key"), uuid.New(), 3, 2)
err := account.Validate()
assert.Error(t, err)
assert.Equal(t, entities.ErrInvalidUsername, err)
})
t.Run("should fail validation with empty email", func(t *testing.T) {
account := entities.NewAccount("user", "", []byte("key"), uuid.New(), 3, 2)
err := account.Validate()
assert.Error(t, err)
assert.Equal(t, entities.ErrInvalidEmail, err)
})
t.Run("should fail validation with empty public key", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte{}, uuid.New(), 3, 2)
err := account.Validate()
assert.Error(t, err)
assert.Equal(t, entities.ErrInvalidPublicKey, err)
})
t.Run("should fail validation with invalid threshold", func(t *testing.T) {
account := entities.NewAccount("user", "user@test.com", []byte("key"), uuid.New(), 2, 3) // t > n
err := account.Validate()
assert.Error(t, err)
assert.Equal(t, entities.ErrInvalidThreshold, err)
})
}
func TestAccountID(t *testing.T) {
t.Run("should create new account ID", func(t *testing.T) {
id := value_objects.NewAccountID()
assert.False(t, id.IsZero())
})
t.Run("should create account ID from string", func(t *testing.T) {
original := value_objects.NewAccountID()
parsed, err := value_objects.AccountIDFromString(original.String())
require.NoError(t, err)
assert.True(t, original.Equals(parsed))
})
t.Run("should fail to parse invalid account ID", func(t *testing.T) {
_, err := value_objects.AccountIDFromString("invalid-uuid")
assert.Error(t, err)
})
}
func TestAccountStatus(t *testing.T) {
t.Run("should validate status correctly", func(t *testing.T) {
validStatuses := []value_objects.AccountStatus{
value_objects.AccountStatusActive,
value_objects.AccountStatusSuspended,
value_objects.AccountStatusLocked,
value_objects.AccountStatusRecovering,
}
for _, status := range validStatuses {
assert.True(t, status.IsValid(), "status %s should be valid", status)
}
invalidStatus := value_objects.AccountStatus("invalid")
assert.False(t, invalidStatus.IsValid())
})
}
func TestShareType(t *testing.T) {
t.Run("should validate share type correctly", func(t *testing.T) {
validTypes := []value_objects.ShareType{
value_objects.ShareTypeUserDevice,
value_objects.ShareTypeServer,
value_objects.ShareTypeRecovery,
}
for _, st := range validTypes {
assert.True(t, st.IsValid(), "share type %s should be valid", st)
}
invalidType := value_objects.ShareType("invalid")
assert.False(t, invalidType.IsValid())
})
}
func TestAccountShare(t *testing.T) {
t.Run("should create account share with correct initial state", func(t *testing.T) {
accountID := value_objects.NewAccountID()
share := entities.NewAccountShare(
accountID,
value_objects.ShareTypeUserDevice,
"party1",
0,
)
assert.NotEqual(t, uuid.Nil, share.ID)
assert.True(t, share.AccountID.Equals(accountID))
assert.Equal(t, value_objects.ShareTypeUserDevice, share.ShareType)
assert.Equal(t, "party1", share.PartyID)
assert.Equal(t, 0, share.PartyIndex)
assert.True(t, share.IsActive)
})
t.Run("should set device info", func(t *testing.T) {
accountID := value_objects.NewAccountID()
share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)
share.SetDeviceInfo("iOS", "device123")
assert.NotNil(t, share.DeviceType)
assert.Equal(t, "iOS", *share.DeviceType)
assert.NotNil(t, share.DeviceID)
assert.Equal(t, "device123", *share.DeviceID)
})
t.Run("should deactivate share", func(t *testing.T) {
accountID := value_objects.NewAccountID()
share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)
share.Deactivate()
assert.False(t, share.IsActive)
})
t.Run("should identify share types correctly", func(t *testing.T) {
accountID := value_objects.NewAccountID()
userShare := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "p1", 0)
serverShare := entities.NewAccountShare(accountID, value_objects.ShareTypeServer, "p2", 1)
recoveryShare := entities.NewAccountShare(accountID, value_objects.ShareTypeRecovery, "p3", 2)
assert.True(t, userShare.IsUserDeviceShare())
assert.False(t, userShare.IsServerShare())
assert.True(t, serverShare.IsServerShare())
assert.False(t, serverShare.IsUserDeviceShare())
assert.True(t, recoveryShare.IsRecoveryShare())
assert.False(t, recoveryShare.IsServerShare())
})
t.Run("should validate share correctly", func(t *testing.T) {
accountID := value_objects.NewAccountID()
share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "party1", 0)
err := share.Validate()
assert.NoError(t, err)
})
t.Run("should fail validation with empty party ID", func(t *testing.T) {
accountID := value_objects.NewAccountID()
share := entities.NewAccountShare(accountID, value_objects.ShareTypeUserDevice, "", 0)
err := share.Validate()
assert.Error(t, err)
})
}
func TestRecoverySession(t *testing.T) {
t.Run("should create recovery session with correct initial state", func(t *testing.T) {
accountID := value_objects.NewAccountID()
session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
assert.NotEqual(t, uuid.Nil, session.ID)
assert.True(t, session.AccountID.Equals(accountID))
assert.Equal(t, value_objects.RecoveryTypeDeviceLost, session.RecoveryType)
assert.Equal(t, value_objects.RecoveryStatusRequested, session.Status)
})
t.Run("should start keygen", func(t *testing.T) {
accountID := value_objects.NewAccountID()
session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
keygenID := uuid.New()
err := session.StartKeygen(keygenID)
require.NoError(t, err)
assert.Equal(t, value_objects.RecoveryStatusInProgress, session.Status)
assert.NotNil(t, session.NewKeygenSessionID)
assert.Equal(t, keygenID, *session.NewKeygenSessionID)
})
t.Run("should complete recovery", func(t *testing.T) {
accountID := value_objects.NewAccountID()
session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
session.StartKeygen(uuid.New())
err := session.Complete()
require.NoError(t, err)
assert.Equal(t, value_objects.RecoveryStatusCompleted, session.Status)
assert.NotNil(t, session.CompletedAt)
})
t.Run("should fail recovery", func(t *testing.T) {
accountID := value_objects.NewAccountID()
session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
err := session.Fail()
require.NoError(t, err)
assert.Equal(t, value_objects.RecoveryStatusFailed, session.Status)
})
t.Run("should not complete already completed recovery", func(t *testing.T) {
accountID := value_objects.NewAccountID()
session := entities.NewRecoverySession(accountID, value_objects.RecoveryTypeDeviceLost)
session.StartKeygen(uuid.New())
session.Complete()
err := session.Fail()
assert.Error(t, err)
})
}