Skip to content

Commit

Permalink
email/pardot: Add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
beautifulentropy committed Feb 19, 2025
1 parent 4c788db commit 81aa930
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 4 deletions.
8 changes: 4 additions & 4 deletions email/pardot.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (pc *PardotClientImpl) CreateProspect(email string) error {

req, err := http.NewRequest("POST", pc.prospectsURL, bytes.NewReader(payload))
if err != nil {
finalErr = fmt.Errorf("failed to create prospects request: %w", err)
finalErr = fmt.Errorf("failed to create new prospect request: %w", err)
continue
}
req.Header.Set("Content-Type", "application/json")
Expand All @@ -169,7 +169,7 @@ func (pc *PardotClientImpl) CreateProspect(email string) error {

resp, err := http.DefaultClient.Do(req)
if err != nil {
finalErr = fmt.Errorf("prospects request failed: %w", err)
finalErr = fmt.Errorf("create prospect request failed: %w", err)
continue
}

Expand All @@ -180,10 +180,10 @@ func (pc *PardotClientImpl) CreateProspect(email string) error {

body, err := io.ReadAll(resp.Body)
if err != nil {
finalErr = fmt.Errorf("prospects request returned status %d; while reading body: %w", resp.StatusCode, err)
finalErr = fmt.Errorf("create prospect request returned status %d; while reading body: %w", resp.StatusCode, err)
continue
}
finalErr = fmt.Errorf("prospects request returned status %d: %s", resp.StatusCode, redactEmail(body, email))
finalErr = fmt.Errorf("create prospect request returned status %d: %s", resp.StatusCode, redactEmail(body, email))
continue
}

Expand Down
190 changes: 190 additions & 0 deletions email/pardot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package email

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/test"
)

func TestCreateProspectSuccess(t *testing.T) {
t.Parallel()

tokenHandler := func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "dummy",
"expires_in": 3600,
})
}

prospectHandler := func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer dummy" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}

tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler))
defer tokenSrv.Close()

prospectSrv := httptest.NewServer(http.HandlerFunc(prospectHandler))
defer prospectSrv.Close()

clk := clock.NewFake()
client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, prospectSrv.URL)
test.AssertNotError(t, err, "failed to create client")

err = client.CreateProspect("[email protected]")
test.AssertNotError(t, err, "CreateProspect should succeed")
}

func TestCreateProspectUpdateTokenFails(t *testing.T) {
t.Parallel()

tokenHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "token error")
}

prospectHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler))
defer tokenSrv.Close()

prospectSrv := httptest.NewServer(http.HandlerFunc(prospectHandler))
defer prospectSrv.Close()

clk := clock.NewFake()
client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, prospectSrv.URL)
test.AssertNotError(t, err, "Failed to create client")

err = client.CreateProspect("[email protected]")
test.AssertError(t, err, "Expected token update to fail")
test.AssertContains(t, err.Error(), "failed to update token")
}

func TestCreateProspect4xx(t *testing.T) {
t.Parallel()

gotToken := false
tokenHandler := func(w http.ResponseWriter, r *http.Request) {
gotToken = true
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "dummy",
"expires_in": 3600,
})
}

prospectHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "bad request")
}

tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler))
defer tokenSrv.Close()

prospectSrv := httptest.NewServer(http.HandlerFunc(prospectHandler))
defer prospectSrv.Close()

clk := clock.NewFake()
client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, prospectSrv.URL)
test.AssertNotError(t, err, "Failed to create client")

err = client.CreateProspect("[email protected]")
test.Assert(t, gotToken, "Did not attempt to get token")
test.AssertError(t, err, "Should fail on 400")
test.AssertContains(t, err.Error(), "create prospect request returned status 400")
}

func TestCreateProspectTokenExpiry(t *testing.T) {
t.Parallel()

// tokenHandler returns "old_token" on the first call and "new_token" on subsequent calls.
tokenRetrieved := false
tokenHandler := func(w http.ResponseWriter, r *http.Request) {
token := "new_token"
if !tokenRetrieved {
token = "old_token"
tokenRetrieved = true
}
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": token,
"expires_in": 3600,
})
}

// prospectHandler expects "old_token" for the first request and "new_token" for the next.
firstRequest := true
prospectHandler := func(w http.ResponseWriter, r *http.Request) {
expectedToken := "new_token"
if firstRequest {
expectedToken = "old_token"
firstRequest = false
}
if r.Header.Get("Authorization") != "Bearer "+expectedToken {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}

tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler))
defer tokenSrv.Close()

prospectSrv := httptest.NewServer(http.HandlerFunc(prospectHandler))
defer prospectSrv.Close()

clk := clock.NewFake()
client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, prospectSrv.URL)
test.AssertNotError(t, err, "Failed to create client")

// First call uses the initial token ("old_token").
err = client.CreateProspect("[email protected]")
test.AssertNotError(t, err, "CreateProspect should succeed with the initial token")

// Advance time to force token expiry.
clk.Add(3601 * time.Second)

// Second call should refresh the token to "new_token".
err = client.CreateProspect("[email protected]")
test.AssertNotError(t, err, "CreateProspect should succeed after refreshing the token")
}

func TestCreateProspectServerErrorsAfterMaxAttempts(t *testing.T) {
t.Parallel()

gotAttempts := 0
tokenHandler := func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "dummy",
"expires_in": 3600,
})
}

prospectHandler := func(w http.ResponseWriter, r *http.Request) {
gotAttempts++
w.WriteHeader(http.StatusServiceUnavailable)
}

tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler))
defer tokenSrv.Close()

prospectSrv := httptest.NewServer(http.HandlerFunc(prospectHandler))
defer prospectSrv.Close()

client, _ := NewPardotClientImpl(clock.NewFake(), "biz-unit", "cid", "csec", tokenSrv.URL, prospectSrv.URL)

err := client.CreateProspect("[email protected]")
test.AssertError(t, err, "Should fail after retrying all attempts")
test.AssertEquals(t, maxAttempts, gotAttempts)
test.AssertContains(t, err.Error(), "create prospect request returned status 503")
}

0 comments on commit 81aa930

Please sign in to comment.