diff --git a/.env.example b/.env.example index c82051095f..b769141c4f 100644 --- a/.env.example +++ b/.env.example @@ -333,6 +333,11 @@ QRCODE_LIMIT=30 # Color of the QRCode on base64 QRCODE_COLOR='#175197' +# Abuse safety guardrails +ABUSE_SAFETY_WHATSAPP_NUMBERS_MAX_BATCH_SIZE=50 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_SIZE=10 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_INTERVAL_MS=1000 + # Typebot - Environment variables TYPEBOT_ENABLED=false # old | latest diff --git a/.gitignore b/.gitignore index 1bbb894ae6..bbfc21c9d3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ lerna-debug.log* # Project related /instances/* !/instances/.gitkeep -/test/ /src/env.yml /store *.env @@ -47,4 +46,4 @@ lerna-debug.log* /prisma/migrations/* -_evo* \ No newline at end of file +_evo* diff --git a/README.md b/README.md index d39317f6c6..85d726e44a 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,14 @@ RabbitMQ, Amazon SQS, NATS, Pusher and WebSocket for events. Configurable per in ### Media handling Local storage or S3/MinIO. Automatic media download from WhatsApp. Optional audio transcription via OpenAI. +### Responsible messaging +Evolution API includes abuse-safety guardrails to reduce accidental bursts on +sensitive endpoints such as `/chat/whatsappNumbers`. These controls are not an +anti-ban feature and do not guarantee delivery or account safety. + +See [Responsible messaging and deliverability](./docs/responsible-messaging.md) +for configuration details, known limitations, and related community reports. + --- ## Documentation diff --git a/docs/responsible-messaging.md b/docs/responsible-messaging.md new file mode 100644 index 0000000000..e4719b9451 --- /dev/null +++ b/docs/responsible-messaging.md @@ -0,0 +1,70 @@ +# Responsible Messaging And Deliverability + +Evolution API is a messaging infrastructure project. Operators are responsible +for following WhatsApp and Meta policies, collecting opt-in consent, respecting +opt-out requests, and avoiding unsolicited or high-volume messaging. + +This guide documents guardrails that reduce accidental bursts and make risky +usage easier to identify. They are not anti-ban features, do not bypass platform +enforcement, and do not guarantee message delivery. + +## WhatsApp Number Checks + +The `/chat/whatsappNumbers/{instance}` endpoint can call WhatsApp Web through +Baileys when a number is not already cached. Large uncached batches create a +burst of platform checks from a single instance. + +Evolution API limits and chunks these checks by default: + +```env +ABUSE_SAFETY_WHATSAPP_NUMBERS_MAX_BATCH_SIZE=50 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_SIZE=10 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_INTERVAL_MS=1000 +``` + +When a request exceeds `ABUSE_SAFETY_WHATSAPP_NUMBERS_MAX_BATCH_SIZE`, the API +returns `429 Too Many Requests` with a `Retry-After` header and a structured +response that includes the configured limit. + +The chunk interval only adds backpressure between direct Baileys checks. Cached +numbers, groups, broadcasts, and newsletters do not require the same Baileys +lookup path. + +The endpoint also emits aggregate operational logs for accepted and rejected +checks, including requested count, cache hits/misses, Baileys query count, cache +writes, configured limits, elapsed time, and rejection reason. These logs avoid +raw phone numbers so operators can monitor risky usage patterns without leaking +contact data. + +## Responsible Operation + +- Use WhatsApp Business Platform / Cloud API for production business messaging + when possible. +- Send messages only to contacts who have explicitly opted in. +- Provide and honor opt-out flows. +- Keep batch sizes bounded and monitor failures, pending delivery, and user + complaints. +- Treat `delay` as application pacing only. It does not guarantee delivery, + account safety, or policy compliance. + +## Out Of Scope + +The guardrails in this project intentionally do not implement proxy rotation, +IP rotation, fingerprint randomization, automated warmup, human-like behavior +simulation, or guarantees that an account will not be restricted. + +## References + +- Community report about `/chat/whatsappNumbers` bulk check risk: + https://github.com/evolution-foundation/evolution-api/issues/2228 +- Community discussion about constant bans and high-volume sending: + https://github.com/evolution-foundation/evolution-api/issues/1870 +- High-volume queue/rate-limit question closed as usage support: + https://github.com/evolution-foundation/evolution-api/issues/2538 +- Deliverability reports with pending/one-tick messages: + https://github.com/evolution-foundation/evolution-api/issues/1854 + https://github.com/evolution-foundation/evolution-api/issues/2404 +- WhatsApp Business Messaging Policy: + https://whatsappbusiness.com/policy/ +- WhatsApp Business Terms: + https://www.whatsapp.com/legal/business-terms diff --git a/env.example b/env.example index 60d0ef115a..5f2d403e60 100644 --- a/env.example +++ b/env.example @@ -210,6 +210,13 @@ CONFIG_SESSION_PHONE_NAME=Chrome QRCODE_LIMIT=30 QRCODE_COLOR=#198754 +# =========================================== +# ABUSE SAFETY +# =========================================== +ABUSE_SAFETY_WHATSAPP_NUMBERS_MAX_BATCH_SIZE=50 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_SIZE=10 +ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_INTERVAL_MS=1000 + # =========================================== # INTEGRAÇÕES # =========================================== diff --git a/package.json b/package.json index d81bb691b2..aeedc22520 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "start:prod": "node --network-family-autoselection-attempt-timeout=1000 dist/main", "dev:server": "tsx watch ./src/main.ts", "test": "tsx watch ./test/all.test.ts", + "test:run": "node --import tsx --test ./test/all.test.ts", "lint": "eslint --fix --ext .ts src", "lint:check": "eslint --ext .ts src", "commit": "cz", diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index e042629994..02ca1601e5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -66,6 +66,7 @@ import { ChannelStartupService } from '@api/services/channel.service'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { + AbuseSafety, AudioConverter, CacheConf, Chatwoot, @@ -78,7 +79,12 @@ import { QrCode, S3, } from '@config/env.config'; -import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, + TooManyRequestsException, +} from '@exceptions'; import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import { createId as cuid } from '@paralleldrive/cuid2'; @@ -92,6 +98,11 @@ import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; +import { + buildWhatsappNumbersObservation, + resolveWhatsappNumbersGuardrails, + validateWhatsappNumbersBatch, +} from '@utils/whatsappNumbersGuardrails'; import audioDecode from 'audio-decode'; import axios from 'axios'; import makeWASocket, { @@ -4027,8 +4038,72 @@ export class BaileysStartupService extends ChannelStartupService { }); } + private getWhatsappNumbersGuardrails() { + const config = this.configService.get('ABUSE_SAFETY')?.WHATSAPP_NUMBERS; + + return resolveWhatsappNumbersGuardrails(config); + } + + private async queryOnWhatsappInChunks(numbers: string[], batchSize: number, intervalMs: number) { + const results: { jid: string; exists: boolean }[] = []; + + for (let index = 0; index < numbers.length; index += batchSize) { + const chunk = numbers.slice(index, index + batchSize); + const currentBatch = Math.floor(index / batchSize) + 1; + const totalBatches = Math.ceil(numbers.length / batchSize); + + this.logger.verbose(`Checking ${chunk.length} numbers via Baileys (${currentBatch}/${totalBatches})`); + results.push(...(await this.client.onWhatsApp(...chunk))); + + const hasMoreChunks = index + batchSize < numbers.length; + if (hasMoreChunks && intervalMs > 0) { + await delay(intervalMs); + } + } + + return results; + } + // Chat Controller public async whatsappNumber(data: WhatsAppNumberDto) { + const startedAt = Date.now(); + const guardrails = this.getWhatsappNumbersGuardrails(); + const validation = validateWhatsappNumbersBatch(data?.numbers, guardrails); + + if (validation.ok === false) { + this.logger.warn( + buildWhatsappNumbersObservation({ + requestedNumbers: validation.type === 'too_many_requests' ? validation.received : 0, + userJids: 0, + groupJids: 0, + broadcastJids: 0, + newsletterJids: 0, + cacheHits: 0, + cacheMisses: 0, + baileysQueries: 0, + cacheWrites: 0, + guardrails, + durationMs: Date.now() - startedAt, + rejected: true, + rejectionReason: validation.type, + }), + ); + + if (validation.type === 'too_many_requests') { + throw new TooManyRequestsException(validation.retryAfter, { + message: validation.message, + received: validation.received, + maxBatchSize: validation.maxBatchSize, + retryAfter: validation.retryAfter, + docs: validation.docs, + reference: validation.reference, + }); + } + + throw new BadRequestException(validation.message); + } + + const numbers = validation.numbers; const jids: { groups: { number: string; jid: string }[]; broadcast: { number: string; jid: string }[]; @@ -4036,11 +4111,13 @@ export class BaileysStartupService extends ChannelStartupService { } = { groups: [], broadcast: [], users: [] }; const onWhatsapp: OnWhatsAppDto[] = []; + let newsletterJids = 0; - data.numbers.forEach((number) => { + numbers.forEach((number) => { const jid = createJid(number); if (isJidNewsletter(jid)) { + newsletterJids += 1; onWhatsapp.push( new OnWhatsAppDto( jid, @@ -4093,14 +4170,18 @@ export class BaileysStartupService extends ChannelStartupService { // Separate numbers that are and are not in cache const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions)); const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid)); + const cacheHits = numbersToVerify.length - numbersNotInCache.length; // Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache let verify: { jid: string; exists: boolean }[] = []; const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid')); if (normalNumbersNotInCache.length > 0) { - this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`); - verify = await this.client.onWhatsApp(...normalNumbersNotInCache); + verify = await this.queryOnWhatsappInChunks( + normalNumbersNotInCache, + guardrails.queryBatchSize, + guardrails.queryBatchIntervalMs, + ); } const verifiedUsers = await Promise.all( @@ -4207,6 +4288,22 @@ export class BaileysStartupService extends ChannelStartupService { ); } + this.logger.verbose( + buildWhatsappNumbersObservation({ + requestedNumbers: numbers.length, + userJids: jids.users.length, + groupJids: jids.groups.length, + broadcastJids: jids.broadcast.length, + newsletterJids, + cacheHits, + cacheMisses: numbersNotInCache.length, + baileysQueries: normalNumbersNotInCache.length, + cacheWrites: numbersToCache.length, + guardrails, + durationMs: Date.now() - startedAt, + }), + ); + return onWhatsapp; } diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 86f4d65bee..e6fc21014e 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -62,7 +62,16 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); } catch (error) { console.log(error); - return res.status(HttpStatus.BAD_REQUEST).json(error); + const status = error?.status || HttpStatus.BAD_REQUEST; + if (error?.retryAfter != null) { + res.set('Retry-After', String(error.retryAfter)); + } + + return res.status(status).json({ + status, + error: error?.error || 'Bad Request', + message: error?.details ?? error?.message ?? error, + }); } }) .post(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 45c43fca5b..89791d01b1 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -32,6 +32,7 @@ enum HttpStatus { FORBIDDEN = 403, BAD_REQUEST = 400, UNAUTHORIZED = 401, + TOO_MANY_REQUESTS = 429, INTERNAL_SERVER_ERROR = 500, } diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 1b17a83b53..c0ddec137b 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -3,6 +3,16 @@ import dotenv from 'dotenv'; dotenv.config(); +const positiveIntFromEnv = (value: string | undefined, fallback: number) => { + const parsed = Number.parseInt(value ?? '', 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +const nonNegativeIntFromEnv = (value: string | undefined, fallback: number) => { + const parsed = Number.parseInt(value ?? '', 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; +}; + export type HttpServer = { NAME: string; TYPE: 'http' | 'https'; @@ -328,6 +338,13 @@ export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPu export type ConfigSessionPhone = { CLIENT: string; NAME: string }; export type Baileys = { VERSION?: string }; export type QrCode = { LIMIT: number; COLOR: string }; +export type AbuseSafety = { + WHATSAPP_NUMBERS: { + MAX_BATCH_SIZE: number; + QUERY_BATCH_SIZE: number; + QUERY_BATCH_INTERVAL_MS: number; + }; +}; export type Typebot = { ENABLED: boolean; API_VERSION: string; SEND_MEDIA_BASE64: boolean }; export type Chatwoot = { ENABLED: boolean; @@ -427,6 +444,7 @@ export interface Env { CONFIG_SESSION_PHONE: ConfigSessionPhone; BAILEYS: Baileys; QRCODE: QrCode; + ABUSE_SAFETY: AbuseSafety; TYPEBOT: Typebot; CHATWOOT: Chatwoot; OPENAI: Openai; @@ -837,6 +855,16 @@ export class ConfigService { LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, COLOR: process.env.QRCODE_COLOR || '#198754', }, + ABUSE_SAFETY: { + WHATSAPP_NUMBERS: { + MAX_BATCH_SIZE: positiveIntFromEnv(process.env.ABUSE_SAFETY_WHATSAPP_NUMBERS_MAX_BATCH_SIZE, 50), + QUERY_BATCH_SIZE: positiveIntFromEnv(process.env.ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_SIZE, 10), + QUERY_BATCH_INTERVAL_MS: nonNegativeIntFromEnv( + process.env.ABUSE_SAFETY_WHATSAPP_NUMBERS_QUERY_BATCH_INTERVAL_MS, + 1000, + ), + }, + }, TYPEBOT: { ENABLED: process.env?.TYPEBOT_ENABLED === 'true', API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', diff --git a/src/exceptions/429.exception.ts b/src/exceptions/429.exception.ts new file mode 100644 index 0000000000..cdb115d8ff --- /dev/null +++ b/src/exceptions/429.exception.ts @@ -0,0 +1,14 @@ +import { HttpStatus } from '@api/routes/index.router'; + +export class TooManyRequestsException extends Error { + public readonly status = HttpStatus.TOO_MANY_REQUESTS; + public readonly error = 'Too Many Requests'; + public readonly retryAfter?: number; + public readonly details?: any[]; + + constructor(retryAfter?: number, ...objectError: any[]) { + super('Too Many Requests'); + this.retryAfter = retryAfter; + this.details = objectError.length > 0 ? objectError : undefined; + } +} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts index 1657fc2f73..e1abd8c784 100644 --- a/src/exceptions/index.ts +++ b/src/exceptions/index.ts @@ -2,4 +2,5 @@ export * from './400.exception'; export * from './401.exception'; export * from './403.exception'; export * from './404.exception'; +export * from './429.exception'; export * from './500.exception'; diff --git a/src/utils/whatsappNumbersGuardrails.ts b/src/utils/whatsappNumbersGuardrails.ts new file mode 100644 index 0000000000..681126ca6b --- /dev/null +++ b/src/utils/whatsappNumbersGuardrails.ts @@ -0,0 +1,118 @@ +export type WhatsappNumbersGuardrailsConfig = { + MAX_BATCH_SIZE?: number; + QUERY_BATCH_SIZE?: number; + QUERY_BATCH_INTERVAL_MS?: number; +}; + +export type WhatsappNumbersGuardrails = { + maxBatchSize: number; + queryBatchSize: number; + queryBatchIntervalMs: number; +}; + +export type WhatsappNumbersValidationResult = + | { + ok: true; + numbers: string[]; + } + | { + ok: false; + type: 'bad_request'; + message: string; + } + | { + ok: false; + type: 'too_many_requests'; + message: string; + received: number; + maxBatchSize: number; + retryAfter: number; + docs: string; + reference: string; + }; + +export type WhatsappNumbersObservation = { + action: 'whatsappNumbers.check'; + requestedNumbers: number; + userJids: number; + groupJids: number; + broadcastJids: number; + newsletterJids: number; + cacheHits: number; + cacheMisses: number; + baileysQueries: number; + cacheWrites: number; + maxBatchSize: number; + queryBatchSize: number; + queryBatchIntervalMs: number; + durationMs: number; + rejected: boolean; + rejectionReason?: string; +}; + +export type WhatsappNumbersObservationInput = Omit< + WhatsappNumbersObservation, + 'action' | 'maxBatchSize' | 'queryBatchSize' | 'queryBatchIntervalMs' | 'rejected' +> & { + guardrails: WhatsappNumbersGuardrails; + rejected?: boolean; +}; + +export const resolveWhatsappNumbersGuardrails = ( + config?: WhatsappNumbersGuardrailsConfig, +): WhatsappNumbersGuardrails => ({ + maxBatchSize: Math.max(1, config?.MAX_BATCH_SIZE ?? 50), + queryBatchSize: Math.max(1, config?.QUERY_BATCH_SIZE ?? 10), + queryBatchIntervalMs: Math.max(0, config?.QUERY_BATCH_INTERVAL_MS ?? 1000), +}); + +export const validateWhatsappNumbersBatch = ( + numbers: unknown, + guardrails: WhatsappNumbersGuardrails, +): WhatsappNumbersValidationResult => { + if (!Array.isArray(numbers)) { + return { + ok: false, + type: 'bad_request', + message: 'numbers must be an array of WhatsApp identifiers.', + }; + } + + if (numbers.length === 0) { + return { + ok: false, + type: 'bad_request', + message: 'At least one WhatsApp number must be provided.', + }; + } + + if (numbers.length > guardrails.maxBatchSize) { + const retryAfter = Math.max(1, Math.ceil(guardrails.queryBatchIntervalMs / 1000)); + + return { + ok: false, + type: 'too_many_requests', + message: `whatsappNumbers accepts up to ${guardrails.maxBatchSize} numbers per request.`, + received: numbers.length, + maxBatchSize: guardrails.maxBatchSize, + retryAfter, + docs: 'docs/responsible-messaging.md', + reference: 'https://github.com/evolution-foundation/evolution-api/issues/2228', + }; + } + + return { ok: true, numbers }; +}; + +export const buildWhatsappNumbersObservation = ({ + guardrails, + rejected = false, + ...observation +}: WhatsappNumbersObservationInput): WhatsappNumbersObservation => ({ + action: 'whatsappNumbers.check', + rejected, + maxBatchSize: guardrails.maxBatchSize, + queryBatchSize: guardrails.queryBatchSize, + queryBatchIntervalMs: guardrails.queryBatchIntervalMs, + ...observation, +}); diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index 6677b652fd..03a8dc4231 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -39,6 +39,7 @@ export const whatsappNumberSchema: JSONSchema7 = { }, }, }, + required: ['numbers'], }; export const readMessageSchema: JSONSchema7 = { diff --git a/test/all.test.ts b/test/all.test.ts new file mode 100644 index 0000000000..72eb518493 --- /dev/null +++ b/test/all.test.ts @@ -0,0 +1,109 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { + buildWhatsappNumbersObservation, + resolveWhatsappNumbersGuardrails, + validateWhatsappNumbersBatch, +} from '../src/utils/whatsappNumbersGuardrails'; + +describe('whatsappNumbers guardrails', () => { + it('uses conservative defaults when no environment override exists', () => { + assert.deepEqual(resolveWhatsappNumbersGuardrails(), { + maxBatchSize: 50, + queryBatchSize: 10, + queryBatchIntervalMs: 1000, + }); + }); + + it('normalizes invalid guardrail values to safe minimums', () => { + assert.deepEqual( + resolveWhatsappNumbersGuardrails({ + MAX_BATCH_SIZE: 0, + QUERY_BATCH_SIZE: -10, + QUERY_BATCH_INTERVAL_MS: -1, + }), + { + maxBatchSize: 1, + queryBatchSize: 1, + queryBatchIntervalMs: 0, + }, + ); + }); + + it('rejects missing, non-array, and empty numbers payloads', () => { + const guardrails = resolveWhatsappNumbersGuardrails(); + + assert.deepEqual(validateWhatsappNumbersBatch(undefined, guardrails), { + ok: false, + type: 'bad_request', + message: 'numbers must be an array of WhatsApp identifiers.', + }); + + assert.deepEqual(validateWhatsappNumbersBatch('5511999999999', guardrails), { + ok: false, + type: 'bad_request', + message: 'numbers must be an array of WhatsApp identifiers.', + }); + + assert.deepEqual(validateWhatsappNumbersBatch([], guardrails), { + ok: false, + type: 'bad_request', + message: 'At least one WhatsApp number must be provided.', + }); + }); + + it('rejects oversized batches with retry metadata and docs reference', () => { + const guardrails = resolveWhatsappNumbersGuardrails({ + MAX_BATCH_SIZE: 2, + QUERY_BATCH_INTERVAL_MS: 1500, + }); + + assert.deepEqual(validateWhatsappNumbersBatch(['1', '2', '3'], guardrails), { + ok: false, + type: 'too_many_requests', + message: 'whatsappNumbers accepts up to 2 numbers per request.', + received: 3, + maxBatchSize: 2, + retryAfter: 2, + docs: 'docs/responsible-messaging.md', + reference: 'https://github.com/evolution-foundation/evolution-api/issues/2228', + }); + }); + + it('builds aggregate observations without including raw phone numbers', () => { + const observation = buildWhatsappNumbersObservation({ + requestedNumbers: 12, + userJids: 10, + groupJids: 1, + broadcastJids: 1, + newsletterJids: 0, + cacheHits: 7, + cacheMisses: 3, + baileysQueries: 3, + cacheWrites: 2, + guardrails: resolveWhatsappNumbersGuardrails({ MAX_BATCH_SIZE: 50, QUERY_BATCH_SIZE: 5 }), + durationMs: 25, + }); + + assert.deepEqual(observation, { + action: 'whatsappNumbers.check', + requestedNumbers: 12, + userJids: 10, + groupJids: 1, + broadcastJids: 1, + newsletterJids: 0, + cacheHits: 7, + cacheMisses: 3, + baileysQueries: 3, + cacheWrites: 2, + maxBatchSize: 50, + queryBatchSize: 5, + queryBatchIntervalMs: 1000, + durationMs: 25, + rejected: false, + }); + + assert.equal(JSON.stringify(observation).includes('5511999999999'), false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0b3810e379..63853d3013 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,9 +30,10 @@ }, "moduleResolution": "Node" }, - "exclude": ["node_modules", "./test", "./dist", "./prisma"], + "exclude": ["node_modules", "./dist", "./prisma"], "include": [ "src/**/*", - "src/**/*.json" + "src/**/*.json", + "test/**/*.ts" ] -} \ No newline at end of file +}