246 lines
7.4 KiB
Go
246 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|