- 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
111 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|