finalize multi-device

This commit is contained in:
Adhiraj Singh
2021-09-15 13:40:02 +05:30
parent 9cba28e891
commit f267f27ada
82 changed files with 35228 additions and 10644 deletions

View File

@@ -1,76 +0,0 @@
import { WAConnection, MessageOptions, MessageType, unixTimestampSeconds, toNumber, GET_MESSAGE_ID, waMessageKey } from '../WAConnection'
import * as assert from 'assert'
import {promises as fs} from 'fs'
require ('dotenv').config () // dotenv to load test jid
export const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
export const makeConnection = () => {
const conn = new WAConnection()
conn.connectOptions.maxIdleTimeMs = 15_000
conn.logger.level = 'debug'
let evCounts = {}
conn.on ('close', ({ isReconnecting }) => {
!isReconnecting && console.log ('Events registered: ', evCounts)
})
const onM = conn.on
conn.on = (...args: any[]) => {
evCounts[args[0]] = (evCounts[args[0]] || 0) + 1
return onM.apply (conn, args)
}
const offM = conn.off
conn.off = (...args: any[]) => {
evCounts[args[0]] = (evCounts[args[0]] || 0) - 1
if (evCounts[args[0]] <= 0) delete evCounts[args[0]]
return offM.apply (conn, args)
}
return conn
}
export async function sendAndRetrieveMessage(conn: WAConnection, content, type: MessageType, options: MessageOptions = {}, recipientJid = testJid) {
const response = await conn.sendMessage(recipientJid, content, type, options)
const {messages} = await conn.loadMessages(recipientJid, 10)
const message = messages.find (m => m.key.id === response.key.id)
assert.ok(message)
const chat = conn.chats.get(recipientJid)
assert.ok (chat.messages.get(GET_MESSAGE_ID(message.key)))
assert.ok (chat.t >= (unixTimestampSeconds()-5) )
return message
}
export const WAConnectionTest = (name: string, func: (conn: WAConnection) => void) => (
describe(name, () => {
const conn = new WAConnection()
conn.connectOptions.maxIdleTimeMs = 30_000
conn.logger.level = 'debug'
before(async () => {
const file = './auth_info.json'
await conn.loadAuthInfo(file).connect()
await fs.writeFile(file, JSON.stringify(conn.base64EncodedAuthInfo(), null, '\t'))
})
after(() => conn.close())
afterEach (() => assertChatDBIntegrity (conn))
func(conn)
})
)
export const assertChatDBIntegrity = (conn: WAConnection) => {
conn.chats.all ().forEach (chat => (
assert.deepStrictEqual (
[...chat.messages.all()].sort ((m1, m2) => waMessageKey.compare(waMessageKey.key(m1), waMessageKey.key(m2))),
chat.messages.all()
)
))
conn.chats.all ().forEach (chat => (
assert.deepStrictEqual (
chat.messages.all().filter (m => chat.messages.all().filter(m1 => m1.key.id === m.key.id).length > 1),
[]
)
))
}

View File

@@ -1,89 +0,0 @@
import { strict as assert } from 'assert'
import Encoder from '../Binary/Encoder'
import Decoder from '../Binary/Decoder'
describe('Binary Coding Tests', () => {
const testVectors: [string, Object][] = [
[
'f806092f5a0a10f804f80234fc6c0a350a1b39313735323938373131313740732e77686174736170702e6e657410011a143345423030393637354537454433374141424632122b0a292a7069616e6f20726f6f6d2074696d696e6773206172653a2a0a20363a3030414d2d31323a3030414d18b3faa7f3052003f80234fc4c0a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20304643454335333330463634393239433645394132434646443242433845414418bdfaa7f305c00101f80234fc930a350a1b39313735323938373131313740732e77686174736170702e6e657410011a14334542303033433742353339414644303937353312520a50536f727279206672656e2c204920636f756c646e277420756e6465727374616e6420274c69627261272e2054797065202768656c702720746f206b6e6f77207768617420616c6c20492063616e20646f18c1faa7f3052003f80234fc540a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20413132333042384436423041314437393345433241453245413043313638443812090a076c69627261727918c2faa7f305',
[
'action',
{ last: 'true', add: 'before' },
[
[
'message',
null,
{
key: { remoteJid: '917529871117@s.whatsapp.net', fromMe: true, id: '3EB009675E7ED37AABF2' },
message: { conversation: '*piano room timings are:*\n 6:00AM-12:00AM' },
messageTimestamp: '1584004403',
status: 'DELIVERY_ACK',
},
],
[
'message',
null,
{
key: {
remoteJid: '917529871117@s.whatsapp.net',
fromMe: false,
id: '0FCEC5330F64929C6E9A2CFFD2BC8EAD',
},
messageTimestamp: '1584004413',
messageStubType: 'REVOKE',
},
],
[
'message',
null,
{
key: { remoteJid: '917529871117@s.whatsapp.net', fromMe: true, id: '3EB003C7B539AFD09753' },
message: {
conversation:
"Sorry fren, I couldn't understand 'Libra'. Type 'help' to know what all I can do",
},
messageTimestamp: '1584004417',
status: 'DELIVERY_ACK',
},
],
[
'message',
null,
{
key: {
remoteJid: '917529871117@s.whatsapp.net',
fromMe: false,
id: 'A1230B8D6B0A1D793EC2AE2EA0C168D8',
},
message: { conversation: 'library' },
messageTimestamp: '1584004418',
},
],
],
],
],
[
'f8063f2dfafc0831323334353637385027fc0431323334f801f80228fc0701020304050607',
[
'picture',
{jid: '12345678@c.us', id: '1234'},
[['image', null, Buffer.from([1,2,3,4,5,6,7])]]
]
]
]
const encoder = new Encoder()
const decoder = new Decoder()
it('should decode strings', () => {
testVectors.forEach(pair => {
const buff = Buffer.from(pair[0], 'hex')
const decoded = decoder.read(buff)
//console.log((decoded[2][0][2]))
assert.deepStrictEqual(decoded, pair[1])
const encoded = encoder.write(decoded)
assert.deepStrictEqual(encoded, buff)
})
console.log('all coding tests passed')
})
})

View File

