15 "github.com/stretchr/testify/require"
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"
27 // whatsappTestNumber is the phone number of the own account in the
28 // tests. It is a number for fiction.
29 whatsappTestNumber = "15550001111"
31 // whatsappTestLID is the LID of the own account in the tests.
32 whatsappTestLID = "987654321012345"
34 // whatsappTestCode is the pairing code of the fake.
35 whatsappTestCode = "ABCD-EFGH"
39 // whatsappTestOwn is the own account in the tests.
40 whatsappTestOwn = types.NewJID(whatsappTestNumber, types.DefaultUserServer)
42 // whatsappTestAna is a contact in the tests.
43 whatsappTestAna = types.NewJID("15550002222", types.DefaultUserServer)
45 // whatsappTestBruno is a member of the test group.
46 whatsappTestBruno = types.NewJID("15550003333", types.DefaultUserServer)
48 // whatsappTestFamily is the test group.
49 whatsappTestFamily = types.NewJID("120363000000000001", types.GroupServer)
51 // whatsappTestChat is the private chat with whatsappTestAna.
52 whatsappTestChat = WhatsAppChat{
53 ID: whatsappTestAna.String(),
57 // whatsappTestSendTime is the time of the sends of the fake.
58 whatsappTestSendTime = time.Date(2026, 1, 1, 12, 5, 0, 0, time.UTC)
60 // whatsappTestGroup is the chat of whatsappTestFamily.
61 whatsappTestGroup = WhatsAppChat{
62 ID: whatsappTestFamily.String(),
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
80 handle func(event any) bool
83 groups []*types.GroupInfo
87 sent []WhatsAppFakeSend
88 connections []context.Context
91 // ConnectContext keeps ctx and tells the test that the daemon connected.
92 func (f *WhatsAppFake) ConnectContext(ctx context.Context) error {
94 f.connections = append(f.connections, ctx)
97 f.connects <- struct{}{}
100 defer f.mutex.Unlock()
105 // Connections returns the contexts of the connections, in order.
106 func (f *WhatsAppFake) Connections() []context.Context {
108 defer f.mutex.Unlock()
110 return slices.Clone(f.connections)
113 // Disconnect does nothing.
114 func (f *WhatsAppFake) Disconnect() {
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)
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) {
128 defer f.mutex.Unlock()
130 f.phones = append(f.phones, phone)
132 return whatsappTestCode, nil
135 // Logout counts the logouts and returns logoutErr.
136 func (f *WhatsAppFake) Logout(ctx context.Context) error {
138 defer f.mutex.Unlock()
145 // GetJoinedGroups returns groups.
146 func (f *WhatsAppFake) GetJoinedGroups(ctx context.Context) ([]*types.GroupInfo, error) {
148 defer f.mutex.Unlock()
153 // GenerateMessageID returns a new message ID.
154 func (f *WhatsAppFake) GenerateMessageID() types.MessageID {
156 defer f.mutex.Unlock()
158 return fmt.Sprintf("MSG%d", len(f.sent)+1)
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) {
164 defer f.mutex.Unlock()
166 f.sent = append(f.sent, WhatsAppFakeSend{
168 text: message.GetConversation(),
172 if f.sendErr != nil {
173 return whatsmeow.SendResponse{}, f.sendErr
176 return whatsmeow.SendResponse{
177 Timestamp: whatsappTestSendTime,
181 // Sent returns the sends.
182 func (f *WhatsAppFake) Sent() []WhatsAppFakeSend {
184 defer f.mutex.Unlock()
186 return slices.Clone(f.sent)
189 // Phones returns the phone numbers of the pairings.
190 func (f *WhatsAppFake) Phones() []string {
192 defer f.mutex.Unlock()
194 return append([]string{}, f.phones...)
197 // Logouts returns the number of logouts.
198 func (f *WhatsAppFake) Logouts() int {
200 defer f.mutex.Unlock()
205 // Emit gives event to the handler of the daemon and returns its answer.
206 func (f *WhatsAppFake) Emit(event any) bool {
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 {
217 defer f.mutex.Unlock()
224 // WhatsAppFakeSend is one send of the fake: the chat, the text, and the message
226 type WhatsAppFakeSend struct {
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) {
238 fake := &WhatsAppFake{
239 connects: make(chan struct{}, 16),
240 pairings: make(chan chan whatsmeow.QRChannelItem, 16),
243 logs := &SyncBuffer{}
244 whatsapp, err := openWhatsApp(t.Context(), dir, clock.Now, log.New(logs, "", 0), fake.newClient)
245 require.NoError(t, err)
248 require.NoError(t, whatsapp.Close())
251 return whatsapp, fake, logs
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) {
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))
264 address, err := whatsappSessionAddress(path)
265 require.NoError(t, err)
267 container, err := sqlstore.New(t.Context(), "sqlite", address, waLog.Noop)
268 require.NoError(t, err)
271 device := container.NewDevice()
272 jid := types.NewADJID(whatsappTestNumber, 0, 1)
274 device.LID = types.NewADJID(whatsappTestLID, 0, 1)
275 device.LID.Server = types.HiddenUserServer
276 device.Account = &waAdv.ADVSignedDeviceIdentity{
278 AccountSignature: make([]byte, 64),
279 AccountSignatureKey: make([]byte, 32),
280 DeviceSignature: make([]byte, 64),
282 require.NoError(t, device.Save(t.Context()))
285 require.NoError(t, container.Close())
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)
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{
301 IsFromMe: sender.User == whatsappTestNumber,
302 IsGroup: chat.Server == types.GroupServer,
307 Message: &waE2E.Message{
308 Conversation: new(text),