]> git.giacomolli.uk - onechat/blob - whatsapp_fake_helper_test.go
Send the compaction view in block messages
[onechat] / whatsapp_fake_helper_test.go
1 package main
2
3 import (
4         "context"
5         "fmt"
6         "io"
7         "log"
8         "os"
9         "path/filepath"
10         "slices"
11         "sync"
12         "testing"
13         "time"
14
15         "github.com/stretchr/testify/require"
16         "go.mau.fi/whatsmeow"
17         waAdv "go.mau.fi/whatsmeow/proto/waAdv"
18         "go.mau.fi/whatsmeow/proto/waE2E"
19         "go.mau.fi/whatsmeow/store"
20         "go.mau.fi/whatsmeow/store/sqlstore"
21         "go.mau.fi/whatsmeow/types"
22         "go.mau.fi/whatsmeow/types/events"
23         waLog "go.mau.fi/whatsmeow/util/log"
24 )
25
26 const (
27         // whatsappTestNumber is the phone number of the own account in the
28         // tests. It is a number for fiction.
29         whatsappTestNumber = "15550001111"
30
31         // whatsappTestLID is the LID of the own account in the tests.
32         whatsappTestLID = "987654321012345"
33
34         // whatsappTestCode is the pairing code of the fake.
35         whatsappTestCode = "ABCD-EFGH"
36 )
37
38 var (
39         // whatsappTestOwn is the own account in the tests.
40         whatsappTestOwn = types.NewJID(whatsappTestNumber, types.DefaultUserServer)
41
42         // whatsappTestAna is a contact in the tests.
43         whatsappTestAna = types.NewJID("15550002222", types.DefaultUserServer)
44
45         // whatsappTestBruno is a member of the test group.
46         whatsappTestBruno = types.NewJID("15550003333", types.DefaultUserServer)
47
48         // whatsappTestFamily is the test group.
49         whatsappTestFamily = types.NewJID("120363000000000001", types.GroupServer)
50
51         // whatsappTestChat is the private chat with whatsappTestAna.
52         whatsappTestChat = WhatsAppChat{
53                 ID:   whatsappTestAna.String(),
54                 Name: "Ana Souza",
55         }
56
57         // whatsappTestSendTime is the time of the sends of the fake.
58         whatsappTestSendTime = time.Date(2026, 1, 1, 12, 5, 0, 0, time.UTC)
59
60         // whatsappTestGroup is the chat of whatsappTestFamily.
61         whatsappTestGroup = WhatsAppChat{
62                 ID:    whatsappTestFamily.String(),
63                 Name:  "Family",
64                 Group: true,
65         }
66 )
67
68 // WhatsAppFake is a fake WhatsApp client. It keeps the event handler that the
69 // daemon gives it, and Emit gives events to the handler, as the library does.
70 // Each connection keeps its context and sends to connects, and each pairing
71 // sends its channel to pairings, so that the test gives the items of the
72 // pairing. A connection fails with connectErr, a logout with logoutErr, and a
73 // send with sendErr. groups are the groups of the account. A send that does not
74 // fail gets the time whatsappTestSendTime.
75 type WhatsAppFake struct {
76         connects chan struct{}
77         pairings chan chan whatsmeow.QRChannelItem
78
79         mutex       sync.Mutex
80         handle      func(event any) bool
81         connectErr  error
82         logoutErr   error
83         groups      []*types.GroupInfo
84         sendErr     error
85         phones      []string
86         logouts     int
87         sent        []WhatsAppFakeSend
88         connections []context.Context
89 }
90
91 // ConnectContext keeps ctx and tells the test that the daemon connected.
92 func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
93         f.mutex.Lock()
94         f.connections = append(f.connections, ctx)
95         f.mutex.Unlock()
96
97         f.connects <- struct{}{}
98
99         f.mutex.Lock()
100         defer f.mutex.Unlock()
101
102         return f.connectErr
103 }
104
105 // Connections returns the contexts of the connections, in order.
106 func (f *WhatsAppFake) Connections() []context.Context {
107         f.mutex.Lock()
108         defer f.mutex.Unlock()
109
110         return slices.Clone(f.connections)
111 }
112
113 // Disconnect does nothing.
114 func (f *WhatsAppFake) Disconnect() {
115 }
116
117 // GetQRChannel gives a new channel of pairing items to the test.
118 func (f *WhatsAppFake) GetQRChannel(ctx context.Context) (<-chan whatsmeow.QRChannelItem, error) {
119         items := make(chan whatsmeow.QRChannelItem, 8)
120         f.pairings <- items
121
122         return items, nil
123 }
124
125 // PairPhone keeps phone and returns whatsappTestCode.
126 func (f *WhatsAppFake) PairPhone(ctx context.Context, phone string, showPushNotification bool, clientType whatsmeow.PairClientType, clientDisplayName string) (string, error) {
127         f.mutex.Lock()
128         defer f.mutex.Unlock()
129
130         f.phones = append(f.phones, phone)
131
132         return whatsappTestCode, nil
133 }
134
135 // Logout counts the logouts and returns logoutErr.
136 func (f *WhatsAppFake) Logout(ctx context.Context) error {
137         f.mutex.Lock()
138         defer f.mutex.Unlock()
139
140         f.logouts++
141
142         return f.logoutErr
143 }
144
145 // GetJoinedGroups returns groups.
146 func (f *WhatsAppFake) GetJoinedGroups(ctx context.Context) ([]*types.GroupInfo, error) {
147         f.mutex.Lock()
148         defer f.mutex.Unlock()
149
150         return f.groups, nil
151 }
152
153 // GenerateMessageID returns a new message ID.
154 func (f *WhatsAppFake) GenerateMessageID() types.MessageID {
155         f.mutex.Lock()
156         defer f.mutex.Unlock()
157
158         return fmt.Sprintf("MSG%d", len(f.sent)+1)
159 }
160
161 // SendMessage keeps the send and returns sendErr.
162 func (f *WhatsAppFake) SendMessage(ctx context.Context, to types.JID, message *waE2E.Message, extra ...whatsmeow.SendRequestExtra) (whatsmeow.SendResponse, error) {
163         f.mutex.Lock()
164         defer f.mutex.Unlock()
165
166         f.sent = append(f.sent, WhatsAppFakeSend{
167                 to:   to.String(),
168                 text: message.GetConversation(),
169                 id:   extra[0].ID,
170         })
171
172         if f.sendErr != nil {
173                 return whatsmeow.SendResponse{}, f.sendErr
174         }
175
176         return whatsmeow.SendResponse{
177                 Timestamp: whatsappTestSendTime,
178         }, nil
179 }
180
181 // Sent returns the sends.
182 func (f *WhatsAppFake) Sent() []WhatsAppFakeSend {
183         f.mutex.Lock()
184         defer f.mutex.Unlock()
185
186         return slices.Clone(f.sent)
187 }
188
189 // Phones returns the phone numbers of the pairings.
190 func (f *WhatsAppFake) Phones() []string {
191         f.mutex.Lock()
192         defer f.mutex.Unlock()
193
194         return append([]string{}, f.phones...)
195 }
196
197 // Logouts returns the number of logouts.
198 func (f *WhatsAppFake) Logouts() int {
199         f.mutex.Lock()
200         defer f.mutex.Unlock()
201
202         return f.logouts
203 }
204
205 // Emit gives event to the handler of the daemon and returns its answer.
206 func (f *WhatsAppFake) Emit(event any) bool {
207         f.mutex.Lock()
208         handle := f.handle
209         f.mutex.Unlock()
210
211         return handle(event)
212 }
213
214 // newClient keeps handle and returns the fake as the client of device.
215 func (f *WhatsAppFake) newClient(device *store.Device, handle func(event any) bool) WhatsAppClient {
216         f.mutex.Lock()
217         defer f.mutex.Unlock()
218
219         f.handle = handle
220
221         return f
222 }
223
224 // WhatsAppFakeSend is one send of the fake: the chat, the text, and the message
225 // ID.
226 type WhatsAppFakeSend struct {
227         to   string
228         text string
229         id   string
230 }
231
232 // whatsappStartTest opens the WhatsApp of the data directory dir with a fake
233 // client and the test clock, and closes it at the end of the test. The daemon
234 // log goes to the returned buffer.
235 func whatsappStartTest(t *testing.T, dir string, clock *TestClock) (*WhatsApp, *WhatsAppFake, *SyncBuffer) {
236         t.Helper()
237
238         fake := &WhatsAppFake{
239                 connects: make(chan struct{}, 16),
240                 pairings: make(chan chan whatsmeow.QRChannelItem, 16),
241         }
242
243         logs := &SyncBuffer{}
244         whatsapp, err := openWhatsApp(t.Context(), dir, clock.Now, log.New(logs, "", 0), fake.newClient)
245         require.NoError(t, err)
246
247         t.Cleanup(func() {
248                 require.NoError(t, whatsapp.Close())
249         })
250
251         return whatsapp, fake, logs
252 }
253
254 // whatsappTestSession writes a session database with mode 0600 to the data
255 // directory dir. When linked is true, it holds a device linked to
256 // whatsappTestNumber and whatsappTestLID, and else no device.
257 func whatsappTestSession(t *testing.T, dir string, linked bool) {
258         t.Helper()
259
260         path := filepath.Join(dir, whatsappDirName, whatsappSessionFileName)
261         require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
262         require.NoError(t, os.WriteFile(path, nil, 0o600))
263
264         address, err := whatsappSessionAddress(path)
265         require.NoError(t, err)
266
267         container, err := sqlstore.New(t.Context(), "sqlite", address, waLog.Noop)
268         require.NoError(t, err)
269
270         if linked {
271                 device := container.NewDevice()
272                 jid := types.NewADJID(whatsappTestNumber, 0, 1)
273                 device.ID = &jid
274                 device.LID = types.NewADJID(whatsappTestLID, 0, 1)
275                 device.LID.Server = types.HiddenUserServer
276                 device.Account = &waAdv.ADVSignedDeviceIdentity{
277                         Details:             []byte{},
278                         AccountSignature:    make([]byte, 64),
279                         AccountSignatureKey: make([]byte, 32),
280                         DeviceSignature:     make([]byte, 64),
281                 }
282                 require.NoError(t, device.Save(t.Context()))
283         }
284
285         require.NoError(t, container.Close())
286 }
287
288 // whatsappDiscardLog is a logger for a WhatsApp whose log a test does not read.
289 func whatsappDiscardLog() *log.Logger {
290         return log.New(io.Discard, "", 0)
291 }
292
293 // whatsappTestMessage returns the event of the text message id of sender in
294 // chat at the time at. A sender with whatsappTestNumber is the user.
295 func whatsappTestMessage(chat, sender types.JID, id, text string, at time.Time) *events.Message {
296         return &events.Message{
297                 Info: types.MessageInfo{
298                         MessageSource: types.MessageSource{
299                                 Chat:     chat,
300                                 Sender:   sender,
301                                 IsFromMe: sender.User == whatsappTestNumber,
302                                 IsGroup:  chat.Server == types.GroupServer,
303                         },
304                         ID:        id,
305                         Timestamp: at,
306                 },
307                 Message: &waE2E.Message{
308                         Conversation: new(text),
309                 },
310         }
311 }
312
313 // whatsappTestLogTexts returns the texts of the messages in the log of memory,
314 // after the log holds at least one message.
315 func whatsappTestLogTexts(t *testing.T, memory *Memory) []string {
316         t.Helper()
317
318         require.Eventually(t, func() bool {
319                 return memory.Len() > 0
320         }, 10*time.Second, time.Millisecond)
321
322         var texts []string
323         for id := range memory.Len() {
324                 message, err := memory.Message(id)
325                 require.NoError(t, err)
326
327                 texts = append(texts, message.Text)
328         }
329
330         return texts
331 }
332
333 // whatsappStartTestLinked opens a WhatsApp with a linked device in a temporary
334 // directory, with a quiet time of 50 ms for its batches.
335 func whatsappStartTestLinked(t *testing.T) (*WhatsApp, *WhatsAppFake) {
336         t.Helper()
337
338         dir := t.TempDir()
339         whatsappTestSession(t, dir, true)
340         whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
341         whatsapp.state.quiet = 50 * time.Millisecond
342
343         return whatsapp, fake
344 }
345
346 // whatsappTestConnect waits for the connection of the daemon and gives it the
347 // events of a connection with an empty catch-up.
348 func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
349         t.Helper()
350
351         <-fake.connects
352         require.True(t, fake.Emit(&events.Connected{}))
353         require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
354 }
355
356 // whatsappTestContacts puts the contacts of the tests into the session of
357 // whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
358 func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
359         t.Helper()
360
361         contacts := whatsapp.device.Contacts
362         require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
363         require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
364         _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
365         require.NoError(t, err)
366         require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))
367 }