@@ -1,407 +0,0 @@
import * as assert from 'assert'
import {WAConnection} from '../WAConnection'
import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode, DisconnectReason, WAChat, WAContact } from '../WAConnection/Constants'
import { delay } from '../WAConnection/Utils'
import { assertChatDBIntegrity, makeConnection, testJid } from './Common'
describe('QR Generation', () => {
it('should generate QR', async () => {
const conn = makeConnection ()
conn.connectOptions.maxRetries = 0
let calledQR = 0
conn.removeAllListeners ('qr')
conn.on ('qr', () => calledQR += 1)
await conn.connect()
.then (() => assert.fail('should not have succeeded'))
.catch (error => {})
assert.deepStrictEqual (
Object.keys(conn.eventNames()).filter(key => key.startsWith('TAG:')),
[]
)
assert.ok(calledQR >= 2, 'QR not called')
})
})
describe('Test Connect', () => {
let auth: AuthenticationCredentialsBase64
it('should connect', async () => {
console.log('please be ready to scan with your phone')
const conn = makeConnection ()
let credentialsUpdateCalled = false
conn.on ('credentials-updated', () => credentialsUpdateCalled = true)
await conn.connect ()
assert.ok(conn.user?.jid)
assert.ok(conn.user?.phone)
assert.ok (conn.user?.imgUrl || conn.user.imgUrl === '')
assert.ok (credentialsUpdateCalled)
assertChatDBIntegrity (conn)
conn.close()
auth = conn.base64EncodedAuthInfo()
})
it('should restore session', async () => {
const conn = makeConnection ()
let credentialsUpdateCalled = false
conn.on ('credentials-updated', () => credentialsUpdateCalled = true)
await conn.loadAuthInfo (auth).connect ()
assert.ok(conn.user)
assert.ok(conn.user.jid)
assert.ok (credentialsUpdateCalled)
assertChatDBIntegrity (conn)
await conn.logout()
conn.loadAuthInfo(auth)
await conn.connect()
.then (() => assert.fail('should not have reconnected'))
.catch (err => {
assert.ok (err instanceof BaileysError)
assert.ok ((err as BaileysError).status >= 400)
})
conn.close()
})
it ('should disconnect & reconnect phone', async () => {
const conn = makeConnection ()
conn.logger.level = 'debug'
await conn.loadAuthInfo('./auth_info.json').connect ()
assert.strictEqual (conn.phoneConnected, true)
try {
const waitForEvent = expect => new Promise (resolve => {
conn.on ('connection-phone-change', ({connected}) => {
if (connected === expect) {
conn.removeAllListeners ('connection-phone-change')
resolve(undefined)
}
})
})
console.log ('disconnect your phone from the internet')
await delay (10_000)
console.log ('phone should be disconnected now, testing...')
const messagesPromise = Promise.all (
[
conn.loadMessages (testJid, 50),
conn.getStatus (testJid),
conn.getProfilePicture (testJid).catch (() => '')
]
)
await waitForEvent (false)
console.log ('reconnect your phone to the internet')
await waitForEvent (true)
console.log ('reconnected successfully')
const final = await messagesPromise
assert.ok (final)
} finally {
conn.close ()
}
})
})
describe ('Reconnects', () => {
const verifyConnectionOpen = async (conn: WAConnection) => {
assert.ok (conn.user.jid)
let failed = false
// check that the connection stays open
conn.on ('close', ({reason}) => {
if(reason !== DisconnectReason.intentional) failed = true
})
await delay (60*1000)
const status = await conn.getStatus ()
assert.ok (status)
assert.ok (!conn['debounceTimeout']) // this should be null
conn.close ()
if (failed) assert.fail ('should not have closed again')
}
it('should dispose correctly on bad_session', async () => {
const conn = makeConnection ()
conn.autoReconnect = ReconnectMode.onAllErrors
conn.loadAuthInfo ('./auth_info.json')
let gotClose0 = false
let gotClose1 = false
conn.on ('ws-close', ({ reason }) => {
gotClose0 = true
})
conn.on ('close', ({ reason }) => {
if (reason === DisconnectReason.badSession) gotClose1 = true
})
setTimeout (() => conn['conn'].emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je')), 1500)
await conn.connect ()
setTimeout (() => conn['conn'].emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je')), 1500)
await new Promise (resolve => {
conn.on ('open', resolve)
})
assert.ok (gotClose0, 'did not receive bad_session close initially')
assert.ok (gotClose1, 'did not receive bad_session close')
conn.close ()
})
/**
* the idea is to test closing the connection at multiple points in the connection
* and see if the library cleans up resources correctly
*/
it('should cleanup correctly', async () => {
const conn = makeConnection ()
conn.autoReconnect = ReconnectMode.onAllErrors
conn.loadAuthInfo ('./auth_info.json')
let timeout = 0.1
while (true) {
let tmout = setTimeout (() => conn.close(), timeout*1000)
try {
await conn.connect ()
clearTimeout (tmout)
break
} catch (error) {
}
// exponentially increase the timeout disconnect
timeout *= 2
}
await verifyConnectionOpen (conn)
})
/**
* the idea is to test closing the connection at multiple points in the connection
* and see if the library cleans up resources correctly
*/
it('should disrupt connect loop', async () => {
const conn = makeConnection ()
conn.autoReconnect = ReconnectMode.onAllErrors
conn.loadAuthInfo ('./auth_info.json')
let timeout = 1000
let tmout
const endConnection = async () => {
while (!conn['conn']) {
await delay(100)
}
conn['conn'].close ()
while (conn['conn']) {
await delay(100)
}
timeout *= 2
tmout = setTimeout (endConnection, timeout)
}
tmout = setTimeout (endConnection, timeout)
await conn.connect ()
clearTimeout (tmout)
await verifyConnectionOpen (conn)
})
it ('should reconnect on broken connection', async () => {
const conn = makeConnection ()
conn.autoReconnect = ReconnectMode.onConnectionLost
await conn.loadAuthInfo('./auth_info.json').connect ()
assert.strictEqual (conn.phoneConnected, true)
try {
const closeConn = () => conn['conn']?.terminate ()
const task = new Promise (resolve => {
let closes = 0
conn.on ('close', ({reason, isReconnecting}) => {
console.log (`closed: ${reason}`)
assert.ok (reason)
assert.ok (isReconnecting)
closes += 1
// let it fail reconnect a few times
if (closes >= 1) {
conn.removeAllListeners ('close')
conn.removeAllListeners ('connecting')
resolve(undefined)
}
})
conn.on ('connecting', () => {
// close again
delay (3500).then (closeConn)
})
})
closeConn ()
await task
await new Promise (resolve => {
conn.on ('open', () => {
conn.removeAllListeners ('open')
resolve(undefined)
})
})
conn.close ()
conn.on ('connecting', () => assert.fail('should not connect'))
await delay (2000)
} finally {
conn.removeAllListeners ('connecting')
conn.removeAllListeners ('close')
conn.removeAllListeners ('open')
conn.close ()
}
})
it ('should reconnect & stay connected', async () => {
const conn = makeConnection ()
conn.autoReconnect = ReconnectMode.onConnectionLost
await conn.loadAuthInfo('./auth_info.json').connect ()
assert.strictEqual (conn.phoneConnected, true)
await delay (30*1000)
conn['conn']?.terminate ()
conn.on ('close', () => {
assert.ok (!conn['lastSeen'])
console.log ('connection closed')
})
await new Promise (resolve => conn.on ('open', resolve))
await verifyConnectionOpen (conn)
})
})
describe ('Pending Requests', () => {
it ('should correctly send updates for chats', async () => {
const conn = makeConnection ()
conn.pendingRequestTimeoutMs = null
conn.loadAuthInfo('./auth_info.json')
const task = new Promise(resolve => conn.once('chats-received', resolve))
await conn.connect ()
await task
conn.close ()
const oldChat = conn.chats.all()[0]
oldChat.archive = 'true' // mark the first chat as archived
oldChat.modify_tag = '1234' // change modify tag to detect change
const promise = new Promise(resolve => conn.once('chats-update', resolve))
const result = await conn.connect ()
assert.ok (!result.newConnection)
const chats = await promise as Partial<WAChat>[]
const chat = chats.find (c => c.jid === oldChat.jid)
assert.ok (chat)
assert.ok ('archive' in chat)
assert.strictEqual (Object.keys(chat).length, 3)
assert.strictEqual (Object.keys(chats).length, 1)
conn.close ()
})
it ('should correctly send updates for contacts', async () => {
const conn = makeConnection ()
conn.pendingRequestTimeoutMs = null
conn.loadAuthInfo('./auth_info.json')
const task: any = new Promise(resolve => conn.once('contacts-received', resolve))
await conn.connect ()
const initialResult = await task
assert.strictEqual(
initialResult.updatedContacts.length,
Object.keys(conn.contacts).length
)
conn.close ()
const [jid] = Object.keys(conn.contacts)
const oldContact = conn.contacts[jid]
oldContact.name = 'Lol'
oldContact.index = 'L'
const promise = new Promise(resolve => conn.once('contacts-received', resolve))
const result = await conn.connect ()
assert.ok (!result.newConnection)
const {updatedContacts} = await promise as { updatedContacts: Partial<WAContact>[] }
const contact = updatedContacts.find (c => c.jid === jid)
assert.ok (contact)
assert.ok ('name' in contact)
assert.strictEqual (Object.keys(contact).length, 3)
assert.strictEqual (Object.keys(updatedContacts).length, 1)
conn.close ()
})
it('should queue requests when closed', async () => {
const conn = makeConnection ()
//conn.pendingRequestTimeoutMs = null
await conn.loadAuthInfo('./auth_info.json').connect ()
await delay (2000)
conn.close ()
const task: Promise<any> = conn.query({json: ['query', 'Status', conn.user.jid]})
await delay (2000)
conn.connect ()
const json = await task
assert.ok (json.status)
conn.close ()
})
it('[MANUAL] should receive query response after phone disconnect', async () => {
const conn = makeConnection ()
await conn.loadAuthInfo('./auth_info.json').connect ()
console.log(`disconnect your phone from the internet!`)
await delay(5000)
const task = conn.loadMessages(testJid, 50)
setTimeout(() => console.log('reconnect your phone!'), 20_000)
const result = await task
assert.ok(result.messages[0])
assert.ok(!conn['phoneCheckInterval']) // should be undefined
conn.close ()
})
it('should re-execute query on connection closed error', async () => {
const conn = makeConnection ()
//conn.pendingRequestTimeoutMs = 10_000
await conn.loadAuthInfo('./auth_info.json').connect ()
const task: Promise<any> = conn.query({json: ['query', 'Status', conn.user.jid], waitForOpen: true})
await delay(20)
conn['onMessageRecieved']('1234,["Pong",false]') // fake cancel the connection
await delay(2000)
const json = await task
assert.ok (json.status)
conn.close ()
})
})

View File

@@ -1,193 +0,0 @@
import { MessageType, GroupSettingChange, delay, ChatModification, whatsappID } from '../WAConnection'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetrieveMessage } from './Common'
WAConnectionTest('Groups', (conn) => {
let gid: string
it('should create a group', async () => {
const response = await conn.groupCreate('Cool Test Group', [testJid])
assert.ok (conn.chats.get(response.gid))
const {chats} = await conn.loadChats(10, null)
assert.strictEqual (chats[0].jid, response.gid) // first chat should be new group
gid = response.gid
console.log('created group: ' + JSON.stringify(response))
})
it('should retrieve group invite code', async () => {
const code = await conn.groupInviteCode(gid)
assert.ok(code)
assert.strictEqual(typeof code, 'string')
})
it('should joined group via invite code', async () => {
const response = await conn.acceptInvite(gid)
assert.ok(response.status)
assert.strictEqual(response.status, response.gid)
})
it('should retrieve group metadata', async () => {
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.id, gid)
assert.strictEqual(metadata.participants.filter((obj) => obj.jid.split('@')[0] === testJid.split('@')[0]).length, 1)
assert.ok(conn.chats.get(gid))
assert.ok(conn.chats.get(gid).metadata)
})
it('should update the group description', async () => {
const newDesc = 'Wow this was set from Baileys'
const waitForEvent = new Promise (resolve => (
conn.once ('group-update', ({jid, desc}) => {
if (jid === gid && desc) {
assert.strictEqual(desc, newDesc)
assert.strictEqual(
conn.chats.get(jid).metadata.desc,
newDesc
)
resolve(undefined)
}
})
))
await conn.groupUpdateDescription (gid, newDesc)
await waitForEvent
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.desc, newDesc)
})
it('should send a message on the group', async () => {
await sendAndRetrieveMessage(conn, 'Hello!', MessageType.text, {}, gid)
})
it('should delete a message on the group', async () => {
const message = await sendAndRetrieveMessage(conn, 'Hello!', MessageType.text, {}, gid)
await delay(1500)
await conn.deleteMessage(message.key)
})
it('should quote a message on the group', async () => {
const {messages} = await conn.loadMessages (gid, 100)
const quotableMessage = messages.find (m => m.message)
assert.ok (quotableMessage, 'need at least one message')
const response = await conn.sendMessage(gid, 'hello', MessageType.extendedText, {quoted: quotableMessage})
const loaded = await conn.loadMessages(gid, 10)
const message = loaded.messages.find (m => m.key.id === response.key.id)?.message?.extendedTextMessage
assert.ok(message)
assert.strictEqual (message.contextInfo.stanzaId, quotableMessage.key.id)
})
it('should update the subject', async () => {
const subject = 'Baileyz ' + Math.floor(Math.random()*5)
const waitForEvent = new Promise (resolve => {
conn.once ('chat-update', ({jid, name}) => {
if (jid === gid) {
assert.strictEqual(name, subject)
assert.strictEqual(conn.chats.get(jid).name, subject)
resolve(undefined)
}
})
})
await conn.groupUpdateSubject(gid, subject)
await waitForEvent
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject)
})
it('should update the group settings', async () => {
const waitForEvent = new Promise (resolve => {
conn.once ('group-update', ({jid, announce}) => {
if (jid === gid) {
assert.strictEqual (announce, 'true')
assert.strictEqual(conn.chats.get(gid).metadata.announce, announce)
resolve(undefined)
}
})
})
await conn.groupSettingChange (gid, GroupSettingChange.messageSend, true)
await waitForEvent
conn.removeAllListeners ('group-update')
await delay (2000)
await conn.groupSettingChange (gid, GroupSettingChange.settingsChange, true)
})
it('should promote someone', async () => {
const waitForEvent = new Promise (resolve => {
conn.once ('group-participants-update', ({ jid, action, participants }) => {
if (jid === gid) {
assert.strictEqual (action, 'promote')
console.log(participants)
console.log(conn.chats.get(jid).metadata)
assert.ok(
conn.chats.get(jid).metadata.participants.find(({ jid, isAdmin }) => (
whatsappID(jid) === whatsappID(participants[0]) && isAdmin
)),
)
resolve(undefined)
}
})
})
await conn.groupMakeAdmin(gid, [ testJid ])
await waitForEvent
})
it('should remove someone from a group', async () => {
const metadata = await conn.groupMetadata (gid)
if (metadata.participants.find(({jid}) => whatsappID(jid) === testJid)) {
const waitForEvent = new Promise (resolve => {
conn.once ('group-participants-update', ({jid, participants, action}) => {
if (jid === gid) {
assert.strictEqual (participants[0], testJid)
assert.strictEqual (action, 'remove')
assert.deepStrictEqual(
conn.chats.get(jid).metadata.participants.find(p => whatsappID(p.jid) === whatsappID(participants[0])),
undefined
)
resolve(undefined)
}
})
})
await conn.groupRemove(gid, [testJid])
await waitForEvent
} else console.log(`could not find testJid`)
})
it('should leave the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.once ('chat-update', ({jid, read_only}) => {
if (jid === gid) {
assert.strictEqual (read_only, 'true')
resolve(undefined)
}
})
})
await conn.groupLeave(gid)
await waitForEvent
await conn.groupMetadataMinimal (gid)
})
it('should archive the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.once ('chat-update', ({jid, archive}) => {
if (jid === gid) {
assert.strictEqual (archive, 'true')
resolve(undefined)
}
})
})
await conn.modifyChat(gid, ChatModification.archive)
await waitForEvent
})
it('should delete the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.once ('chat-update', (chat) => {
if (chat.jid === gid) {
assert.strictEqual (chat['delete'], 'true')
resolve(undefined)
}
})
})
await conn.modifyChat(gid, 'delete')
await waitForEvent
})
})

