dana-medical-site/internal/crm/client_test.go
Abdulkadyr Karagishiev 121f286016 feat(crm): integrate website leads and online booking with new CRM and 1C
- Add internal/crm client with CanonicalJSON and HMAC-SHA256 signing for POST /api/public/leads
- Add database migration 0026_crm_deals.sql with crm_deal_id and partial indexes
- Add async lead intake dispatch to new CRM in handlers_leads.go
- Support dual-mode booking confirmation (CRM_MODE=both/crm_only/bitrix_only) in booking_confirm.go
- Add LinkCRM endpoint to bridge_1c.py and onec.Client
- Update config and env examples with CRM_API_URL, CRM_LEAD_INTAKE_SECRET, CRM_MODE
2026-10-16 18:01:49 +03:00

111 lines
2.9 KiB
Go

package crm
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestCanonicalJSONAndSignature(t *testing.T) {
draft := LeadDraft{
Name: "Иван Иванов",
Phone: "+79991234567",
Comment: "Тестовая запись",
SourceLabel: "Сайт",
}
canon, err := CanonicalJSON(draft)
if err != nil {
t.Fatalf("CanonicalJSON failed: %v", err)
}
// Verify that keys are sorted in canonical JSON
expectedPrefix := `{"comment":"Тестовая запись","name":"Иван Иванов","phone":"+79991234567","sourceLabel":"Сайт"}`
if canon != expectedPrefix {
t.Errorf("CanonicalJSON got %s, want %s", canon, expectedPrefix)
}
secret := "test-secret-key-123"
timestamp := "1729080000000"
idempotencyKey := "idemp-key-12345"
sig := SignPayload(secret, timestamp, idempotencyKey, canon)
if len(sig) != 64 {
t.Errorf("expected 64-char hex signature, got %s", sig)
}
// Manual check
h := sha256.Sum256([]byte(canon))
ph := hex.EncodeToString(h[:])
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "." + idempotencyKey + "." + ph))
expectedSig := hex.EncodeToString(mac.Sum(nil))
if sig != expectedSig {
t.Errorf("signature mismatch: got %s, want %s", sig, expectedSig)
}
}
func TestClientCreateLeadSuccess(t *testing.T) {
secret := "secret123"
expectedDealID := "deal_xyz987"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/public/leads" {
t.Errorf("unexpected path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
if r.Method != http.MethodPost {
t.Errorf("unexpected method: %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
timestamp := r.Header.Get("x-gt-timestamp")
idempotencyKey := r.Header.Get("x-gt-idempotency-key")
sig := r.Header.Get("x-gt-signature")
if timestamp == "" || idempotencyKey == "" || sig == "" {
t.Errorf("missing headers: timestamp=%s, idemp=%s, sig=%s", timestamp, idempotencyKey, sig)
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Errorf("failed to decode body: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(LeadResponse{
OK: true,
DealID: expectedDealID,
})
}))
defer ts.Close()
client := New(ts.URL, secret)
if !client.Configured() {
t.Fatal("client must be configured")
}
dealID, err := client.CreateLead(context.Background(), LeadDraft{
Name: "Анна Петрова",
Phone: "+79281112233",
}, "test-key-001")
if err != nil {
t.Fatalf("CreateLead failed: %v", err)
}
if dealID != expectedDealID {
t.Errorf("got dealID %s, want %s", dealID, expectedDealID)
}
}