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) }) }