View File

@@ -1,43 +0,0 @@
import { deepStrictEqual, strictEqual } from 'assert'
import { createWriteStream } from 'fs'
import { readFile } from 'fs/promises'
import { proto } from '../../WAMessage'
import { MessageType } from '../WAConnection'
import { aesEncrypWithIV, decryptMediaMessageBuffer, encryptedStream, getMediaKeys, getStream, hmacSign, sha256 } from '../WAConnection/Utils'
import { WAConnectionTest } from './Common'
describe('Media Download Tests', () => {
it('should encrypt media streams correctly', async function() {
const url = './Media/meme.jpeg'
const streamValues = await encryptedStream({ url }, MessageType.image)
const buffer = await readFile(url)
const mediaKeys = getMediaKeys(streamValues.mediaKey, MessageType.image)
const enc = aesEncrypWithIV(buffer, mediaKeys.cipherKey, mediaKeys.iv)
const mac = hmacSign(Buffer.concat([mediaKeys.iv, enc]), mediaKeys.macKey).slice(0, 10)
const body = Buffer.concat([enc, mac]) // body is enc + mac
const fileSha256 = sha256(buffer)
const fileEncSha256 = sha256(body)
deepStrictEqual(streamValues.fileSha256, fileSha256)
strictEqual(streamValues.fileLength, buffer.length)
deepStrictEqual(streamValues.mac, mac)
deepStrictEqual(await readFile(streamValues.encBodyPath), body)
deepStrictEqual(streamValues.fileEncSha256, fileEncSha256)
})
})
/*
WAConnectionTest('Media Upload', conn => {
it('should upload the same file', async () => {
const FILES = [
{ url: './Media/meme.jpeg', type: MessageType.image },
{ url: './Media/ma_gif.mp4', type: MessageType.video },
{ url: './Media/sonata.mp3', type: MessageType.audio },
]
})
})*/

