Files
status-go/internal/protocol/push_notification_test.go
Igor Sirotin f3e4363808 refactor: move protocol to internal/protocol
Part of the Go project layout migration, item 27.

Pure move plus import-path rewrite across 502 files. No API or behaviour
change. `internal/` keeps the messaging application logic unimportable
from outside the module, which is what the issue asks for -- status-go is
consumed through the C-bindings in mobile/, not as a Go library.

Things that had to follow the move, beyond the Go imports:

- tools/generate-handlers/template.txt. messenger_handlers.go is
  generated, and the template hard-codes the imports it emits, so the
  generated file kept importing protocol/common and failed typecheck.
- .gitignore. The ignore rule for that generated file was pinned to the
  old path; without moving it, a 1486-line generated file starts being
  tracked.
- Makefile: the logosstorage and torrent test targets (both the archive
  packages and ./protocol itself), the archive README, migration-protocol.
- scripts/run_unit_tests.sh, which names the protocol package explicitly
  to shard its tests.
- scripts/cleanup_generated_files.sh and .golangci.yml.

scripts/migration_check.sh also needed a fix that is not specific to this
move: it validated every file the branch touched under a migration dir
against the timestamp naming rule, and a directory rename makes every
migration in it look newly added. It now excludes renames, so moving a
migration is not mistaken for adding one.

refs #7067
2026-08-21 10:11:05 +02:00

1366 lines
41 KiB
Go

