]> git.giacomolli.uk - onechat/blob - whatsapp_fake_helper_test.go
Check spacing between Go statement blocks
[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 takes its error from connectErrors, then connectErr. A
73 // logout fails with logoutErr, and a send with sendErr. groups are the groups
74 // of the account. A send that does not 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 connectErrors []error
83 connectTimes []time.Time
84 logoutErr error
85 groups []*types.GroupInfo
86 sendErr error
87 phones []string
88 logouts int
89 sent []WhatsAppFakeSend
90 connections []context.Context
91 }
92
93 // ConnectContext keeps ctx and tells the test that the daemon connected.
94 func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
95 f.mutex.Lock()
96
97 f.connections = append(f.connections, ctx)
98 f.connectTimes = append(f.connectTimes, time.Now())
99 f.mutex.Unlock()
100
101 f.connects <- struct{}{}
102
103 f.mutex.Lock()
104 defer f.mutex.Unlock()
105
106 if len(f.connectErrors) > 0 {
107 err := f.connectErrors[0]
108 f.connectErrors = f.connectErrors[1:]
109
110 return err
111 }
112
113 return f.connectErr
114 }
115
116 // Connections returns the contexts of the connections, in order.
117 func (f *WhatsAppFake) Connections() []context.Context {
118 f.mutex.Lock()
119 defer f.mutex.Unlock()
120
121 return slices.Clone(f.connections)
122 }
123
124 // ConnectionTimes returns the time of each connection call, in order.
125 func (f *WhatsAppFake) ConnectionTimes() []time.Time {
126 f.mutex.Lock()
127 defer f.mutex.Unlock()
128
129 return slices.Clone(f.connectTimes)
130 }
131
132 // Disconnect does nothing.
133 func (f *WhatsAppFake) Disconnect() {
134 }
135
136 // GetQRChannel gives a new channel of pairing items to the test.
137 func (f *WhatsAppFake) GetQRChannel(_ context.Context) (<-chan whatsmeow.QRChannelItem, error) {
138 items := make(chan whatsmeow.QRChannelItem, 8)
139 f.pairings <- items
140
141 return items, nil
142 }
143
144 // PairPhone keeps phone and returns whatsappTestCode.
145 func (f *WhatsAppFake) PairPhone(_ context.Context, phone string, _ bool, _ whatsmeow.PairClientType, _ string) (string, error) {
146 f.mutex.Lock()
147 defer f.mutex.Unlock()
148
149 f.phones = append(f.phones, phone)
150
151 return whatsappTestCode, nil
152 }
153
154 // Logout counts the logouts and returns logoutErr.
155 func (f *WhatsAppFake) Logout(_ context.Context) error {
156 f.mutex.Lock()
157 defer f.mutex.Unlock()
158
159 f.logouts++
160
161 return f.logoutErr
162 }
163
164 // GetJoinedGroups returns groups.
165 func (f *WhatsAppFake) GetJoinedGroups(_ context.Context) ([]*types.GroupInfo, error) {
166 f.mutex.Lock()
167 defer f.mutex.Unlock()
168
169 return f.groups, nil
170 }
171
172 // GenerateMessageID returns a new message ID.
173 func (f *WhatsAppFake) GenerateMessageID() types.MessageID {
174 f.mutex.Lock()
175 defer f.mutex.Unlock()
176
177 return fmt.Sprintf("MSG%d", len(f.sent)+1)
178 }
179
180 // SendMessage keeps the send and returns sendErr.
181 func (f *WhatsAppFake) SendMessage(_ context.Context, to types.JID, message *waE2E.Message, extra ...whatsmeow.SendRequestExtra) (whatsmeow.SendResponse, error) {
182 f.mutex.Lock()
183 defer f.mutex.Unlock()
184
185 f.sent = append(f.sent, WhatsAppFakeSend{
186 to: to.String(),
187 text: message.GetConversation(),
188 id: extra[0].ID,
189 })
190
191 if f.sendErr != nil {
192 return whatsmeow.SendResponse{}, f.sendErr
193 }
194
195 return whatsmeow.SendResponse{
196 Timestamp: whatsappTestSendTime,
197 }, nil
198 }
199
200 // Sent returns the sends.
201 func (f *WhatsAppFake) Sent() []WhatsAppFakeSend {
202 f.mutex.Lock()
203 defer f.mutex.Unlock()
204
205 return slices.Clone(f.sent)
206 }
207
208 // Phones returns the phone numbers of the pairings.
209 func (f *WhatsAppFake) Phones() []string {
210 f.mutex.Lock()
211 defer f.mutex.Unlock()
212
213 return append([]string{}, f.phones...)
214 }
215
216 // Logouts returns the number of logouts.
217 func (f *WhatsAppFake) Logouts() int {
218 f.mutex.Lock()
219 defer f.mutex.Unlock()
220
221 return f.logouts
222 }
223
224 // Emit gives event to the handler of the daemon and returns its answer.
225 func (f *WhatsAppFake) Emit(event any) bool {
226 f.mutex.Lock()
227
228 handle := f.handle
229 f.mutex.Unlock()
230
231 return handle(event)
232 }
233
234 // newClient keeps handle and returns the fake as the client of device.
235 func (f *WhatsAppFake) newClient(_ *store.Device, handle func(event any) bool) WhatsAppClient {
236 f.mutex.Lock()
237 defer f.mutex.Unlock()
238
239 f.handle = handle
240
241 return f
242 }
243
244 // WhatsAppFakeSend is one send of the fake: the chat, the text, and the message
245 // ID.
246 type WhatsAppFakeSend struct {
247 to string
248 text string
249 id string
250 }
251
252 // whatsappStartTest opens the WhatsApp of the data directory dir with a fake
253 // client and the test clock, and closes it at the end of the test. The daemon
254 // log goes to the returned buffer.
255 func whatsappStartTest(t *testing.T, dir string, clock *TestClock) (*WhatsApp, *WhatsAppFake, *SyncBuffer) {
256 t.Helper()
257
258 fake := &WhatsAppFake{
259 connects: make(chan struct{}, 16),
260 pairings: make(chan chan whatsmeow.QRChannelItem, 16),
261 }
262
263 logs := &SyncBuffer{}
264 whatsapp, err := openWhatsApp(t.Context(), dir, clock.Now, log.New(logs, "", 0), fake.newClient)
265 require.NoError(t, err)
266
267 t.Cleanup(func() {
268 require.NoError(t, whatsapp.Close())
269 })
270
271 return whatsapp, fake, logs
272 }
273
274 // whatsappTestSession writes a session database with mode 0600 to the data
275 // directory dir. When linked is true, it holds a device linked to
276 // whatsappTestNumber and whatsappTestLID, and else no device.
277 func whatsappTestSession(t *testing.T, dir string, linked bool) {
278 t.Helper()
279
280 path := filepath.Join(dir, whatsappDirName, whatsappSessionFileName)
281 require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
282 require.NoError(t, os.WriteFile(path, nil, 0o600))
283
284 address, err := whatsappSessionAddress(path)
285 require.NoError(t, err)
286
287 container, err := sqlstore.New(t.Context(), "sqlite", address, waLog.Noop)
288 require.NoError(t, err)
289
290 if linked {
291 device := container.NewDevice()
292 jid := types.NewADJID(whatsappTestNumber, 0, 1)
293 device.ID = &jid
294 device.LID = types.NewADJID(whatsappTestLID, 0, 1)
295 device.LID.Server = types.HiddenUserServer
296 device.Account = &waAdv.ADVSignedDeviceIdentity{
297 Details: []byte{},
298 AccountSignature: make([]byte, 64),
299 AccountSignatureKey: make([]byte, 32),
300 DeviceSignature: make([]byte, 64),
301 }
302 require.NoError(t, device.Save(t.Context()))
303 }
304
305 require.NoError(t, container.Close())
306 }
307
308 // whatsappDiscardLog is a logger for a WhatsApp whose log a test does not read.
309 func whatsappDiscardLog() *log.Logger {
310 return log.New(io.Discard, "", 0)
311 }
312
313 // whatsappTestMessage returns the event of the text message id of sender in
314 // chat at the time at. A sender with whatsappTestNumber is the user.
315 func whatsappTestMessage(chat, sender types.JID, id, text string, at time.Time) *events.Message {
316 return &events.Message{
317 Info: types.MessageInfo{
318 MessageSource: types.MessageSource{
319 Chat: chat,
320 Sender: sender,
321 IsFromMe: sender.User == whatsappTestNumber,
322 IsGroup: chat.Server == types.GroupServer,
323 },
324 ID: id,
325 Timestamp: at,
326 },
327 Message: &waE2E.Message{
328 Conversation: new(text),
329 },
330 }
331 }
332
333 // whatsappTestLogTexts returns the texts of the messages in the log of memory,
334 // after the log holds at least one message.
335 func whatsappTestLogTexts(t *testing.T, memory *Memory) []string {
336 t.Helper()
337
338 require.Eventually(t, func() bool {
339 return memory.Len() > 0
340 }, 10*time.Second, time.Millisecond)
341
342 var texts []string
343
344 for id := range memory.Len() {
345 message, err := memory.Message(id)
346 require.NoError(t, err)
347
348 texts = append(texts, message.Text)
349 }
350
351 return texts
352 }
353
354 // whatsappStartTestLinked opens a WhatsApp with a linked device in a temporary
355 // directory, with a quiet time of 50 ms for its batches.
356 func whatsappStartTestLinked(t *testing.T) (*WhatsApp, *WhatsAppFake) {
357 t.Helper()
358
359 dir := t.TempDir()
360 whatsappTestSession(t, dir, true)
361
362 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
363 whatsapp.state.quiet = 50 * time.Millisecond
364
365 return whatsapp, fake
366 }
367
368 // whatsappTestConnect waits for the connection of the daemon and gives it the
369 // events of a connection with an empty catch-up.
370 func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
371 t.Helper()
372
373 <-fake.connects
374 require.True(t, fake.Emit(&events.Connected{}))
375 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
376 }
377
378 // whatsappTestContacts puts the contacts of the tests into the session of
379 // whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
380 func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
381 t.Helper()
382
383 contacts := whatsapp.device.Contacts
384 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
385 require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
386
387 _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
388 require.NoError(t, err)
389 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))
390 }