View File

@@ -1,268 +0,0 @@
import { MessageType, Mimetype, delay, promiseTimeout, WA_MESSAGE_STATUS_TYPE, generateMessageID, WAMessage } from '../WAConnection'
import { promises as fs } from 'fs'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetrieveMessage } from './Common'
WAConnectionTest('Messages', conn => {
it('should send a text message', async () => {
const message = await sendAndRetrieveMessage(conn, 'hello fren', MessageType.text)
assert.strictEqual(message.message.conversation || message.message.extendedTextMessage?.text, 'hello fren')
})
it('should send a pending message', async () => {
const message = await sendAndRetrieveMessage(conn, 'hello fren', MessageType.text, { waitForAck: false })
await new Promise(resolve => conn.on('chat-update', update => {
if (update.jid === testJid &&
update.messages &&
update.messages.first.key.id === message.key.id &&
update.messages.first.status === WA_MESSAGE_STATUS_TYPE.SERVER_ACK) {
resolve(undefined)
}
}))
})
it('should forward a message', async () => {
let {messages} = await conn.loadMessages (testJid, 1)
await conn.forwardMessage (testJid, messages[0], true)
messages = (await conn.loadMessages (testJid, 1)).messages
const message = messages.slice (-1)[0]
const content = message.message[ Object.keys(message.message)[0] ]
assert.strictEqual (content?.contextInfo?.isForwarded, true)
})
it('should send a link preview', async () => {
const text = 'hello this is from https://www.github.com/adiwajshing/Baileys'
const message = await sendAndRetrieveMessage(conn, text, MessageType.text, { detectLinks: true })
const received = message.message.extendedTextMessage
assert.strictEqual(received.text, text)
assert.ok (received.canonicalUrl)
assert.ok (received.title)
assert.ok (received.description)
})
it('should quote a message', async () => {
const quoted = (await conn.loadMessages(testJid, 2)).messages[0]
const message = await sendAndRetrieveMessage(conn, 'hello fren 2', MessageType.extendedText, { quoted })
assert.strictEqual(
message.message.extendedTextMessage.contextInfo.stanzaId,
quoted.key.id
)
assert.strictEqual(
message.message.extendedTextMessage.contextInfo.participant,
quoted.key.fromMe ? conn.user.jid : quoted.key.id
)
})
it('should upload media successfully', async () => {
const content = await fs.readFile('./Media/sonata.mp3')
// run 10 uploads
for (let i = 0; i < 10;i++) {
await conn.prepareMessageContent (content, MessageType.audio, { filename: 'audio.mp3', mimetype: Mimetype.mp4Audio })
}
})
it('should send a gif', async () => {
const message = await sendAndRetrieveMessage(conn, { url: './Media/ma_gif.mp4' }, MessageType.video, { mimetype: Mimetype.gif })
await conn.downloadAndSaveMediaMessage(message,'./Media/received_vid')
})
it('should send an audio', async () => {
const content = await fs.readFile('./Media/sonata.mp3')
const message = await sendAndRetrieveMessage(conn, content, MessageType.audio, { mimetype: Mimetype.mp4Audio })
// check duration was okay
assert.ok (message.message.audioMessage.seconds > 0)
await conn.downloadAndSaveMediaMessage(message,'./Media/received_aud')
})
it('should send an audio as a voice note', async () => {
const content = await fs.readFile('./Media/sonata.mp3')
const message = await sendAndRetrieveMessage(conn, content, MessageType.audio, { mimetype: Mimetype.mp4Audio, ptt: true })
assert.ok (message.message.audioMessage.seconds > 0)
assert.strictEqual (message.message?.audioMessage?.ptt, true)
await conn.downloadAndSaveMediaMessage(message,'./Media/received_aud')
})
it('should send a jpeg image', async () => {
const message = await sendAndRetrieveMessage(conn, { url: './Media/meme.jpeg' }, MessageType.image)
assert.ok(message.message.imageMessage.jpegThumbnail.length > 0)
const msg = await conn.downloadMediaMessage(message)
assert.deepStrictEqual(msg, await fs.readFile('./Media/meme.jpeg'))
})
it('should send a remote jpeg image', async () => {
const message = await sendAndRetrieveMessage(
conn,
{ url: 'https://www.memestemplates.com/wp-content/uploads/2020/05/tom-with-phone.jpg' },
MessageType.image
)
assert.ok (message.message?.imageMessage?.jpegThumbnail)
await conn.downloadMediaMessage(message)
})
it('should send a png image', async () => {
const content = await fs.readFile('./Media/icon.png')
const message = await sendAndRetrieveMessage(conn, content, MessageType.image, { mimetype: 'image/png' })
assert.ok (message.message?.imageMessage?.jpegThumbnail)
await conn.downloadMediaMessage(message)
})
it('should send a sticker', async () => {
const content = await fs.readFile('./Media/octopus.webp')
const message = await sendAndRetrieveMessage(conn, content, MessageType.sticker)
await conn.downloadMediaMessage(message)
})
/*it('should send an interactive message', async () => {
console.log (
JSON.stringify(await conn.loadMessages (testJid, 5), null, '\t')
)
const message = conn.prepareMessageFromContent (
testJid,
{
templateMessage: {
fourRowTemplate: {
content: {
namespace: 'my-namespace',
localizableParams: [
],
params: ['hello!']
},
buttons: [
{
index: 0,
quickReplyButton: {
displayText: {
params: ['my name jeff']
}
}
},
{
index: 1,
quickReplyButton: {
displayText: {
params: ['my name NOT jeff'],
}
}
}
]
}
}
},
{}
)
await conn.relayWAMessage (message)
})*/
it('should send an image & quote', async () => {
const quoted = (await conn.loadMessages(testJid, 2)).messages[0]
const content = await fs.readFile('./Media/meme.jpeg')
const message = await sendAndRetrieveMessage(conn, content, MessageType.image, { quoted })
await conn.downloadMediaMessage(message) // check for successful decoding
assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, quoted.key.id)
})
it('should send a message & delete it', async () => {
const message = await sendAndRetrieveMessage(conn, 'hello fren', MessageType.text)
await delay (2000)
await conn.deleteMessage (testJid, message.key)
})
it('should clear the most recent message', async () => {
const {messages} = await conn.loadMessages (testJid, 1)
await delay (2000)
await conn.clearMessage (messages[0].key)
})
it('should send media after close', async () => {
const content = await fs.readFile('./Media/octopus.webp')
await sendAndRetrieveMessage(conn, content, MessageType.sticker)
conn.close ()
await conn.connect ()
const content2 = await fs.readFile('./Media/cat.jpeg')
await sendAndRetrieveMessage(conn, content2, MessageType.image)
})
it('should fail to send a text message', async () => {
const JID = '1234-1234@g.us'
const messageId = generateMessageID()
conn.sendMessage(JID, 'hello', MessageType.text, { messageId })
await new Promise(resolve => (
conn.on ('chat-update', async update => {
console.log(messageId, update.messages?.first)
if (
update.jid === JID &&
update.messages?.first.key.id === messageId &&
update.messages.first.status === WA_MESSAGE_STATUS_TYPE.ERROR) {
resolve(undefined)
}
})
))
conn.removeAllListeners('chat-update')
})
it('should maintain message integrity', async () => {
// loading twice does not alter the results
const results = await Promise.all ([
conn.loadMessages (testJid, 50),
conn.loadMessages (testJid, 50)
])
assert.strictEqual (results[0].messages.length, results[1].messages.length)
for (let i = 0; i < results[1].messages.length;i++) {
assert.deepStrictEqual (
results[0].messages[i].key,
results[1].messages[i].key,
`failed equal at ${i}`
)
}
assert.ok (results[0].messages.length <= 50)
// check if messages match server
let msgs = await conn.fetchMessagesFromWA (testJid, 50)
for (let i = 0; i < results[1].messages.length;i++) {
assert.deepStrictEqual (
results[0].messages[i].key,
msgs[i].key,
`failed equal at ${i}`
)
}
// check with some arbitary cursors
let cursor = results[0].messages.slice(-1)[0].key
msgs = await conn.fetchMessagesFromWA (testJid, 20, cursor)
let {messages} = await conn.loadMessages (testJid, 20, cursor)
for (let i = 0; i < messages.length;i++) {
assert.deepStrictEqual (
messages[i].key,
msgs[i].key,
`failed equal at ${i}`
)
}
for (let i = 0; i < 3;i++) {
cursor = results[0].messages[i].key
msgs = await conn.fetchMessagesFromWA (testJid, 20, cursor)
messages = (await conn.loadMessages (testJid, 20, cursor)).messages
for (let i = 0; i < messages.length;i++) {
assert.deepStrictEqual (messages[i].key, msgs[i].key, `failed equal at ${i}`)
}
cursor = msgs[0].key
msgs = await conn.fetchMessagesFromWA (testJid, 20, cursor)
messages = (await conn.loadMessages (testJid, 20, cursor)).messages
for (let i = 0; i < messages.length;i++) {
assert.deepStrictEqual (messages[i].key, msgs[i].key, `failed equal at ${i}`)
}
}
})
it('should deliver a message', async () => {
const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text)
const waitForUpdate =
promiseTimeout(15000, resolve => {
conn.on('chat-update', update => {
if (update.messages?.first.key.id === response.key.id) {
resolve(update.messages.first)
}
})
}) as Promise<WAMessage>
const m = await waitForUpdate
assert.ok (m.status >= WA_MESSAGE_STATUS_TYPE.DELIVERY_ACK)
})
})

