Add leads handlers, WhatsApp integration, and test files

This commit is contained in:
2026-05-02 17:40:38 +00:00
parent 8833e38b80
commit 57e6e5a6fd
24 changed files with 2305 additions and 115 deletions

View File

@@ -0,0 +1,245 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"go-crm/internal/whatsapp"
"golang.org/x/crypto/bcrypt"
)
func newQRTestEnv(t *testing.T) (clientID int64, sessionID string, cleanup func()) {
t.Helper()
testDB, cleanupDB := SetupTestDB(t)
DB = testDB
WAConnector = whatsapp.NewFakeConnector()
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
testDB.Exec(
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
"qr-test@example.com", "QR Test Account", string(hashedPassword), time.Now().Unix(),
)
var accountID int64
testDB.QueryRow("SELECT account_id FROM accounts WHERE email = ?", "qr-test@example.com").Scan(&accountID)
sessionID = "qr-test-session"
testDB.Exec(
"INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)",
sessionID, accountID, time.Now().Add(time.Hour).Unix(),
)
testDB.QueryRow(
"INSERT INTO clients (account_id, name, phone, created_at) VALUES (?, ?, ?, ?) RETURNING client_id",
accountID, "QR Test Client", "+5521999999999", time.Now().Unix(),
).Scan(&clientID)
return clientID, sessionID, cleanupDB
}
func TestLeadsQR_FirstPollReturnsQRCode(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v — body: %s", err, w.Body.String())
}
if resp["qr"] == nil || resp["qr"] == "" {
t.Errorf("expected non-empty qr field, got: %v", resp)
}
if resp["status"] != "waiting" {
t.Errorf("expected status=waiting, got: %v", resp["status"])
}
}
func TestLeadsQR_AlreadyConnectedReturnsReady(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
// Mark the client as connected in the fake connector.
WAConnector.(*whatsapp.FakeConnector).MarkConnected(clientID)
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if resp["status"] != "ready" {
t.Errorf("expected status=ready, got: %v", resp["status"])
}
if resp["qr"] != nil {
t.Errorf("expected no qr field when already connected, got: %v", resp["qr"])
}
}
func TestLeadsQR_ConnectorErrorReturnsErrorStatus(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
fake := whatsapp.NewFakeConnector()
fake.SetConnectError(fmt.Errorf("connector unavailable"))
WAConnector = fake
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if resp["status"] != "error" {
t.Errorf("expected status=error, got: %v", resp["status"])
}
if resp["error"] == nil || resp["error"] == "" {
t.Errorf("expected non-empty error field")
}
}
func TestLeadsQR_UnknownClientReturnsNotFound(t *testing.T) {
_, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id=99999", nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
}
}
func TestLeadsQR_NoSessionReturnsUnauthorized(t *testing.T) {
clientID, _, cleanup := newQRTestEnv(t)
defer cleanup()
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
// No session cookie.
w := httptest.NewRecorder()
LeadsQR(w, req)
// /leads/qr is API-mode: returns JSON {"error":"unauthorized"}, not a redirect.
body := w.Body.String()
if !strings.Contains(body, "unauthorized") {
t.Errorf("expected unauthorized in response body, got: %s", body)
}
// Must not return a QR code or ready status.
if strings.Contains(body, `"qr"`) || strings.Contains(body, `"ready"`) {
t.Errorf("unauthenticated response must not contain qr or ready: %s", body)
}
}
func TestLeadsQR_WAConnectorNilReturns503(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
WAConnector = nil
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
if w.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d: %s", w.Code, w.Body.String())
}
}
func TestLeadsQR_SecondPollBeforeScanStillReturnsQR(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
// Pre-queue two QR frames so both polls have something to read from the shared channel.
fake := WAConnector.(*whatsapp.FakeConnector)
fake.QueueQR(clientID,
whatsapp.QRFrame{QR: "qr-code-poll-1", State: whatsapp.StateWaitingQR},
whatsapp.QRFrame{QR: "qr-code-poll-2", State: whatsapp.StateWaitingQR},
)
// First poll.
req1 := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req1.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w1 := httptest.NewRecorder()
LeadsQR(w1, req1)
var resp1 map[string]interface{}
json.Unmarshal(w1.Body.Bytes(), &resp1)
if resp1["status"] != "waiting" {
t.Fatalf("first poll: expected waiting, got %v — body: %s", resp1["status"], w1.Body.String())
}
// Second poll — same channel reused, second QR frame available.
req2 := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req2.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w2 := httptest.NewRecorder()
LeadsQR(w2, req2)
var resp2 map[string]interface{}
if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil {
t.Fatalf("second poll: invalid JSON: %v — body: %s", err, w2.Body.String())
}
if resp2["status"] == "ready" {
t.Errorf("second poll: got ready before scan — IsConnected/IsLoggedIn mismatch bug present")
}
if resp2["qr"] == nil || resp2["qr"] == "" {
t.Errorf("second poll: expected QR code, got: %v", resp2)
}
}
func TestLeadsQR_QRCodeResponseContainsClientID(t *testing.T) {
clientID, sessionID, cleanup := newQRTestEnv(t)
defer cleanup()
req := httptest.NewRequest(http.MethodGet, "/leads/qr?client_id="+strconv.FormatInt(clientID, 10), nil)
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
w := httptest.NewRecorder()
LeadsQR(w, req)
body := w.Body.String()
if !strings.Contains(body, strconv.FormatInt(clientID, 10)) {
t.Errorf("response missing client_id: %s", body)
}
}