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) } }