]> git.giacomolli.uk - onechat/blob - whatsapp_fake_helper_test.go
Reflow the comments of promptTools and terminal
[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 sends to connects, and each pairing sends its channel to
71 // pairings, so that the test gives the items of the pairing. A connection fails
72 // with connectErr, a logout with logoutErr, and a send with sendErr. groups are
73 // the groups of the account. A send that does not fail gets the time
74 // 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 }
89
90 // ConnectContext tells the test that the daemon connected.
91 func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
92 f.connects <- struct{}{}
93
94 f.mutex.Lock()
95 defer f.mutex.Unlock()
96
97 return f.connectErr
98 }
99
100 // Disconnect does nothing.
101 func (f *WhatsAppFake) Disconnect() {
102 }
103
104 // GetQRChannel gives a new channel of pairing items to the test.
105 func (f *WhatsAppFake) GetQRChannel(ctx context.Context) (<-chan whatsmeow.QRChannelItem, error) {
106 items := make(chan whatsmeow.QRChannelItem, 8)
107 f.pairings <- items
108
109 return items, nil
110 }
111
112 // PairPhone keeps phone and returns whatsappTestCode.
113 func (f *WhatsAppFake) PairPhone(ctx context.Context, phone string, showPushNotification bool, clientType whatsmeow.PairClientType, clientDisplayName string) (string, error) {
114 f.mutex.Lock()
115 defer f.mutex.Unlock()
116
117 f.phones = append(f.phones, phone)
118
119 return whatsappTestCode, nil
120 }
121
122 // Logout counts the logouts and returns logoutErr.
123 func (f *WhatsAppFake) Logout(ctx context.Context) error {
124 f.mutex.Lock()
125 defer f.mutex.Unlock()
126
127 f.logouts++
128
129 return f.logoutErr
130 }
131
132 // GetJoinedGroups returns groups.
133 func (f *WhatsAppFake) GetJoinedGroups(ctx context.Context) ([]*types.GroupInfo, error) {
134 f.mutex.Lock()
135 defer f.mutex.Unlock()
136
137 return f.groups, nil
138 }
139
140 // GenerateMessageID returns a new message ID.
141 func (f *WhatsAppFake) GenerateMessageID() types.MessageID {
142 f.mutex.Lock()
143 defer f.mutex.Unlock()
144
145 return fmt.Sprintf("MSG%d", len(f.sent)+1)
146 }
147
148 // SendMessage keeps the send and returns sendErr.
149 func (f *WhatsAppFake) SendMessage(ctx context.Context, to types.JID, message *waE2E.Message, extra ...whatsmeow.SendRequestExtra) (whatsmeow.SendResponse, error) {
150 f.mutex.Lock()
151 defer f.mutex.Unlock()
152
153 f.sent = append(f.sent, WhatsAppFakeSend{
154 to: to.String(),
155 text: message.GetConversation(),
156 id: extra[0].ID,
157 })
158
159 if f.sendErr != nil {
160 return whatsmeow.SendResponse{}, f.sendErr
161 }
162
163 return whatsmeow.SendResponse{
164 Timestamp: whatsappTestSendTime,
165 }, nil
166 }
167
168 // Sent returns the sends.
169 func (f *WhatsAppFake) Sent() []WhatsAppFakeSend {
170 f.mutex.Lock()
171 defer f.mutex.Unlock()
172
173 return slices.Clone(f.sent)
174 }
175
176 // Phones returns the phone numbers of the pairings.
177 func (f *WhatsAppFake) Phones() []string {
178 f.mutex.Lock()
179 defer f.mutex.Unlock()
180
181 return append([]string{}, f.phones...)
182 }
183
184 // Logouts returns the number of logouts.
185 func (f *WhatsAppFake) Logouts() int {
186 f.mutex.Lock()
187 defer f.mutex.Unlock()
188
189 return f.logouts
190 }
191
192 // Emit gives event to the handler of the daemon and returns its answer.
193 func (f *WhatsAppFake) Emit(event any) bool {
194 f.mutex.Lock()
195 handle := f.handle
196 f.mutex.Unlock()
197
198 return handle(event)
199 }
200
201 // newClient keeps handle and returns the fake as the client of device.
202 func (f *WhatsAppFake) newClient(device *store.Device, handle func(event any) bool) WhatsAppClient {
203 f.mutex.Lock()
204 defer f.mutex.Unlock()
205
206 f.handle = handle
207
208 return f
209 }
210
211 // WhatsAppFakeSend is one send of the fake: the chat, the text, and the message
212 // ID.
213 type WhatsAppFakeSend struct {
214 to string
215 text string
216 id string
217 }
218
219 // whatsappStartTest opens the WhatsApp of the data directory dir with a fake
220 // client and the test clock, and closes it at the end of the test. The daemon
221 // log goes to the returned buffer.
222 func whatsappStartTest(t *testing.T, dir string, clock *TestClock) (*WhatsApp, *WhatsAppFake, *SyncBuffer) {
223 t.Helper()
224
225 fake := &WhatsAppFake{
226 connects: make(chan struct{}, 16),
227 pairings: make(chan chan whatsmeow.QRChannelItem, 16),
228 }
229
230 logs := &SyncBuffer{}
231 whatsapp, err := openWhatsApp(t.Context(), dir, clock.Now, log.New(logs, "", 0), fake.newClient)
232 require.NoError(t, err)
233
234 t.Cleanup(func() {
235 require.NoError(t, whatsapp.Close())
236 })
237
238 return whatsapp, fake, logs
239 }
240
241 // whatsappTestSession writes a session database with mode 0600 to the data
242 // directory dir. When linked is true, it holds a device linked to
243 // whatsappTestNumber and whatsappTestLID, and else no device.
244 func whatsappTestSession(t *testing.T, dir string, linked bool) {
245 t.Helper()
246
247 path := filepath.Join(dir, whatsappDirName, whatsappSessionFileName)
248 require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
249 require.NoError(t, os.WriteFile(path, nil, 0o600))
250
251 address, err := whatsappSessionAddress(path)
252 require.NoError(t, err)
253
254 container, err := sqlstore.New(t.Context(), "sqlite", address, waLog.Noop)
255 require.NoError(t, err)
256
257 if linked {
258 device := container.NewDevice()
259 jid := types.NewADJID(whatsappTestNumber, 0, 1)
260 device.ID = &jid
261 device.LID = types.NewADJID(whatsappTestLID, 0, 1)
262 device.LID.Server = types.HiddenUserServer
263 device.Account = &waAdv.ADVSignedDeviceIdentity{
264 Details: []byte{},
265 AccountSignature: make([]byte, 64),
266 AccountSignatureKey: make([]byte, 32),
267 DeviceSignature: make([]byte, 64),
268 }
269 require.NoError(t, device.Save(t.Context()))
270 }
271
272 require.NoError(t, container.Close())
273 }
274
275 // whatsappDiscardLog is a logger for a WhatsApp whose log a test does not read.
276 func whatsappDiscardLog() *log.Logger {
277 return log.New(io.Discard, "", 0)
278 }
279
280 // whatsappTestMessage returns the event of the text message id of sender in
281 // chat at the time at. A sender with whatsappTestNumber is the user.
282 func whatsappTestMessage(chat, sender types.JID, id, text string, at time.Time) *events.Message {
283 return &events.Message{
284 Info: types.MessageInfo{
285 MessageSource: types.MessageSource{
286 Chat: chat,
287 Sender: sender,
288 IsFromMe: sender.User == whatsappTestNumber,
289 IsGroup: chat.Server == types.GroupServer,
290 },
291 ID: id,
292 Timestamp: at,
293 },
294 Message: &waE2E.Message{
295 Conversation: new(text),
296 },
297 }
298 }