View File

@@ -1,430 +0,0 @@
import { Presence, ChatModification, delay, newMessagesDB, WA_DEFAULT_EPHEMERAL, MessageType, WAMessage } from '../WAConnection'
import { promises as fs } from 'fs'
import * as assert from 'assert'
import got from 'got'
import { WAConnectionTest, testJid, sendAndRetrieveMessage } from './Common'
WAConnectionTest('Misc', conn => {
it('should tell if someone has an account on WhatsApp', async () => {
const response = await conn.isOnWhatsApp(testJid)
assert.strictEqual(response, true)
const responseFail = await conn.isOnWhatsApp('abcd@s.whatsapp.net')
assert.strictEqual(responseFail, false)
})
it('should return the status', async () => {
const response = await conn.getStatus(testJid)
assert.strictEqual(typeof response.status, 'string')
})
it('should update status', async () => {
const newStatus = 'v cool status'
const waitForEvent = new Promise (resolve => {
conn.on ('contact-update', ({jid, status}) => {
if (jid === conn.user.jid) {
assert.strictEqual (status, newStatus)
conn.removeAllListeners ('contact-update')
resolve(undefined)
}
})
})
const response = await conn.getStatus()
assert.strictEqual(typeof response.status, 'string')
await delay (1000)
await conn.setStatus (newStatus)
const response2 = await conn.getStatus()
assert.strictEqual (response2.status, newStatus)
await waitForEvent
await delay (1000)
await conn.setStatus (response.status) // update back
})
it('should update profile name', async () => {
const newName = 'v cool name'
await delay (1000)
const originalName = conn.user.name!
const waitForEvent = new Promise<void> (resolve => {
conn.on ('contact-update', ({name}) => {
assert.strictEqual (name, newName)
conn.removeAllListeners ('contact-update')
resolve ()
})
})
await conn.updateProfileName (newName)
await waitForEvent
await delay (1000)
assert.strictEqual (conn.user.name, newName)
await delay (1000)
await conn.updateProfileName (originalName) // update back
})
it('should return the stories', async () => {
await conn.getStories()
})
it('should return the profile picture correctly', async () => {
// wait for chats
await new Promise(resolve => (
conn.once('initial-data-received', resolve)
))
const pictures = await Promise.all(
conn.chats.all().slice(0, 15).map(({ jid }) => (
conn.getProfilePicture(jid)
.catch(err => '')
))
)
// pictures should return correctly
const truePictures = pictures.filter(pp => !!pp)
assert.strictEqual(
new Set(truePictures).size,
truePictures.length
)
})
it('should change the profile picture', async () => {
await delay (5000)
const ppUrl = await conn.getProfilePicture(conn.user.jid)
const {rawBody: oldPP} = await got(ppUrl)
const newPP = await fs.readFile('./Media/cat.jpeg')
await conn.updateProfilePicture(conn.user.jid, newPP)
await delay (10000)
await conn.updateProfilePicture (conn.user.jid, oldPP) // revert back
})
it('should send typing indicator', async () => {
const response = await conn.updatePresence(testJid, Presence.composing)
assert.ok(response)
})
it('should change a chat read status', async () => {
const jids = conn.chats.all ().map (c => c.jid)
for (let jid of jids.slice(0, 5)) {
console.log (`changing read status for ${jid}`)
const waitForEvent = new Promise (resolve => {
conn.once ('chat-update', ({jid: tJid, count}) => {
if (jid === tJid) {
assert.ok (count < 0)
resolve(undefined)
}
})
})
await conn.chatRead (jid, 'unread')
await waitForEvent
await delay (5000)
await conn.chatRead (jid, 'read')
}
})
it('should archive & unarchive', async () => {
// wait for chats
await new Promise(resolve => (
conn.once('chats-received', ({ }) => resolve(undefined))
))
const idx = conn.chats.all().findIndex(chat => chat.jid === testJid)
await conn.modifyChat (testJid, ChatModification.archive)
const idx2 = conn.chats.all().findIndex(chat => chat.jid === testJid)
assert.ok(idx < idx2) // should move further down the array
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unarchive)
const idx3 = conn.chats.all().findIndex(chat => chat.jid === testJid)
assert.strictEqual(idx, idx3) // should be back there
})
it('should archive & unarchive on new message', async () => {
// wait for chats
await new Promise(resolve => (
conn.once('chats-received', ({ }) => resolve(undefined))
))
const idx = conn.chats.all().findIndex(chat => chat.jid === testJid)
await conn.modifyChat (testJid, ChatModification.archive)
const idx2 = conn.chats.all().findIndex(chat => chat.jid === testJid)
assert.ok(idx < idx2) // should move further down the array
await delay (2000)
await sendAndRetrieveMessage(conn, 'test', MessageType.text)
// should be unarchived
const idx3 = conn.chats.all().findIndex(chat => chat.jid === testJid)
assert.strictEqual(idx, idx3) // should be back there
})
it('should pin & unpin a chat', async () => {
await conn.modifyChat (testJid, ChatModification.pin)
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unpin)
})
it('should mute & unmute a chat', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', ({jid, mute}) => {
if (jid === testJid ) {
assert.ok (mute)
conn.removeAllListeners ('chat-update')
resolve(undefined)
}
})
})
await conn.modifyChat (testJid, ChatModification.mute, 8*60*60*1000) // 8 hours in the future
await waitForEvent
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unmute)
})
it('should star/unstar messages', async () => {
for (let i = 1; i <= 5; i++) {
await conn.sendMessage(testJid, `Message ${i}`, MessageType.text)
await delay(1000)
}
let response = await conn.loadMessages(testJid, 5)
let starred = response.messages.filter(m => m.starred)
assert.strictEqual(starred.length, 0)
conn.starMessage(response.messages[2].key)
await delay(2000)
conn.starMessage(response.messages[4].key)
await delay(2000)
response = await conn.loadMessages(testJid, 5)
starred = response.messages.filter(m => m.starred)
assert.strictEqual(starred.length, 2)
await delay(2000)
conn.starMessage(response.messages[2].key, 'unstar')
await delay(2000)
response = await conn.loadMessages(testJid, 5)
starred = response.messages.filter(m => m.starred)
assert.strictEqual(starred.length, 1)
})
it('should clear a chat', async () => {
// Uses chat with yourself to avoid losing chats
const selfJid = conn.user.jid
for (let i = 1; i <= 5; i++) {
await conn.sendMessage(selfJid, `Message ${i}`, MessageType.text)
await delay(1000)
}
let response = await conn.loadMessages(selfJid, 50)
const initialCount = response.messages.length
assert.ok(response.messages.length >= 0)
conn.starMessage(response.messages[2].key)
await delay(2000)
conn.starMessage(response.messages[4].key)
await delay(2000)
await conn.modifyChat(selfJid, ChatModification.clear)
await delay(2000)
response = await conn.loadMessages(selfJid, 50)
await delay(2000)
assert.ok(response.messages.length < initialCount)
assert.ok(response.messages.length > 1)
await conn.modifyChat(selfJid, ChatModification.clear, true)
await delay(2000)
response = await conn.loadMessages(selfJid, 50)
assert.strictEqual(response.messages.length, 1)
})
it('should return search results', async () => {
const jids = [null, testJid]
for (let i in jids) {
let response = await conn.searchMessages('Hello', jids[i], 25, 1)
assert.ok (response.messages)
assert.ok (response.messages.length >= 0)
response = await conn.searchMessages('剛剛試咗😋一個字', jids[i], 25, 1)
assert.ok (response.messages)
assert.ok (response.messages.length >= 0)
}
})
it('should load a single message', async () => {
const {messages} = await conn.loadMessages (testJid, 25)
for (var message of messages) {
const loaded = await conn.loadMessage (testJid, message.key.id)
assert.strictEqual (loaded.key.id, message.key.id, `loaded message ${JSON.stringify(message)} incorrectly`)
await delay (500)
}
})
// open the other phone and look at the updates to really verify stuff
it('should send presence updates', async () => {
conn.shouldLogMessages = true
conn.requestPresenceUpdate(testJid)
const sequence = [ Presence.available, Presence.composing, Presence.paused, Presence.recording, Presence.paused, Presence.unavailable ]
for (const presence of sequence) {
await delay(5000)
await conn.updatePresence(presence !== Presence.unavailable ? testJid : null, presence)
//console.log(conn.messageLog.slice(-1))
console.log('sent update ', presence)
}
})
it('should generate link previews correctly', async () => {
await conn.generateLinkPreview ('hello this is from https://www.github.com/adiwajshing/Baileys')
// two links should fail
await assert.rejects (
conn.generateLinkPreview ('I sent links to https://teachyourselfcs.com/ and https://www.fast.ai/')
)
})
// this test requires quite a few messages with the test JID
it('should detect overlaps and clear messages accordingly', async () => {
// wait for chats
await new Promise(resolve => (
conn.once('initial-data-received', resolve)
))
conn.maxCachedMessages = 100
const chat = conn.chats.get(testJid)
const oldCount = chat.messages.length
console.log(`test chat has ${oldCount} pre-loaded messages`)
// load 100 messages
await conn.loadMessages(testJid, 100, undefined)
assert.strictEqual(chat.messages.length, 100)
conn.close()
// remove all latest messages
chat.messages = newMessagesDB( chat.messages.all().slice(0, 20) )
const task = new Promise(resolve => (
conn.on('initial-data-received', ({ chatsWithMissingMessages }) => {
assert.strictEqual(Object.keys(chatsWithMissingMessages).length, 1)
const missing = chatsWithMissingMessages.find(({ jid }) => jid === testJid)
assert.ok(missing, 'missing message not detected')
assert.strictEqual(
conn.chats.get(testJid).messages.length,
missing.count
)
assert.strictEqual(missing.count, oldCount)
resolve(undefined)
})
))
await conn.connect()
await task
})
it('should toggle disappearing messages', async () => {
let chat = conn.chats.get(testJid)
if (!chat) {
// wait for chats
await new Promise(resolve => (
conn.once('chats-received', resolve)
))
chat = conn.chats.get(testJid)
}
const waitForChatUpdate = (ephemeralOn: boolean) => (
new Promise(resolve => (
conn.on('chat-update', ({ jid, ephemeral }) => {
if (jid === testJid && typeof ephemeral !== 'undefined') {
assert.strictEqual(!!(+ephemeral), ephemeralOn)
assert.strictEqual(!!(+chat.ephemeral), ephemeralOn)
resolve(undefined)
conn.removeAllListeners('chat-update')
}
})
))
)
const toggleDisappearingMessages = async (on: boolean) => {
const update = waitForChatUpdate(on)
await conn.toggleDisappearingMessages(testJid, on ? WA_DEFAULT_EPHEMERAL : 0)
await update
}
if (!chat.eph_setting_ts) {
await toggleDisappearingMessages(true)
}
await delay(1000)
let msg = await sendAndRetrieveMessage(
conn,
'This will go poof 😱',
MessageType.text
)
assert.ok(msg.message?.ephemeralMessage)
const contextInfo = msg.message?.ephemeralMessage?.message?.extendedTextMessage?.contextInfo
assert.strictEqual(contextInfo.expiration, chat.ephemeral)
assert.strictEqual(+contextInfo.ephemeralSettingTimestamp, +chat.eph_setting_ts)
// test message deletion
await conn.deleteMessage(testJid, msg.key)
await delay(1000)
await toggleDisappearingMessages(false)
await delay(1000)
msg = await sendAndRetrieveMessage(
conn,
'This will not go poof 😔',
MessageType.text
)
assert.ok(msg.message.extendedTextMessage)
})
it('should block & unblock a user', async () => {
const blockedCount = conn.blocklist.length;
const waitForEventAdded = new Promise<void> (resolve => {
conn.once ('blocklist-update', ({added}) => {
assert.ok (added.length)
resolve ()
})
})
await conn.blockUser (testJid, 'add')
assert.strictEqual(conn.blocklist.length, blockedCount + 1);
await waitForEventAdded
await delay (2000)
const waitForEventRemoved = new Promise<void> (resolve => {
conn.once ('blocklist-update', ({removed}) => {
assert.ok (removed.length)
resolve ()
})
})
await conn.blockUser (testJid, 'remove')
assert.strictEqual(conn.blocklist.length, blockedCount);
await waitForEventRemoved
})
it('should exit an invalid query', async () => {
// try and send an already sent message
let msg: WAMessage
await conn.findMessage(testJid, 5, m => {
if(m.key.fromMe) {
msg = m
return true
}
})
try {
await conn.relayWAMessage(msg)
assert.fail('should not have sent')
} catch(error) {
assert.strictEqual(error.status, 422)
}
})
})

