415 lines
13 KiB
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)
|
|
})
|
|
}
|