Add leads handlers, WhatsApp integration, and test files
This commit is contained in:
245
apps/go-crm/internal/handlers/leads_qr_test.go
Normal file
245
apps/go-crm/internal/handlers/leads_qr_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user