View File

@@ -1,111 +0,0 @@
import { strict as assert } from 'assert'
import { Mutex } from '../WAConnection/Mutex'
const DEFAULT_WAIT = 1000
class MyClass {
didDoWork = false
values: { [k: string]: number } = {}
counter = 0
@Mutex ()
async myFunction () {
if (this.didDoWork) return
await new Promise (resolve => setTimeout(resolve, DEFAULT_WAIT))
if (this.didDoWork) {
throw new Error ('work already done')
}
this.didDoWork = true
}
@Mutex (key => key)
async myKeyedFunction (key: string) {
if (!this.values[key]) {
await new Promise (resolve => setTimeout(resolve, DEFAULT_WAIT))
if (this.values[key]) throw new Error ('value already set for ' + key)
this.values[key] = Math.floor(Math.random ()*100)
}
return this.values[key]
}
@Mutex (key => key)
async myQueingFunction (key: string) {
await new Promise (resolve => setTimeout(resolve, DEFAULT_WAIT))
}
@Mutex ()
async myErrorFunction () {
await new Promise (resolve => setTimeout(resolve, 100))
this.counter += 1
if (this.counter % 2 === 0) {
throw new Error ('failed')
}
}
}
describe ('garbage', () => {
it ('should only execute once', async () => {
const stuff = new MyClass ()
const start = new Date ()
await Promise.all ([...Array(1000)].map(() => stuff.myFunction ()))
const diff = new Date ().getTime()-start.getTime()
assert.ok (diff < DEFAULT_WAIT*1.25)
})
it ('should only execute once based on the key', async () => {
const stuff = new MyClass ()
const start = new Date ()
/*
In this test, the mutex will lock the function based on the key.
So, if the function with argument `key1` is underway
and another function with key `key1` is called,
the call is blocked till the first function completes.
However, if argument `key2` is passed, the function is allowed to pass.
*/
const keys = ['key1', 'key2', 'key3']
const duplicates = 1000
const results = await Promise.all (
keys.flatMap (key => (
[...Array(duplicates)].map(() => stuff.myKeyedFunction (key))
))
)
assert.deepStrictEqual (
results.slice(0, duplicates).filter (r => r !== results[0]),
[]
)
const diff = new Date ().getTime()-start.getTime()
assert.ok (diff < DEFAULT_WAIT*1.25)
})
it ('should execute operations in a queue', async () => {
const stuff = new MyClass ()
const start = new Date ()
const keys = ['key1', 'key2', 'key3']
await Promise.all (
keys.flatMap (key => (
[...Array(2)].map(() => stuff.myQueingFunction (key))
))
)
const diff = new Date ().getTime()-start.getTime()
assert.ok (diff < DEFAULT_WAIT*2.2 && diff > DEFAULT_WAIT*1.5)
})
it ('should throw an error on selected items', async () => {
const stuff = new MyClass ()
const start = new Date ()
const WAIT = 100
const FUNCS = 40
const results = await Promise.all (
[...Array(FUNCS)].map(() => stuff.myErrorFunction ().catch(err => err.message))
)
const diff = new Date ().getTime()-start.getTime()
assert.ok (diff < WAIT*FUNCS*1.1)
assert.strictEqual (
results.filter (r => r === 'failed').length,
FUNCS/2 // half should fail
)
})
})

