Files
warmbly/internal/api/handler/crm_provider_test.go
T

117 lines
3.9 KiB
Go

package handler
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/warmbly/warmbly/internal/app/hubspot"
)
func hubspotSigned(t *testing.T, secret, signedURL, path, body string) *http.Request {
t.Helper()
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(http.MethodPost + signedURL + body + ts))
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
req.Header.Set("X-HubSpot-Signature-v3", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
req.Header.Set("X-HubSpot-Request-Timestamp", ts)
return req
}
func TestVerifyHubSpotUsesPublicURLOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("BACKEND_PUBLIC_URL", "https://api.example.com")
h := &Handler{HubSpot: hubspot.New(hubspot.Deps{ClientSecret: "shh"})}
path := "/api/v1/integrations/hubspot/webhooks"
body := `[]`
check := func(req *http.Request) bool {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = req
return h.verifyHubSpot(c, []byte(body))
}
if !check(hubspotSigned(t, "shh", "https://api.example.com"+path, path, body)) {
t.Fatal("a signature over the public URL was refused")
}
req := hubspotSigned(t, "shh", "https://attacker.example"+path, path, body)
req.Host = "attacker.example"
req.Header.Set("X-Forwarded-Proto", "https")
if check(req) {
t.Fatal("a signature over a client-chosen host was accepted")
}
v2 := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
v2.Header.Set("X-HubSpot-Signature", "deadbeef")
if check(v2) {
t.Fatal("an untimestamped signature was accepted")
}
}
func TestHubSpotServerRoutesRefuseCardFetches(t *testing.T) {
cases := map[string]bool{
"": false,
"portalId=1": true,
"appId=2": true,
"userId=3": true,
"userEmail=a%40b.com": true,
"PORTALID=1": true,
"other=1": false,
"portalId=1&userId=3&appId=2&x=1": true,
}
for raw, want := range cases {
q, err := url.ParseQuery(raw)
if err != nil {
t.Fatal(err)
}
if got := fromHubSpotFetch(q); got != want {
t.Errorf("fromHubSpotFetch(%q) = %v, want %v", raw, got, want)
}
}
gin.SetMode(gin.TestMode)
h := &Handler{HubSpot: hubspot.New(hubspot.Deps{ClientSecret: "shh"})}
for _, route := range []func(*gin.Context){h.HubSpotWebhook, h.HubSpotActionEnroll, h.HubSpotActionCampaigns} {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/x?portalId=1&userId=3&userEmail=a%40b.com&appId=2", strings.NewReader(`{}`))
route(c)
if got := c.Writer.Status(); got != http.StatusForbidden {
t.Errorf("a card fetch to a server route answered %d, want 403", got)
}
}
}
func TestHubSpotFetchUserNeedsOneOfEach(t *testing.T) {
cases := map[string]bool{
"portalId=1&userId=2&userEmail=a%40b.com": true,
"portalId=1&userId=2": false,
"portalId=1&userEmail=a%40b.com": false,
"userId=2&userEmail=a%40b.com": false,
"portalId=1&userId=2&userEmail=a%40b.com&userEmail=c%40d.com": false,
"portalId=1&userId=2&userId=9&userEmail=a%40b.com": false,
"portalId=1&portalId=9&userId=2&userEmail=a%40b.com": false,
"portalId=1&userId=2&userEmail=+": false,
}
for raw, want := range cases {
q, err := url.ParseQuery(raw)
if err != nil {
t.Fatal(err)
}
u, ok := hubspotFetchUser(q)
if ok != want {
t.Errorf("hubspotFetchUser(%q) ok = %v, want %v", raw, ok, want)
}
if ok && (u.ID != "2" || u.Email != "a@b.com") {
t.Errorf("hubspotFetchUser(%q) = %+v", raw, u)
}
}
}