Skip to content

Commit 24b2d8e

Browse files
authored
Merge branch 'develop' into test/cross-version-chat-compat
2 parents 7087784 + 8120705 commit 24b2d8e

16 files changed

Lines changed: 812 additions & 19 deletions

protocol/messenger_handler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2701,6 +2701,14 @@ func (m *Messenger) HandleGroupChatInvitation(ctx context.Context, state *Receiv
27012701
}
27022702

27032703
func (m *Messenger) HandleContactCodeAdvertisement(ctx context.Context, state *ReceivedMessageState, cca *protobuf.ContactCodeAdvertisement, statusMessage *common.StatusMessage) error {
2704+
publicKey := state.CurrentMessageState.PublicKey
2705+
2706+
if m.pushNotificationClient != nil {
2707+
if err := m.pushNotificationClient.HandleContactCodeAdvertisement(publicKey, cca); err != nil {
2708+
m.logger.Warn("failed to handle ContactCodeAdvertisement push notification info", zap.Error(err))
2709+
}
2710+
}
2711+
27042712
if cca.ChatIdentity == nil {
27052713
return nil
27062714
}

protocol/push_notification_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,74 @@ func (s *MessengerPushNotificationSuite) TestContactCode() {
692692

693693
}
694694

695+
// TestContactCodeAdvertisementStoresPushInfoViaHandler is a regression test for the bug
696+
// where Messenger.HandleContactCodeAdvertisement stopped forwarding the advertisement to
697+
// the push notification client (the forwarding was dropped in the "generate handlers"
698+
// refactor). The push info carried in a contact code is the channel light clients rely on
699+
// to learn a contact's push registration — the live query response is ephemeral and is
700+
// routinely missed by light clients — so the handler MUST forward it. Unlike TestContactCode
701+
// (which calls the push client method directly and so never exercised this wiring), this
702+
// test drives the messenger handler and asserts the recipient's push info is stored.
703+
func (s *MessengerPushNotificationSuite) TestContactCodeAdvertisementStoresPushInfoViaHandler() {
704+
bob1 := s.m
705+
messenger, _ := s.newPushNotificationServer()
706+
alice := s.newMessenger()
707+
708+
s.Require().NoError(alice.EnableSendingPushNotifications())
709+
710+
// Register bob1 with the push notification server so its contact code carries push info.
711+
err := bob1.AddPushNotificationsServer(context.Background(), &messenger.identity.PublicKey, pushnotificationclient.ServerTypeCustom)
712+
s.Require().NoError(err)
713+
714+
err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN)
715+
s.Require().NoError(err)
716+
717+
err = testutils2.RetryWithBackOff(func() error {
718+
if _, err := messenger.RetrieveAll(); err != nil {
719+
return err
720+
}
721+
if _, err := bob1.RetrieveAll(); err != nil {
722+
return err
723+
}
724+
registered, err := bob1.RegisteredForPushNotifications()
725+
if err != nil {
726+
return err
727+
}
728+
if !registered {
729+
return errors.New("not registered")
730+
}
731+
return nil
732+
})
733+
s.Require().NoError(err)
734+
735+
contactCodeAdvertisement, err := bob1.buildContactCodeAdvertisement()
736+
s.Require().NoError(err)
737+
s.Require().NotNil(contactCodeAdvertisement)
738+
s.Require().NotEmpty(contactCodeAdvertisement.PushNotificationInfo)
739+
// The common case for periodic push-info re-advertisements has no chat identity attached;
740+
// this is precisely the shape the buggy handler dropped (it bailed when ChatIdentity == nil).
741+
s.Require().Nil(contactCodeAdvertisement.ChatIdentity)
742+
743+
// alice has never interacted with bob1, so it must not yet have bob1's push info.
744+
infoBefore, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, nil)
745+
s.Require().NoError(err)
746+
s.Require().Empty(infoBefore)
747+
748+
// Drive the messenger handler (the wiring under test) rather than the push client directly.
749+
state := &ReceivedMessageState{
750+
CurrentMessageState: &CurrentMessageState{
751+
PublicKey: &bob1.identity.PublicKey,
752+
},
753+
}
754+
s.Require().NoError(alice.HandleContactCodeAdvertisement(context.Background(), state, contactCodeAdvertisement, nil))
755+
756+
// The handler must have forwarded the advertisement to the push client, which stores
757+
// bob1's push registration so a subsequent send can dispatch a notification.
758+
infoAfter, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, nil)
759+
s.Require().NoError(err)
760+
s.Require().NotEmpty(infoAfter, "messenger handler must forward contact-code push info to the push notification client")
761+
}
762+
695763
func (s *MessengerPushNotificationSuite) TestReceivePushNotificationMention() {
696764
bob := s.m
697765
messenger, _ := s.newPushNotificationServer()

protocol/pushnotificationserver/gorush.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,30 @@ import (
1212
"github.com/status-im/status-go/protocol/protobuf"
1313
)
1414

15-
const defaultNewMessageNotificationText = "You have a new message"
16-
const defaultMentionNotificationText = "Someone mentioned you"
15+
const defaultNewMessageNotificationText = "New message in Messenger"
16+
const defaultMentionNotificationText = "New mention or reply in Communities"
1717
const defaultRequestToJoinCommunityNotificationText = "Someone requested to join a community you are an admin of"
1818

19+
const deepLinkChats = "status-app://chats" // plain messages -> messenger
20+
const deepLinkActivityCenter = "status-app://ac" // mentions, community requests, etc. -> activity center
21+
1922
type GoRushRequestData struct {
2023
EncryptedMessage string `json:"encryptedMessage"`
2124
ChatID string `json:"chatId"`
2225
PublicKey string `json:"publicKey"`
26+
DeepLink string `json:"deepLink,omitempty"`
2327
}
2428

2529
type GoRushRequestNotification struct {
26-
Tokens []string `json:"tokens"`
27-
Platform uint `json:"platform"`
28-
Message string `json:"message"`
29-
Topic string `json:"topic"`
30-
Data *GoRushRequestData `json:"data"`
30+
Tokens []string `json:"tokens"`
31+
Platform uint `json:"platform"`
32+
Message string `json:"message"`
33+
Topic string `json:"topic"`
34+
ContentAvailable bool `json:"content_available,omitempty"`
35+
Sound string `json:"sound,omitempty"`
36+
Priority string `json:"priority,omitempty"`
37+
PushType string `json:"push_type,omitempty"`
38+
Data *GoRushRequestData `json:"data"`
3139
}
3240

3341
type GoRushRequest struct {
@@ -62,16 +70,33 @@ func PushNotificationRegistrationToGoRushRequest(requestAndRegistrations []*Requ
6270
} else {
6371
text = defaultMentionNotificationText
6472
}
73+
isAPN := registration.TokenType == protobuf.PushNotificationRegistration_APN_TOKEN
74+
var sound, priority, pushType, deepLink string
75+
if isAPN {
76+
sound = "default"
77+
priority = "high"
78+
pushType = "alert"
79+
if request.Type == protobuf.PushNotification_MESSAGE {
80+
deepLink = deepLinkChats
81+
} else {
82+
deepLink = deepLinkActivityCenter
83+
}
84+
}
6585
goRushRequests.Notifications = append(goRushRequests.Notifications,
6686
&GoRushRequestNotification{
67-
Tokens: []string{registration.DeviceToken},
68-
Platform: tokenTypeToGoRushPlatform(registration.TokenType),
69-
Message: text,
70-
Topic: registration.ApnTopic,
87+
Tokens: []string{registration.DeviceToken},
88+
Platform: tokenTypeToGoRushPlatform(registration.TokenType),
89+
Message: text,
90+
Topic: registration.ApnTopic,
91+
ContentAvailable: isAPN,
92+
Sound: sound,
93+
Priority: priority,
94+
PushType: pushType,
7195
Data: &GoRushRequestData{
7296
EncryptedMessage: types.EncodeHex(request.Message),
7397
ChatID: types.EncodeHex(request.ChatId),
7498
PublicKey: types.EncodeHex(request.PublicKey),
99+
DeepLink: deepLink,
75100
},
76101
})
77102
}

protocol/pushnotificationserver/gorush_test.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,18 @@ func TestPushNotificationRegistrationToGoRushRequest(t *testing.T) {
7474
expectedRequests := &GoRushRequest{
7575
Notifications: []*GoRushRequestNotification{
7676
{
77-
Tokens: []string{token1},
78-
Platform: platform1,
79-
Message: defaultNewMessageNotificationText,
77+
Tokens: []string{token1},
78+
Platform: platform1,
79+
Message: defaultNewMessageNotificationText,
80+
ContentAvailable: true,
81+
Sound: "default",
82+
Priority: "high",
83+
PushType: "alert",
8084
Data: &GoRushRequestData{
8185
EncryptedMessage: hexMessage1,
8286
ChatID: types.EncodeHex(chatID),
8387
PublicKey: types.EncodeHex(publicKey1),
88+
DeepLink: deepLinkChats,
8489
},
8590
},
8691
{
@@ -108,3 +113,58 @@ func TestPushNotificationRegistrationToGoRushRequest(t *testing.T) {
108113
actualRequests := PushNotificationRegistrationToGoRushRequest(requestAndRegistrations)
109114
require.Equal(t, expectedRequests, actualRequests)
110115
}
116+
117+
func TestGoRushRequestMakesAPNAlertWithSound(t *testing.T) {
118+
req := PushNotificationRegistrationToGoRushRequest([]*RequestAndRegistration{{
119+
Request: &protobuf.PushNotification{Type: protobuf.PushNotification_MESSAGE},
120+
Registration: &protobuf.PushNotificationRegistration{
121+
TokenType: protobuf.PushNotificationRegistration_APN_TOKEN,
122+
DeviceToken: "tok",
123+
ApnTopic: "im.status.ethereum",
124+
},
125+
}})
126+
n := req.Notifications[0]
127+
require.Equal(t, "default", n.Sound)
128+
require.Equal(t, "high", n.Priority)
129+
require.Equal(t, "alert", n.PushType)
130+
require.True(t, n.ContentAvailable)
131+
}
132+
133+
func TestGoRushRequestFirebaseUnaffected(t *testing.T) {
134+
req := PushNotificationRegistrationToGoRushRequest([]*RequestAndRegistration{{
135+
Request: &protobuf.PushNotification{Type: protobuf.PushNotification_MESSAGE},
136+
Registration: &protobuf.PushNotificationRegistration{
137+
TokenType: protobuf.PushNotificationRegistration_FIREBASE_TOKEN,
138+
DeviceToken: "tok",
139+
},
140+
}})
141+
n := req.Notifications[0]
142+
require.Empty(t, n.Sound)
143+
require.Empty(t, n.PushType)
144+
require.Empty(t, n.Data.DeepLink)
145+
}
146+
147+
func TestGoRushRequestDeepLinkByType(t *testing.T) {
148+
cases := []struct {
149+
name string
150+
pushType protobuf.PushNotification_PushNotificationType
151+
expected string
152+
}{
153+
{"message", protobuf.PushNotification_MESSAGE, deepLinkChats},
154+
{"mention", protobuf.PushNotification_MENTION, deepLinkActivityCenter},
155+
{"community request", protobuf.PushNotification_REQUEST_TO_JOIN_COMMUNITY, deepLinkActivityCenter},
156+
{"unknown", protobuf.PushNotification_UNKNOWN_PUSH_NOTIFICATION_TYPE, deepLinkActivityCenter},
157+
}
158+
for _, tc := range cases {
159+
t.Run(tc.name, func(t *testing.T) {
160+
req := PushNotificationRegistrationToGoRushRequest([]*RequestAndRegistration{{
161+
Request: &protobuf.PushNotification{Type: tc.pushType},
162+
Registration: &protobuf.PushNotificationRegistration{
163+
TokenType: protobuf.PushNotificationRegistration_APN_TOKEN,
164+
DeviceToken: "tok",
165+
},
166+
}})
167+
require.Equal(t, tc.expected, req.Notifications[0].Data.DeepLink)
168+
})
169+
}
170+
}

tests-functional/README.MD

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,32 @@ If you see some import issues, make sure that you have all requirements installe
287287
Make sure that you made `test-functional` source folder (Right click > Mark directory as > Source folder)
288288
289289
290+
## Apple Silicon (macOS): Waku fleet connection failures — disable Rosetta
291+
292+
The `wakuorg/nwaku` fleet image is `amd64`-only, so on Apple Silicon it runs under emulation.
293+
With Docker Desktop's **Rosetta** emulation enabled, the libp2p Noise handshake between the
294+
`status-go` nodes (go-libp2p) and the nwaku fleet nodes (nim-libp2p) fails, so the app nodes
295+
never connect to `boot-1`/`store`. Symptoms:
296+
297+
- status-go logs: `failed to negotiate security protocol: ... chacha20poly1305: message authentication failed`
298+
- nwaku (boot-1/store) logs: `decryptWithAd failed tag authentication.`
299+
- Tests that depend on the fleet (store/history sync, communities, anything store-backed) hang
300+
or fail, and the store node never receives messages.
301+
302+
The handshake only fails across go-libp2p ↔ nim-libp2p (go↔go and nim↔nim connections work),
303+
which points at Rosetta's emulation of the crypto path.
304+
305+
**Fix:** Docker Desktop → Settings → General → uncheck
306+
**"Use Rosetta for x86_64/amd64 emulation on Apple Silicon"**, then restart Docker Desktop and
307+
recreate the fleet:
308+
309+
```sh
310+
docker compose -f tests-functional/docker-compose.anvil.yml -f tests-functional/docker-compose.waku.yml up --build --remove-orphans -d
311+
```
312+
313+
With the default (QEMU) emulation the go↔nim handshake succeeds.
314+
315+
290316
## Reliability tests
291317
292318
This framework also hosts a suite of reliability tests. See more info [here](https://github.com/status-im/status-go/blob/develop/tests-functional/tests/reliability/README.MD)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Client for the logos-delivery store REST API.
2+
3+
The transport that status-go currently runs on (and that backs the functional
4+
test fleet) is historically called "waku". The project term going forward is
5+
"logos delivery", so new code uses that name. The fleet store node exposes the
6+
nwaku REST API (`/store/v3/messages`); this client queries it so tests can
7+
assert that messages were actually persisted by the store node, instead of
8+
sleeping and hoping.
9+
"""
10+
11+
import logging
12+
import time
13+
14+
import docker
15+
import requests
16+
from tenacity import retry, stop_after_attempt, wait_fixed
17+
18+
from utils.config import Config
19+
20+
logger = logging.getLogger(__name__)
21+
22+
# Container port the nwaku store node serves its REST API on (mapped to an
23+
# ephemeral host port by docker-compose.waku.yml).
24+
STORE_REST_CONTAINER_PORT = "8645/tcp"
25+
26+
27+
class LogosDeliveryClient:
28+
def __init__(self, base_url: str | None = None, timeout: float = 10.0):
29+
# The store REST port is published on an ephemeral host port to avoid
30+
# collisions on shared CI hosts, so discover it at runtime via the
31+
# Docker API (same approach as clients/anvil.py). An explicit base_url
32+
# can still be passed (e.g. for --status_backend_url style setups).
33+
self.base_url = (base_url or self._discover_store_url()).rstrip("/")
34+
self.timeout = timeout
35+
36+
@retry(stop=stop_after_attempt(15), wait=wait_fixed(0.2), reraise=True)
37+
def _discover_store_url(self) -> str:
38+
docker_client = docker.from_env()
39+
project = Config.docker_project_name
40+
network = docker_client.networks.get(f"{project}_default")
41+
prefix = f"{project}-store"
42+
store = next((c for c in network.containers if c.name and prefix in c.name), None)
43+
if store is None:
44+
raise RuntimeError(f"store container ('{prefix}*') not found")
45+
mappings = store.attrs["NetworkSettings"]["Ports"].get(STORE_REST_CONTAINER_PORT) or []
46+
if not mappings:
47+
raise RuntimeError("store REST port is not exposed")
48+
host_ip = mappings[0]["HostIp"] or "127.0.0.1"
49+
if host_ip == "0.0.0.0":
50+
host_ip = "127.0.0.1"
51+
return f"http://{host_ip}:{mappings[0]['HostPort']}"
52+
53+
def get_messages(
54+
self,
55+
*,
56+
content_topics: list[str] | None = None,
57+
pubsub_topic: str | None = None,
58+
start_time: int | None = None,
59+
end_time: int | None = None,
60+
hashes: list[str] | None = None,
61+
page_size: int = 100,
62+
ascending: bool = True,
63+
include_data: bool = True,
64+
) -> list[dict]:
65+
"""Return all stored messages matching the filters, following pagination.
66+
67+
Times are Unix nanoseconds (matching the store node's timestamps).
68+
"""
69+
params: dict[str, str] = {
70+
"pageSize": str(page_size),
71+
"ascending": "true" if ascending else "false",
72+
"includeData": "true" if include_data else "false",
73+
}
74+
if content_topics:
75+
params["contentTopics"] = ",".join(content_topics)
76+
if pubsub_topic:
77+
params["pubsubTopic"] = pubsub_topic
78+
if start_time is not None:
79+
params["startTime"] = str(start_time)
80+
if end_time is not None:
81+
params["endTime"] = str(end_time)
82+
if hashes:
83+
params["hashes"] = ",".join(hashes)
84+
85+
messages: list[dict] = []
86+
cursor = None
87+
while True:
88+
page_params = dict(params)
89+
if cursor:
90+
page_params["cursor"] = cursor
91+
response = requests.get(f"{self.base_url}/store/v3/messages", params=page_params, timeout=self.timeout)
92+
response.raise_for_status()
93+
body = response.json()
94+
messages.extend(body.get("messages", []) or [])
95+
cursor = body.get("paginationCursor") or body.get("cursor")
96+
if not cursor:
97+
break
98+
return messages
99+
100+
def count_messages(self, **kwargs) -> int:
101+
return len(self.get_messages(**kwargs))
102+
103+
def get_peers(self) -> list[dict]:
104+
response = requests.get(f"{self.base_url}/admin/v1/peers", timeout=self.timeout)
105+
response.raise_for_status()
106+
return response.json()
107+
108+
def wait_for_message_count(self, expected_count: int, *, timeout: float = 60.0, poll_interval: float = 2.0, **kwargs) -> list[dict]:
109+
"""Poll the store until at least *expected_count* messages match, or time out."""
110+
deadline = time.time() + timeout
111+
messages: list[dict] = []
112+
while time.time() < deadline:
113+
messages = self.get_messages(**kwargs)
114+
if len(messages) >= expected_count:
115+
return messages
116+
time.sleep(poll_interval)
117+
return messages

0 commit comments

Comments
 (0)