diff --git a/satellite/analytics/service.go b/satellite/analytics/service.go index 03219c063..72f3732d4 100644 --- a/satellite/analytics/service.go +++ b/satellite/analytics/service.go @@ -98,6 +98,24 @@ type Config struct { HubSpot HubSpotConfig } +// FreezeTracker is an interface for account freeze event tracking methods. +type FreezeTracker interface { + // TrackAccountFrozen sends an account frozen event to Segment. + TrackAccountFrozen(userID uuid.UUID, email string) + + // TrackAccountUnfrozen sends an account unfrozen event to Segment. + TrackAccountUnfrozen(userID uuid.UUID, email string) + + // TrackAccountUnwarned sends an account unwarned event to Segment. + TrackAccountUnwarned(userID uuid.UUID, email string) + + // TrackAccountFreezeWarning sends an account freeze warning event to Segment. + TrackAccountFreezeWarning(userID uuid.UUID, email string) + + // TrackLargeUnpaidInvoice sends an event to Segment indicating that a user has not paid a large invoice. + TrackLargeUnpaidInvoice(invID string, userID uuid.UUID, email string) +} + // Service for sending analytics. // // architecture: Service diff --git a/satellite/console/accountfreezes.go b/satellite/console/accountfreezes.go index ac27527b9..c94027477 100644 --- a/satellite/console/accountfreezes.go +++ b/satellite/console/accountfreezes.go @@ -63,16 +63,16 @@ type AccountFreezeService struct { freezeEventsDB AccountFreezeEvents usersDB Users projectsDB Projects - analytics *analytics.Service + tracker analytics.FreezeTracker } // NewAccountFreezeService creates a new account freeze service. -func NewAccountFreezeService(freezeEventsDB AccountFreezeEvents, usersDB Users, projectsDB Projects, analytics *analytics.Service) *AccountFreezeService { +func NewAccountFreezeService(freezeEventsDB AccountFreezeEvents, usersDB Users, projectsDB Projects, tracker analytics.FreezeTracker) *AccountFreezeService { return &AccountFreezeService{ freezeEventsDB: freezeEventsDB, usersDB: usersDB, projectsDB: projectsDB, - analytics: analytics, + tracker: tracker, } } @@ -173,7 +173,7 @@ func (s *AccountFreezeService) FreezeUser(ctx context.Context, userID uuid.UUID) } } - s.analytics.TrackAccountFrozen(userID, user.Email) + s.tracker.TrackAccountFrozen(userID, user.Email) return nil } @@ -212,7 +212,7 @@ func (s *AccountFreezeService) UnfreezeUser(ctx context.Context, userID uuid.UUI return err } - s.analytics.TrackAccountUnfrozen(userID, user.Email) + s.tracker.TrackAccountUnfrozen(userID, user.Email) return nil } @@ -233,7 +233,7 @@ func (s *AccountFreezeService) WarnUser(ctx context.Context, userID uuid.UUID) ( return ErrAccountFreeze.Wrap(err) } - s.analytics.TrackAccountFreezeWarning(userID, user.Email) + s.tracker.TrackAccountFreezeWarning(userID, user.Email) return nil } @@ -256,7 +256,7 @@ func (s *AccountFreezeService) UnWarnUser(ctx context.Context, userID uuid.UUID) return err } - s.analytics.TrackAccountUnwarned(userID, user.Email) + s.tracker.TrackAccountUnwarned(userID, user.Email) return nil } @@ -271,3 +271,8 @@ func (s *AccountFreezeService) GetAll(ctx context.Context, userID uuid.UUID) (fr return freeze, warning, nil } + +// TestChangeFreezeTracker changes the freeze tracker service for tests. +func (s *AccountFreezeService) TestChangeFreezeTracker(t analytics.FreezeTracker) { + s.tracker = t +} diff --git a/satellite/console/accountfreezes_test.go b/satellite/console/accountfreezes_test.go index 82f9bd8d8..085a8cf2b 100644 --- a/satellite/console/accountfreezes_test.go +++ b/satellite/console/accountfreezes_test.go @@ -8,9 +8,13 @@ import ( "testing" "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" ) @@ -242,3 +246,76 @@ func TestAccountFreezeAlreadyFrozen(t *testing.T) { }) }) } + +func TestFreezeEffects(t *testing.T) { + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 2, UplinkCount: 2, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + config.AccountFreeze.Enabled = true + }, + }, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + sat := planet.Satellites[0] + usersDB := sat.DB.Console().Users() + projectsDB := sat.DB.Console().Projects() + consoleService := sat.API.Console.Service + freezeService := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + + uplink1 := planet.Uplinks[0] + user1, _, err := consoleService.GetUserByEmailWithUnverified(ctx, uplink1.User[sat.ID()].Email) + require.NoError(t, err) + + bucketName := "testbucket" + path := "test/path" + + expectedData := testrand.Bytes(50 * memory.KiB) + + shouldUploadAndDownload := func(testT *testing.T) { + // Should be able to upload because account is not warned nor frozen. + err = uplink1.Upload(ctx, sat, bucketName, path, expectedData) + require.NoError(testT, err) + + // Should be able to download because account is not frozen. + data, err := uplink1.Download(ctx, sat, bucketName, path) + require.NoError(testT, err) + require.Equal(testT, expectedData, data) + } + + t.Run("Freeze effect on project owner", func(t *testing.T) { + shouldUploadAndDownload(t) + + err = freezeService.WarnUser(ctx, user1.ID) + require.NoError(t, err) + + // Should be able to download because account is not frozen. + data, err := uplink1.Download(ctx, sat, bucketName, path) + require.NoError(t, err) + require.Equal(t, expectedData, data) + + err = freezeService.FreezeUser(ctx, user1.ID) + require.NoError(t, err) + + // Should not be able to upload because account is frozen. + err = uplink1.Upload(ctx, sat, bucketName, path, expectedData) + require.Error(t, err) + + // Should not be able to download because account is frozen. + _, err = uplink1.Download(ctx, sat, bucketName, path) + require.Error(t, err) + + // Should not be able to create bucket because account is frozen. + err = uplink1.CreateBucket(ctx, sat, "anotherBucket") + require.Error(t, err) + + // Should be able to list even if frozen. + objects, err := uplink1.ListObjects(ctx, sat, bucketName) + require.NoError(t, err) + require.Len(t, objects, 1) + + // Should be able to delete even if frozen. + err = uplink1.DeleteObject(ctx, sat, bucketName, path) + require.NoError(t, err) + }) + }) +} diff --git a/satellite/payments/accountfreeze/chore.go b/satellite/payments/accountfreeze/chore.go index b301c5468..3738bad8d 100644 --- a/satellite/payments/accountfreeze/chore.go +++ b/satellite/payments/accountfreeze/chore.go @@ -188,6 +188,11 @@ func (chore *Chore) TestSetNow(f func() time.Time) { chore.nowFn = f } +// TestSetFreezeService changes the freeze service for tests. +func (chore *Chore) TestSetFreezeService(service *console.AccountFreezeService) { + chore.freezeService = service +} + // Close closes the chore. func (chore *Chore) Close() error { chore.Loop.Close() diff --git a/satellite/payments/accountfreeze/chore_test.go b/satellite/payments/accountfreeze/chore_test.go index d0bd1f2a0..f3eeac996 100644 --- a/satellite/payments/accountfreeze/chore_test.go +++ b/satellite/payments/accountfreeze/chore_test.go @@ -12,6 +12,7 @@ import ( "go.uber.org/zap" "storj.io/common/testcontext" + "storj.io/common/uuid" "storj.io/storj/private/testplanet" "storj.io/storj/satellite" "storj.io/storj/satellite/console" @@ -33,8 +34,9 @@ func TestAutoFreezeChore(t *testing.T) { customerDB := sat.Core.DB.StripeCoinPayments().Customers() usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, newFreezeTrackerMock(t)) chore := sat.Core.Payments.AccountFreeze + chore.TestSetFreezeService(service) user, err := sat.AddUser(ctx, console.CreateUser{ FullName: "Test User", @@ -49,6 +51,8 @@ func TestAutoFreezeChore(t *testing.T) { curr := string(stripe.CurrencyUSD) t.Run("No freeze event for paid invoice", func(t *testing.T) { + // AnalyticsMock tests that events are sent once. + service.TestChangeFreezeTracker(newFreezeTrackerMock(t)) item, err := stripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{ Params: stripe.Params{Context: ctx}, Amount: &amount, @@ -102,6 +106,8 @@ func TestAutoFreezeChore(t *testing.T) { }) t.Run("Freeze event for failed invoice", func(t *testing.T) { + // AnalyticsMock tests that events are sent once. + service.TestChangeFreezeTracker(newFreezeTrackerMock(t)) // reset chore clock chore.TestSetNow(time.Now) @@ -160,3 +166,46 @@ func TestAutoFreezeChore(t *testing.T) { }) }) } + +type freezeTrackerMock struct { + t *testing.T + freezeCounts map[string]int + warnCounts map[string]int +} + +func newFreezeTrackerMock(t *testing.T) *freezeTrackerMock { + return &freezeTrackerMock{ + t: t, + freezeCounts: map[string]int{}, + warnCounts: map[string]int{}, + } +} + +// The following functions are implemented from analytics.FreezeTracker. +// They mock/test to make sure freeze events are sent just once. + +func (mock *freezeTrackerMock) TrackAccountFrozen(_ uuid.UUID, email string) { + mock.freezeCounts[email]++ + // make sure this tracker has not been called already for this email. + require.Equal(mock.t, 1, mock.freezeCounts[email]) +} + +func (mock *freezeTrackerMock) TrackAccountUnfrozen(_ uuid.UUID, email string) { + mock.freezeCounts[email]-- + // make sure this tracker has not been called already for this email. + require.Equal(mock.t, 0, mock.freezeCounts[email]) +} + +func (mock *freezeTrackerMock) TrackAccountUnwarned(_ uuid.UUID, email string) { + mock.warnCounts[email]-- + // make sure this tracker has not been called already for this email. + require.Equal(mock.t, 0, mock.warnCounts[email]) +} + +func (mock *freezeTrackerMock) TrackAccountFreezeWarning(_ uuid.UUID, email string) { + mock.warnCounts[email]++ + // make sure this tracker has not been called already for this email. + require.Equal(mock.t, 1, mock.warnCounts[email]) +} + +func (mock *freezeTrackerMock) TrackLargeUnpaidInvoice(_ string, _ uuid.UUID, _ string) {}