View File

@@ -1,112 +0,0 @@
describe ('Reconnects', () => {
const verifyConnectionOpen = async (conn: Connection) => {
expect((await conn.getState()).user).toBeDefined()
let failed = false
// check that the connection stays open
conn.ev.on('state.update', ({ connection, lastDisconnect }) => {
if(connection === 'close' && !!lastDisconnect.error) {
failed = true
}
})
await delay (60*1000)
conn.close ()
expect(failed).toBe(false)
}
it('should dispose correctly on bad_session', async () => {
const conn = makeConnection({
reconnectMode: 'on-any-error',
credentials: './auth_info.json',
maxRetries: 2,
connectCooldownMs: 500
})
let gotClose0 = false
let gotClose1 = false
const openPromise = conn.open()
conn.getSocket().ev.once('ws-close', () => {
gotClose0 = true
})
conn.ev.on('state.update', ({ lastDisconnect }) => {
//@ts-ignore
if(lastDisconnect?.error?.output?.statusCode === DisconnectReason.badSession) {
gotClose1 = true
}
})
setTimeout (() => conn.getSocket().ws.emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je')), 1500)
await openPromise
console.log('opened connection')
await delay(1000)
conn.getSocket().ws.emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je'))
await delay(2000)
await conn.waitForConnection()
conn.close()
expect(gotClose0).toBe(true)
expect(gotClose1).toBe(true)
}, 20_000)
/**
* the idea is to test closing the connection at multiple points in the connection
* and see if the library cleans up resources correctly
*/
it('should cleanup correctly', async () => {
const conn = makeConnection({
reconnectMode: 'on-any-error',
credentials: './auth_info.json'
})
let timeoutMs = 100
while (true) {
let tmout = setTimeout (() => {
conn.close()
}, timeoutMs)
try {
await conn.open()
clearTimeout (tmout)
break
} catch (error) {
}
// exponentially increase the timeout disconnect
timeoutMs *= 2
}
await verifyConnectionOpen(conn)
}, 120_000)
/**
* the idea is to test closing the connection at multiple points in the connection
* and see if the library cleans up resources correctly
*/
it('should disrupt connect loop', async () => {
const conn = makeConnection({
reconnectMode: 'on-any-error',
credentials: './auth_info.json'
})
let timeout = 1000
let tmout
const endConnection = async () => {
while (!conn.getSocket()) {
await delay(100)
}
conn.getSocket().end(Boom.preconditionRequired('conn close'))
while (conn.getSocket()) {
await delay(100)
}
timeout *= 2
tmout = setTimeout (endConnection, timeout)
}
tmout = setTimeout (endConnection, timeout)
await conn.open()
clearTimeout (tmout)
await verifyConnectionOpen(conn)
}, 120_000)
})

View File

@@ -1,95 +0,0 @@
import BinaryNode from '../BinaryNode'
describe('Binary Coding Tests', () => {
const TEST_VECTORS: [string, BinaryNode][] = [
[
'f806092f5a0a10f804f80234fc6c0a350a1b39313735323938373131313740732e77686174736170702e6e657410011a143345423030393637354537454433374141424632122b0a292a7069616e6f20726f6f6d2074696d696e6773206172653a2a0a20363a3030414d2d31323a3030414d18b3faa7f3052003f80234fc4c0a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20304643454335333330463634393239433645394132434646443242433845414418bdfaa7f305c00101f80234fc930a350a1b39313735323938373131313740732e77686174736170702e6e657410011a14334542303033433742353339414644303937353312520a50536f727279206672656e2c204920636f756c646e277420756e6465727374616e6420274c69627261272e2054797065202768656c702720746f206b6e6f77207768617420616c6c20492063616e20646f18c1faa7f3052003f80234fc540a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20413132333042384436423041314437393345433241453245413043313638443812090a076c69627261727918c2faa7f305',
new BinaryNode(
'action',
{ last: 'true', add: 'before' },
[
new BinaryNode(
'message',
{},
{
key: { remoteJid: '917529871117@s.whatsapp.net', fromMe: true, id: '3EB009675E7ED37AABF2' },
message: { conversation: '*piano room timings are:*\n 6:00AM-12:00AM' },
messageTimestamp: '1584004403',
status: 'DELIVERY_ACK',
} as any
),
new BinaryNode(
'message',
{},
{
key: {
remoteJid: '917529871117@s.whatsapp.net',
fromMe: false,
id: '0FCEC5330F64929C6E9A2CFFD2BC8EAD',
},
messageTimestamp: '1584004413',
messageStubType: 'REVOKE',
} as any
),
new BinaryNode(
'message',
{},
{
key: { remoteJid: '917529871117@s.whatsapp.net', fromMe: true, id: '3EB003C7B539AFD09753' },
message: {
conversation:
"Sorry fren, I couldn't understand 'Libra'. Type 'help' to know what all I can do",
},
messageTimestamp: '1584004417',
status: 'DELIVERY_ACK',
} as any
),
new BinaryNode(
'message',
{},
{
key: {
remoteJid: '917529871117@s.whatsapp.net',
fromMe: false,
id: 'A1230B8D6B0A1D793EC2AE2EA0C168D8',
},
message: { conversation: 'library' },
messageTimestamp: '1584004418',
} as any
),
]
)
],
[
'f8063f2dfafc0831323334353637385027fc0431323334f801f80228fc0701020304050607',
new BinaryNode(
'picture',
{jid: '12345678@s.whatsapp.net', id: '1234'},
[
new BinaryNode(
'image',
{},
Buffer.from([1,2,3,4,5,6,7])
)
]
)
]
]
it('should encode/decode strings', () => {
for(const [input, output] of TEST_VECTORS) {
const buff = Buffer.from(input, 'hex')
const node = BinaryNode.from(buff)
expect(
JSON.parse(JSON.stringify(node))
).toStrictEqual(
JSON.parse(JSON.stringify(output))
)
expect(
node.toBuffer().toString('hex')
).toStrictEqual(
input
)
}
})
})

View File

@@ -1,94 +0,0 @@
import { Boom } from '@hapi/boom'
import P from 'pino'
import BinaryNode from '../BinaryNode'
import makeConnection from '../Connection'
import { delay } from '../Utils/generics'
describe('QR Generation', () => {
it('should generate QR', async () => {
const QR_GENS = 1
const {ev} = makeConnection({
maxRetries: 0,
maxQRCodes: QR_GENS,
logger: P({ level: 'trace' })
})
let calledQR = 0
ev.on('connection.update', ({ qr }) => {
if(qr) calledQR += 1
})
await expect(open()).rejects.toThrowError('Too many QR codes')
expect(calledQR).toBeGreaterThanOrEqual(QR_GENS)
}, 60_000)
})
describe('Test Connect', () => {
const logger = P({ level: 'trace' })
it('should connect', async () => {
logger.info('please be ready to scan with your phone')
const conn = makeConnection({
logger,
printQRInTerminal: true
})
await conn.waitForConnection(true)
const { user, isNewLogin } = await conn.getState()
expect(user).toHaveProperty('jid')
expect(user).toHaveProperty('name')
expect(isNewLogin).toBe(true)
conn.end(undefined)
}, 65_000)
it('should restore session', async () => {
let conn = makeConnection({
printQRInTerminal: true,
logger,
})
await conn.waitForConnection(true)
conn.end(undefined)
await delay(2500)
conn = makeConnection({
printQRInTerminal: true,
logger,
})
await conn.waitForConnection(true)
const { user, isNewLogin, qr } = await conn.getState()
expect(user).toHaveProperty('jid')
expect(user).toHaveProperty('name')
expect(isNewLogin).toBe(false)
expect(qr).toBe(undefined)
conn.end(undefined)
}, 65_000)
it('should logout', async () => {
let conn = makeConnection({
printQRInTerminal: true,
logger,
})
await conn.waitForConnection(true)
const { user, qr } = await conn.getState()
expect(user).toHaveProperty('jid')
expect(user).toHaveProperty('name')
expect(qr).toBe(undefined)
const credentials = conn.getAuthInfo()
await conn.logout()
conn = makeConnection({
credentials,
logger
})
await expect(
conn.waitForConnection()
).rejects.toThrowError('Unexpected error in login')
}, 65_000)
})

View File

@@ -1,8 +0,0 @@
describe('Message Generation', () => {
it('should generate a text message', () => {
})
})

View File

@@ -1,165 +0,0 @@
import BinaryNode from '../BinaryNode'
import makeConnection from '../makeConnection'
import { delay } from '../WAConnection/Utils'
describe('Queries', () => {
/*it ('should correctly send updates for chats', async () => {
const conn = makeConnection({
pendingRequestTimeoutMs: undefined,
credentials: './auth_info.json'
})
const task = new Promise(resolve => conn.once('chats-received', resolve))
await conn.connect ()
await task
conn.close ()
const oldChat = conn.chats.all()[0]
oldChat.archive = 'true' // mark the first chat as archived
oldChat.modify_tag = '1234' // change modify tag to detect change
const promise = new Promise(resolve => conn.once('chats-update', resolve))
const result = await conn.connect ()
assert.ok (!result.newConnection)
const chats = await promise as Partial<WAChat>[]
const chat = chats.find (c => c.jid === oldChat.jid)
assert.ok (chat)
assert.ok ('archive' in chat)
assert.strictEqual (Object.keys(chat).length, 3)
assert.strictEqual (Object.keys(chats).length, 1)
conn.close ()
})
it ('should correctly send updates for contacts', async () => {
const conn = makeConnection ()
conn.pendingRequestTimeoutMs = null
conn.loadAuthInfo('./auth_info.json')
const task: any = new Promise(resolve => conn.once('contacts-received', resolve))
await conn.connect ()
const initialResult = await task
assert.strictEqual(
initialResult.updatedContacts.length,
Object.keys(conn.contacts).length
)
conn.close ()
const [jid] = Object.keys(conn.contacts)
const oldContact = conn.contacts[jid]
oldContact.name = 'Lol'
oldContact.index = 'L'
const promise = new Promise(resolve => conn.once('contacts-received', resolve))
const result = await conn.connect ()
assert.ok (!result.newConnection)
const {updatedContacts} = await promise as { updatedContacts: Partial<WAContact>[] }
const contact = updatedContacts.find (c => c.jid === jid)
assert.ok (contact)
assert.ok ('name' in contact)
assert.strictEqual (Object.keys(contact).length, 3)
assert.strictEqual (Object.keys(updatedContacts).length, 1)
conn.close ()
})*/
it('should queue requests when closed', async () => {
const conn = makeConnection({
credentials: './auth_info.json'
})
await conn.open()
await delay(2000)
conn.close()
const { user: { jid } } = await conn.getState()
const task: Promise<any> = conn.query({
json: ['query', 'Status', jid]
})
await delay(2000)
conn.open()
const json = await task
expect(json.status).toBeDefined()
conn.close()
}, 65_000)
it('[MANUAL] should recieve query response after phone disconnect', async () => {
const conn = makeConnection ({
printQRInTerminal: true,
credentials: './auth_info.json'
})
await conn.open()
const { phoneConnected } = await conn.getState()
expect(phoneConnected).toBe(true)
try {
const waitForEvent = expect => new Promise (resolve => {
conn.ev.on('state.update', ({phoneConnected}) => {
if (phoneConnected === expect) {
conn.ev.removeAllListeners('state.update')
resolve(undefined)
}
})
})
console.log('disconnect your phone from the internet')
await delay(10_000)
console.log('phone should be disconnected now, testing...')
const query = conn.query({
json: new BinaryNode(
'query',
{
epoch: conn.getSocket().currentEpoch().toString(),
type: 'message',
jid: '1234@s.whatsapp.net',
kind: 'before',
count: '10',
}
),
requiresPhoneConnection: true,
expect200: false
})
await waitForEvent(false)
console.log('reconnect your phone to the internet')
await waitForEvent(true)
console.log('reconnected successfully')
await expect(query).resolves.toBeDefined()
} finally {
conn.close()
}
}, 65_000)
it('should re-execute query on connection closed error', async () => {
const conn = makeConnection({
credentials: './auth_info.json'
})
await conn.open()
const { user: { jid } } = await conn.getState()
const task: Promise<any> = conn.query({ json: ['query', 'Status', jid], waitForOpen: true })
await delay(20)
// fake cancel the connection
conn.getSocket().ev.emit('message', '1234,["Pong",false]')
await delay(2000)
const json = await task
expect(json.status).toBeDefined()
conn.close()
}, 65_000)
})