package processor import ( "context" "errors" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "apskel-pos-be/internal/repository" ) // customerDeviceRepoFake keeps devices the way the table does: one row per customer // and device, and a token belongs to one row only. type customerDeviceRepoFake struct{ devices []repository.CustomerDevice } func (f *customerDeviceRepoFake) Register(_ context.Context, d repository.CustomerDevice, _ time.Time) error { kept := f.devices[:0] for _, existing := range f.devices { sameRow := existing.CustomerID == d.CustomerID && existing.DeviceID == d.DeviceID if !sameRow && existing.FCMToken != d.FCMToken { kept = append(kept, existing) } } f.devices = append(kept, d) return nil } func (f *customerDeviceRepoFake) Unregister(_ context.Context, customerID uuid.UUID, deviceID string) error { kept := f.devices[:0] for _, d := range f.devices { if d.CustomerID != customerID || d.DeviceID != deviceID { kept = append(kept, d) } } f.devices = kept return nil } func (f *customerDeviceRepoFake) ListTokens(_ context.Context, customerID uuid.UUID) ([]string, error) { var tokens []string for _, d := range f.devices { if d.CustomerID == customerID { tokens = append(tokens, d.FCMToken) } } return tokens, nil } type fcmFake struct { tokens [][]string title string body string data map[string]string err error } func (f *fcmFake) SendMulticastNotification(_ context.Context, tokens []string, title, body string, data map[string]string) error { f.tokens = append(f.tokens, tokens) f.title, f.body, f.data = title, body, data return f.err } func TestCustomerDevice_NotifiesEveryDeviceOfTheCustomer(t *testing.T) { repo, fcm := &customerDeviceRepoFake{}, &fcmFake{} p := NewCustomerDeviceProcessor(repo, fcm) ctx := context.Background() budi, anita := uuid.New(), uuid.New() require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: budi, DeviceID: "phone", FCMToken: "t1", Platform: ptr("Android")})) require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: budi, DeviceID: "tablet", FCMToken: "t2"})) // A refreshed token replaces the old one of the same device. require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: budi, DeviceID: "phone", FCMToken: "t1b"})) require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: anita, DeviceID: "phone", FCMToken: "t3"})) require.NoError(t, p.Notify(ctx, budi, "EnakPoint masuk", "Kamu menerima 10 EnakPoint", map[string]string{"type": "X"})) assert.Equal(t, [][]string{{"t2", "t1b"}}, fcm.tokens) assert.Equal(t, "EnakPoint masuk", fcm.title) assert.Equal(t, map[string]string{"type": "X"}, fcm.data) } func TestCustomerDevice_TokenMovesToWhoeverLogsInOnThePhone(t *testing.T) { repo, fcm := &customerDeviceRepoFake{}, &fcmFake{} p := NewCustomerDeviceProcessor(repo, fcm) ctx := context.Background() budi, anita := uuid.New(), uuid.New() require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: budi, DeviceID: "phone", FCMToken: "shared"})) require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: anita, DeviceID: "phone", FCMToken: "shared"})) // Budi's notifications no longer reach the phone Anita is now logged in on. require.NoError(t, p.Notify(ctx, budi, "t", "b", nil)) require.NoError(t, p.Notify(ctx, anita, "t", "b", nil)) assert.Equal(t, [][]string{{"shared"}}, fcm.tokens) } func TestCustomerDevice_NothingToSend(t *testing.T) { repo, fcm := &customerDeviceRepoFake{}, &fcmFake{} ctx := context.Background() customer := uuid.New() // No device registered, or logged out: nothing is sent and nothing fails. require.NoError(t, NewCustomerDeviceProcessor(repo, fcm).Notify(ctx, customer, "t", "b", nil)) require.NoError(t, NewCustomerDeviceProcessor(repo, fcm).Register(ctx, repository.CustomerDevice{CustomerID: customer, DeviceID: "phone", FCMToken: "t1"})) require.NoError(t, NewCustomerDeviceProcessor(repo, fcm).Unregister(ctx, customer, "phone")) require.NoError(t, NewCustomerDeviceProcessor(repo, fcm).Notify(ctx, customer, "t", "b", nil)) assert.Empty(t, fcm.tokens) // FCM not configured. require.NoError(t, NewCustomerDeviceProcessor(repo, nil).Notify(ctx, customer, "t", "b", nil)) } func TestCustomerDevice_FCMFailureIsReported(t *testing.T) { repo, fcm := &customerDeviceRepoFake{}, &fcmFake{err: errors.New("unavailable")} p := NewCustomerDeviceProcessor(repo, fcm) ctx := context.Background() customer := uuid.New() require.NoError(t, p.Register(ctx, repository.CustomerDevice{CustomerID: customer, DeviceID: "phone", FCMToken: "t1"})) assert.Error(t, p.Notify(ctx, customer, "t", "b", nil)) } func TestCustomerDevice_RejectsIncompleteRegistrations(t *testing.T) { p := NewCustomerDeviceProcessor(&customerDeviceRepoFake{}, nil) ctx := context.Background() for name, d := range map[string]repository.CustomerDevice{ "no device": {FCMToken: "t"}, "no token": {DeviceID: "phone", FCMToken: " "}, "bad platform": {DeviceID: "phone", FCMToken: "t", Platform: ptr("symbian")}, } { assert.ErrorIs(t, p.Register(ctx, d), ErrInvalidCustomerDevice, name) } }