On this page
Test environmentHighest-value test areas (by risk)Edge cases & gotchas to probeRegression must-notsKnown gaps to log, not file as bugsFor Quality
What to test, and where the edges are. Written for QA.
Test environment
The feat-crm-module sandbox has Meta connectable and TikTok/Snapchat/Google disabled. For full integration flows without real platform apps, drive packages/platform-mock — it posts correctly signed webhooks and serves a mock Graph API (point META_GRAPH_URL at it). It also offers deterministic insight spend (campaign 5001 = 100/day, 5002 = 40/day) so CPL/ROI math is hand-checkable, a tamperSignature option to test 401 rejection, and a silent-lead endpoint to simulate a dropped webhook for reconciliation testing.
Highest-value test areas (by risk)
1. Webhook ingestion & idempotency
- Send the same webhook twice → exactly one lead/message (dedupe on
[platform, externalEventId]).
- Tampered signature →
401, no persistence. - Unroutable event (unknown asset id) → stored
DEAD, not attributed to any company. - Force processing failure → event goes
FAILED, retries (5, backoff), thenDEAD; verify a
topic with no handler is parked as RECEIVED (not failed) and replays if enabled later.
- Ack timing: the controller returns
200before processing — verify a slow processor
doesn't cause the platform to see a timeout.
2. Lead dedupe & normalization
- Same phone in Latin, Arabic-Indic (٠١٢٣), and Eastern-Arabic (۰۱۲۳) digits → one lead.
- Phone with spaces/dashes/RTL marks → normalizes to the same E.164.
- Existing patient vs new lead with same phone → patient wins, no duplicate lead
(EXISTING_LEAD_PHONE).
- Round-robin: create N leads into a stage with M users → assignments rotate evenly.
- Lead captured before campaign hierarchy sync → still attributes once hierarchy arrives
(raw-id attribution).
3. Unified inbox & the 24h window
- Reply inside 24h → sent; reply after → blocked with
MESSAGING_WINDOW_CLOSEDat both
the server pre-check and the gateway (the gateway is authoritative — test that the client pre-check can be bypassed but the gateway still refuses).
- Comment channels (FB/IG) → composer disabled.
- WhatsApp channel → rejected by
sendConversationMessage(must use the WA path); confirm
the existing WhatsApp module is unchanged (regression).
- SLA lifecycle: inbound arms
firstResponseDueAt; cross the deadline → cron flips
slaBreachedAt within ~5 min; reply → flag clears and firstRespondedAt sets.
- Auto-create leads on → new DM from a stranger creates a lead in the default stage; off →
it doesn't.
- Sending on a non-ACTIVE integration →
INTEGRATION_INACTIVE.
4. Automation
- Each live trigger fires: Lead created (from ad capture, manual add, QR, import,
patient→pipeline), Stage changed, Stale in stage (after staleDays).
- Once-per-event: redeliver the source event → rule runs once (
[ruleId, dedupeKey]). - Stale re-fire: a lead that leaves and re-enters a stage → fires again (anchored on
stagedAt); a lead sitting still → does not re-fire.
- Depth cap: a move-stage action that would trigger a stage-changed rule → does not loop.
- Actions execute in order; verify assign/tag/move/WA-template each apply.
- Confirm the 5 unemitted triggers are hidden in the builder.
5. Marketing hub / ROI math
- With deterministic mock spend, hand-check CPL (spend ÷ CRM leads), CPA/CAC (spend ÷
won leads), ROAS (collected revenue ÷ effective spend), contribution (revenueNet − spend).
—vs0: zero leads → CPL shows—, not0.- Effective spend prefers booked GL spend over platform spend when posted.
- Revenue bounding: invoices dated before a lead's capture date must not be credited
to the campaign; only on/after count.
- Mixed currency: ad accounts in different currencies → no summed figure, warning shown.
- Reconciliation report surfaces platform-vs-CRM lead gaps (use
silent-leadto create one,
then confirm the nightly/5-min poll heals it).
6. Reviews
- Send request → WhatsApp with a
/r/<code>link; tap →clickedAtset, statusCLICKED,
302 to the review URL.
- Cooldown: second request inside cooldown → blocked; a prior FAILED request does not
block.
- Analytics aggregate correctly per branch/status/rating.
7. Security & retention
- Platform tokens are never present in any GraphQL response (inspect payloads).
WebhookEvent.payloadolder than 90 days → redacted to{ redacted: true }; RECEIVED
rows exempt.
- Gateway fails to boot without
SOCIAL_TOKEN_ENC_KEY. yarn encrypt-wa-tokensis idempotent (re-run → skips encrypted rows).- Permission matrix: test each role (owner/doctor/receptionist/accountant/hrmanager from the
sandbox roster) against each CRM screen — verify VIEW_MARKETING_ROI hides revenue from those without it, etc.
Edge cases & gotchas to probe
- Web/mobile flag asymmetry: turn a
FEATURE_CRM_*flag off → web hides the surface,
mobile still shows it (permissions + leadsEnabled only). Confirm expected behavior.
- Reconnect a platform after token expiry → asset mappings (stage/branch, enabled) survive.
- Counter drift: manually skew a
LeadStage.totalLeads→ nightlyreconcileCrmCounters
repairs it and reports drift to Sentry.
- Legacy WhatsApp token: a row still holding a signed-JWT token →
readSocialTokenstill
reads it (dual-read) before/after running the encrypt script.
- Google no-OAuth path: wrong
google_keyon the lead-form webhook → rejected.
Regression must-nots
- The existing WhatsApp conversation experience must be unchanged.
- Existing Leads functionality must keep working while the deprecated
Leadcolumns are
still present (the CI tripwire audit-deprecated-lead-fields.sh must stay green).
Known gaps to log, not file as bugs
TikTok/Snapchat/Google connect disabled in beta; comment replies unsupported; five automation triggers hidden; Google review ingestion not yet live; the "~1,266 req/s" figure unbacked by a repo benchmark. See Scope, Gaps & Honest Notes.