package protocol
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/internal/crypto"
"github.com/status-im/status-go/internal/crypto/types"
"github.com/status-im/status-go/internal/protocol/common"
"github.com/status-im/status-go/internal/protocol/communities"
"github.com/status-im/status-go/internal/protocol/contacts"
"github.com/status-im/status-go/internal/protocol/protobuf"
"github.com/status-im/status-go/internal/protocol/pushnotificationclient"
"github.com/status-im/status-go/internal/protocol/pushnotificationserver"
"github.com/status-im/status-go/internal/protocol/requests"
"github.com/status-im/status-go/internal/testutils"
)
const (
bob1DeviceToken = "token-1"
bob2DeviceToken = "token-2"
testAPNTopic = "topic"
)
func TestMessengerPushNotificationSuite(t *testing.T) {
suite.Run(t, new(MessengerPushNotificationSuite))
}
type MessengerPushNotificationSuite struct {
MessengerBaseTestSuite
gorushMock *http.Server
gorushMockURL string
}
func (s *MessengerPushNotificationSuite) SetupSuite() {
// Create a new HTTP server as a mock for gorush
s.gorushMock = &http.Server{
Addr: "127.0.0.1:0",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
ReadHeaderTimeout: 5 * time.Second,
}
listener, err := net.Listen("tcp", s.gorushMock.Addr)
s.Require().NoError(err)
// Get the actual server URL after binding to a port
s.gorushMockURL = fmt.Sprintf("http://%s", listener.Addr().String())
// Start the server in a goroutine
go func() {
err := s.gorushMock.Serve(listener)
s.Require().ErrorIs(err, http.ErrServerClosed)
}()
}
func (s *MessengerPushNotificationSuite) TearDownSuite() {
// Create a context with a timeout for graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown the server
if err := s.gorushMock.Shutdown(ctx); err != nil {
s.T().Errorf("Server shutdown error: %v", err)
}
fmt.Println("HTTP server stopped")
}
func (s *MessengerPushNotificationSuite) SetupTest() {
s.MessengerBaseTestSuite.setupMessaging()
s.m = s.newMessenger()
s.privateKey = s.m.identity
}
func (s *MessengerPushNotificationSuite) newMessenger() *Messenger {
messenger, err := newRunningTestMessenger(s.T(), s.messagingEnv, testMessengerConfig{extraOptions: []Option{WithPushNotifications()}})
s.Require().NoError(err)
return messenger
}
func (s *MessengerPushNotificationSuite) newPushNotificationServer() (*Messenger, *pushnotificationserver.Server) {
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
serverConfig := &pushnotificationserver.Config{
Enabled: true,
Logger: testutils.MustCreateTestLogger(),
Identity: privateKey,
GorushURL: s.gorushMockURL,
}
server := pushnotificationserver.New(serverConfig)
messenger, err := newRunningTestMessenger(s.T(), s.messagingEnv, testMessengerConfig{privateKey: privateKey, extraOptions: []Option{WithPushNotificationServer(server)}})
s.Require().NoError(err)
serverPersistence := pushnotificationserver.NewSQLitePersistence(messenger.database)
err = server.Start(serverPersistence, messenger.Messaging())
s.Require().NoError(err)
s.T().Cleanup(func() {
server.Stop()
})
return messenger, server
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotification() {
bob1 := s.m
bob2, err := newRunningTestMessenger(s.T(), s.messagingEnv, testMessengerConfig{privateKey: s.m.identity, extraOptions: []Option{WithPushNotifications()}})
s.Require().NoError(err)
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob1.installationID, bob2.installationID}
// Register bob1
err = bob1.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob1.RetrieveAll()
if err != nil {
return err
}
registered, err := bob1.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob1.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bob1Servers, err := bob1.GetPushNotificationsServers()
s.Require().NoError(err)
// Register bob2
err = bob2.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob2.RegisterForPushNotifications(context.Background(), bob2DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
s.Require().NoError(err)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob2.RetrieveAll()
if err != nil {
return err
}
registered, err := bob2.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob2.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bob2Servers, err := bob2.GetPushNotificationsServers()
s.Require().NoError(err)
// Create one to one chat & send message
pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.getTimesource())
s.Require().NoError(alice.SaveChat(chat))
inputMessage := buildTestMessage(*chat)
response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
// Each paired device advertises only its own installation's push info on the
// shared contact-code topic, and processing one such advertisement stamps a
// query timestamp that suppresses the authoritative server query for
// staleQueryTimeInSeconds. alice only subscribes to that topic when she starts
// the chat above, so a device that advertised earlier is missed. Re-advertise
// from both devices now that she is listening so she receives both.
s.Require().NoError(bob1.PublishIdentityImage())
s.Require().NoError(bob2.PublishIdentityImage())
infoMap := make(map[string]*pushnotificationclient.PushNotificationInfo)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
info, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
for _, i := range info {
infoMap[i.AccessToken] = i
}
// Check we have replies for both bob1 and bob2
if len(infoMap) != 2 {
return errors.New("info not fetched")
}
return nil
})
s.Require().Len(infoMap, 2)
// Check we have replies for both bob1 and bob2
var bob1Info, bob2Info *pushnotificationclient.PushNotificationInfo
bob1Info = infoMap[bob1Servers[0].AccessToken]
bob2Info = infoMap[bob2Servers[0].AccessToken]
s.Require().NotNil(bob1Info)
s.Require().Equal(bob1.installationID, bob1Info.InstallationID)
s.Require().Equal(bob1Servers[0].AccessToken, bob1Info.AccessToken)
s.Require().Equal(&bob1.identity.PublicKey, bob1Info.PublicKey)
s.Require().NotNil(bob2Info)
s.Require().Equal(bob2.installationID, bob2Info.InstallationID)
s.Require().Equal(bob2Servers[0].AccessToken, bob2Info.AccessToken)
s.Require().Equal(&bob2.identity.PublicKey, bob2Info.PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 2)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob1.identity.PublicKey), bob1.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationFromContactOnly() {
bob := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob.installationID}
// Register bob
err := bob.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
// Add alice has a contact
aliceContact := &contacts.Contact{
ID: types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)),
EnsName: "Some Contact",
ContactRequestLocalState: contacts.ContactRequestStateSent,
}
_, err = bob.AddContact(context.Background(), &requests.AddContact{ID: aliceContact.ID})
s.Require().NoError(err)
// Enable from contacts only
err = bob.EnablePushNotificationsFromContactsOnly()
s.Require().NoError(err)
err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
s.Require().NoError(err)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bobServers, err := bob.GetPushNotificationsServers()
s.Require().NoError(err)
// Create one to one chat & send message
pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.getTimesource())
s.Require().NoError(alice.SaveChat(chat))
inputMessage := buildTestMessage(*chat)
response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
var info []*pushnotificationclient.PushNotificationInfo
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
// Check we have replies for bob
if len(info) != 1 {
return errors.New("info not fetched")
}
return nil
})
s.Require().NoError(err)
s.Require().NotNil(info)
s.Require().Equal(bob.installationID, info[0].InstallationID)
s.Require().Equal(bobServers[0].AccessToken, info[0].AccessToken)
s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 1)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationRetries() {
bob := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
// another contact to invalidate the token
frank := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob.installationID}
// Register bob
err := bob.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
// Add alice has a contact
aliceContact := &contacts.Contact{
ID: types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)),
EnsName: "Some Contact",
ContactRequestLocalState: contacts.ContactRequestStateSent,
}
_, err = bob.AddContact(context.Background(), &requests.AddContact{ID: aliceContact.ID})
s.Require().NoError(err)
// Add frank has a contact
frankContact := &contacts.Contact{
ID: types.EncodeHex(crypto.FromECDSAPub(&frank.identity.PublicKey)),
EnsName: "Some Contact",
ContactRequestLocalState: contacts.ContactRequestStateSent,
}
_, err = bob.AddContact(context.Background(), &requests.AddContact{ID: frankContact.ID})
s.Require().NoError(err)
// Enable from contacts only
err = bob.EnablePushNotificationsFromContactsOnly()
s.Require().NoError(err)
err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
s.Require().NoError(err)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bobServers, err := bob.GetPushNotificationsServers()
s.Require().NoError(err)
// Create one to one chat & send message
pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.getTimesource())
s.Require().NoError(alice.SaveChat(chat))
inputMessage := buildTestMessage(*chat)
_, err = alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
// We check that alice retrieves the info from the messenger
var info []*pushnotificationclient.PushNotificationInfo
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
// Check we have replies for bob
if len(info) != 1 {
return errors.New("info not fetched")
}
return nil
})
s.Require().NoError(err)
s.Require().NotNil(info)
s.Require().Equal(bob.installationID, info[0].InstallationID)
s.Require().Equal(bobServers[0].AccessToken, info[0].AccessToken)
s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey)
// The message has been sent, but not received, now we remove a contact so that the token is invalidated
frankContact = &contacts.Contact{
ID: types.EncodeHex(crypto.FromECDSAPub(&frank.identity.PublicKey)),
EnsName: "Some Contact",
}
_, err = bob.RemoveContact(context.Background(), frankContact.ID)
s.Require().NoError(err)
// Re-registration should be triggered, pull from messenger and bob to check we are correctly registered
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
return nil
})
newBobServers, err := bob.GetPushNotificationsServers()
s.Require().NoError(err)
// Make sure access token is not the same
s.Require().NotEqual(newBobServers[0].AccessToken, bobServers[0].AccessToken)
// Send another message, here the token will not be valid
inputMessage = buildTestMessage(*chat)
response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
// Check we have replies for bob
if len(info) != 1 {
return errors.New("info not fetched")
}
if newBobServers[0].AccessToken != info[0].AccessToken {
return errors.New("still using the old access token")
}
return nil
})
s.Require().NoError(err)
s.Require().NotNil(info)
s.Require().Equal(bob.installationID, info[0].InstallationID)
s.Require().Equal(newBobServers[0].AccessToken, info[0].AccessToken)
s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 1)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestContactCode() {
bob1 := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
// Register bob1
err := bob1.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob1.RetrieveAll()
if err != nil {
return err
}
registered, err := bob1.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob1.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
contactCodeAdvertisement, err := bob1.buildContactCodeAdvertisement()
s.Require().NoError(err)
s.Require().NotNil(contactCodeAdvertisement)
s.Require().NoError(alice.pushNotificationClient.HandleContactCodeAdvertisement(&bob1.identity.PublicKey, contactCodeAdvertisement))
}
// TestContactCodeAdvertisementStoresPushInfoViaHandler is a regression test for the bug
// where Messenger.HandleContactCodeAdvertisement stopped forwarding the advertisement to
// the push notification client (the forwarding was dropped in the "generate handlers"
// refactor). The push info carried in a contact code is the channel light clients rely on
// to learn a contact's push registration — the live query response is ephemeral and is
// routinely missed by light clients — so the handler MUST forward it. Unlike TestContactCode
// (which calls the push client method directly and so never exercised this wiring), this
// test drives the messenger handler and asserts the recipient's push info is stored.
func (s *MessengerPushNotificationSuite) TestContactCodeAdvertisementStoresPushInfoViaHandler() {
bob1 := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
// Register bob1 with the push notification server so its contact code carries push info.
err := bob1.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
s.Require().NoError(err)
err = testutils.RetryWithBackOff(func() error {
if _, err := messenger.RetrieveAll(); err != nil {
return err
}
if _, err := bob1.RetrieveAll(); err != nil {
return err
}
registered, err := bob1.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
return nil
})
s.Require().NoError(err)
contactCodeAdvertisement, err := bob1.buildContactCodeAdvertisement()
s.Require().NoError(err)
s.Require().NotNil(contactCodeAdvertisement)
s.Require().NotEmpty(contactCodeAdvertisement.PushNotificationInfo)
// The common case for periodic push-info re-advertisements has no chat identity attached;
// this is precisely the shape the buggy handler dropped (it bailed when ChatIdentity == nil).
s.Require().Nil(contactCodeAdvertisement.ChatIdentity)
// alice has never interacted with bob1, so it must not yet have bob1's push info.
infoBefore, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, nil)
s.Require().NoError(err)
s.Require().Empty(infoBefore)
// Drive the messenger handler (the wiring under test) rather than the push client directly.
state := &ReceivedMessageState{
CurrentMessageState: &CurrentMessageState{
PublicKey: &bob1.identity.PublicKey,
},
}
s.Require().NoError(alice.HandleContactCodeAdvertisement(context.Background(), state, contactCodeAdvertisement, nil))
// The handler must have forwarded the advertisement to the push client, which stores
// bob1's push registration so a subsequent send can dispatch a notification.
infoAfter, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, nil)
s.Require().NoError(err)
s.Require().NotEmpty(infoAfter, "messenger handler must forward contact-code push info to the push notification client")
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationMention() {
bob := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob.installationID}
// Create public chat and join for both alice and bob
chat := CreatePublicChat("status", s.m.getTimesource())
err := bob.SaveChat(chat)
s.Require().NoError(err)
_, err = bob.Join(chat)
s.Require().NoError(err)
err = alice.SaveChat(chat)
s.Require().NoError(err)
_, err = alice.Join(chat)
s.Require().NoError(err)
// Register bob
err = bob.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bobServers, err := bob.GetPushNotificationsServers()
s.Require().NoError(err)
inputMessage := buildTestMessage(*chat)
// message contains a mention
inputMessage.Text = "Hey @" + types.EncodeHex(crypto.FromECDSAPub(&bob.identity.PublicKey))
response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
var bobInfo []*pushnotificationclient.PushNotificationInfo
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
bobInfo, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
// Check we have replies for bob
if len(bobInfo) != 1 {
return errors.New("info not fetched")
}
return nil
})
s.Require().NoError(err)
s.Require().NotEmpty(bobInfo)
s.Require().Equal(bob.installationID, bobInfo[0].InstallationID)
s.Require().Equal(bobServers[0].AccessToken, bobInfo[0].AccessToken)
s.Require().Equal(&bob.identity.PublicKey, bobInfo[0].PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 1)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationCommunityRequest() {
bob := s.m
messenger, server := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
// Register bob
err := bob.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
_, err = bob.GetPushNotificationsServers()
s.Require().NoError(err)
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_MANUAL_ACCEPT,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
response, err := bob.CreateCommunity(description, true)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
// Send a community message
chat := CreateOneToOneChat(crypto.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, alice.getTimesource())
inputMessage := common.NewMessage()
inputMessage.ChatId = chat.ID
inputMessage.Text = "some text"
inputMessage.CommunityID = community.IDString()
err = bob.SaveChat(chat)
s.NoError(err)
_, err = bob.SendChatMessage(context.Background(), inputMessage)
s.NoError(err)
// Pull message and make sure org is received
err = testutils.RetryWithBackOff(func() error {
response, err = alice.RetrieveAll()
if err != nil {
return err
}
if len(response.Communities()) == 0 {
return errors.New("community not received")
}
return nil
})
request := createRequestToJoinCommunity(&s.Suite, community.ID(), alice, alicePassword, []string{aliceAddress1})
alice.communitiesManager.PermissionChecker = &testPermissionChecker{}
// We try to join the org
response, err = alice.RequestToJoinCommunity(request)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.RequestsToJoinCommunity(), 1)
requestToJoin1 := response.RequestsToJoinCommunity()[0]
s.Require().NotNil(requestToJoin1)
s.Require().Equal(community.ID(), requestToJoin1.CommunityID)
s.Require().True(requestToJoin1.Our)
s.Require().NotEmpty(requestToJoin1.ID)
s.Require().NotEmpty(requestToJoin1.Clock)
s.Require().Equal(requestToJoin1.PublicKey, crypto.PubkeyToHex(&alice.identity.PublicKey))
s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin1.State)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
if server.SentRequests != 1 {
return errors.New("request not sent")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationPairedDevices() {
bob1 := s.m
bob2, err := newRunningTestMessenger(s.T(), s.messagingEnv, testMessengerConfig{privateKey: s.m.identity, extraOptions: []Option{WithPushNotifications()}})
s.Require().NoError(err)
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob1.installationID, bob2.installationID}
// Register bob1
err = bob1.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob1.RetrieveAll()
if err != nil {
return err
}
registered, err := bob1.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob1.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bob1Servers, err := bob1.GetPushNotificationsServers()
s.Require().NoError(err)
// Register bob2
err = bob2.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob2.RegisterForPushNotifications(context.Background(), bob2DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
s.Require().NoError(err)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob2.RetrieveAll()
if err != nil {
return err
}
registered, err := bob2.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob2.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bob2Servers, err := bob2.GetPushNotificationsServers()
s.Require().NoError(err)
// Create one to one chat & send message
pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.getTimesource())
s.Require().NoError(alice.SaveChat(chat))
inputMessage := buildTestMessage(*chat)
response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
// Each paired device advertises only its own installation's push info on the
// shared contact-code topic, and processing one such advertisement stamps a
// query timestamp that suppresses the authoritative server query for
// staleQueryTimeInSeconds. alice only subscribes to that topic when she starts
// the chat above, so a device that advertised earlier is missed. Re-advertise
// from both devices now that she is listening so she receives both.
s.Require().NoError(bob1.PublishIdentityImage())
s.Require().NoError(bob2.PublishIdentityImage())
infoMap := make(map[string]*pushnotificationclient.PushNotificationInfo)
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
info, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
for _, i := range info {
infoMap[i.AccessToken] = i
}
// Check we have replies for both bob1 and bob2
if len(infoMap) != 2 {
return errors.New("info not fetched")
}
return nil
})
s.Require().Len(infoMap, 2)
// Check we have replies for both bob1 and bob2
var bob1Info, bob2Info *pushnotificationclient.PushNotificationInfo
bob1Info = infoMap[bob1Servers[0].AccessToken]
bob2Info = infoMap[bob2Servers[0].AccessToken]
s.Require().NotNil(bob1Info)
s.Require().Equal(bob1.installationID, bob1Info.InstallationID)
s.Require().Equal(bob1Servers[0].AccessToken, bob1Info.AccessToken)
s.Require().Equal(&bob1.identity.PublicKey, bob1Info.PublicKey)
s.Require().NotNil(bob2Info)
s.Require().Equal(bob2.installationID, bob2Info.InstallationID)
s.Require().Equal(bob2Servers[0].AccessToken, bob2Info.AccessToken)
s.Require().Equal(&bob2.identity.PublicKey, bob2Info.PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 2)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob1.identity.PublicKey), bob1.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationReply() {
bob := s.m
messenger, _ := s.newPushNotificationServer()
alice := s.newMessenger()
s.Require().NoError(alice.EnableSendingPushNotifications())
bobInstallationIDs := []string{bob.installationID}
// Create public chat and join for both alice and bob
chat := CreatePublicChat("status", s.m.getTimesource())
err := bob.SaveChat(chat)
s.Require().NoError(err)
_, err = bob.Join(chat)
s.Require().NoError(err)
err = alice.SaveChat(chat)
s.Require().NoError(err)
_, err = alice.Join(chat)
s.Require().NoError(err)
// Register bob
err = bob.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
s.Require().NoError(err)
err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
// Pull servers and check we registered
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = bob.RetrieveAll()
if err != nil {
return err
}
registered, err := bob.RegisteredForPushNotifications()
if err != nil {
return err
}
if !registered {
return errors.New("not registered")
}
bobServers, err := bob.GetPushNotificationsServers()
if err != nil {
return err
}
if len(bobServers) == 0 {
return errors.New("not registered")
}
return nil
})
// Make sure we receive it
s.Require().NoError(err)
bobServers, err := bob.GetPushNotificationsServers()
s.Require().NoError(err)
firstMessage := buildTestMessage(*chat)
firstMessage.Text = "Hello!"
response, err := bob.SendChatMessage(context.Background(), firstMessage)
s.Require().NoError(err)
messageIDString := response.Messages()[0].ID
_, err = WaitOnMessengerResponse(
alice,
func(r *MessengerResponse) bool {
for _, message := range r.Messages() {
if message.ID == messageIDString {
return true
}
}
return false
},
"no messages",
)
replyMessage := buildTestMessage(*chat)
replyMessage.Text = "Hello reply"
replyMessage.ResponseTo = messageIDString
response, err = alice.SendChatMessage(context.Background(), replyMessage)
s.Require().NoError(err)
messageIDString = response.Messages()[0].ID
messageID, err := hex.DecodeString(messageIDString[2:])
s.Require().NoError(err)
var bobInfo []*pushnotificationclient.PushNotificationInfo
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
bobInfo, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
if err != nil {
return err
}
// Check we have replies for bob
if len(bobInfo) != 1 {
return errors.New("info not fetched")
}
return nil
})
s.Require().NoError(err)
s.Require().NotEmpty(bobInfo)
s.Require().Equal(bob.installationID, bobInfo[0].InstallationID)
s.Require().Equal(bobServers[0].AccessToken, bobInfo[0].AccessToken)
s.Require().Equal(&bob.identity.PublicKey, bobInfo[0].PublicKey)
retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs)
s.Require().NoError(err)
s.Require().NotNil(retrievedNotificationInfo)
s.Require().Len(retrievedNotificationInfo, 1)
var sentNotification *pushnotificationclient.SentNotification
err = testutils.RetryWithBackOff(func() error {
_, err = messenger.RetrieveAll()
if err != nil {
return err
}
_, err = alice.RetrieveAll()
if err != nil {
return err
}
sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID)
if err != nil {
return err
}
if sentNotification == nil {
return errors.New("sent notification not found")
}
if !sentNotification.Success {
return errors.New("sent notification not successul")
}
return nil
})
s.Require().NoError(err)
}