github.com/letsencrypt/boulder@v0.20251208.0/test/integration/email_exporter_test.go (about) 1 //go:build integration 2 3 package integration 4 5 import ( 6 "bytes" 7 "crypto/ecdsa" 8 "crypto/elliptic" 9 "crypto/rand" 10 "encoding/json" 11 "fmt" 12 "net/http" 13 "net/url" 14 "os" 15 "slices" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/eggsampler/acme/v3" 21 22 "github.com/letsencrypt/boulder/test" 23 ) 24 25 // randomDomain creates a random domain name for testing. 26 func randomDomain(t *testing.T) string { 27 t.Helper() 28 29 var bytes [4]byte 30 _, err := rand.Read(bytes[:]) 31 if err != nil { 32 test.AssertNotError(t, err, "Failed to generate random domain") 33 } 34 return fmt.Sprintf("%x.mail.com", bytes[:]) 35 } 36 37 // getOAuthToken queries the pardot-test-srv for the current OAuth token. 38 func getOAuthToken(t *testing.T) string { 39 t.Helper() 40 41 data, err := os.ReadFile("test/secrets/salesforce_client_id") 42 test.AssertNotError(t, err, "Failed to read Salesforce client ID") 43 clientId := string(data) 44 45 data, err = os.ReadFile("test/secrets/salesforce_client_secret") 46 test.AssertNotError(t, err, "Failed to read Salesforce client secret") 47 clientSecret := string(data) 48 49 httpClient := http.DefaultClient 50 resp, err := httpClient.PostForm("http://localhost:9601/services/oauth2/token", url.Values{ 51 "grant_type": {"client_credentials"}, 52 "client_id": {strings.TrimSpace(clientId)}, 53 "client_secret": {strings.TrimSpace(clientSecret)}, 54 }) 55 test.AssertNotError(t, err, "Failed to fetch OAuth token") 56 test.AssertEquals(t, resp.StatusCode, http.StatusOK) 57 defer resp.Body.Close() 58 59 var response struct { 60 AccessToken string `json:"access_token"` 61 } 62 decoder := json.NewDecoder(resp.Body) 63 err = decoder.Decode(&response) 64 test.AssertNotError(t, err, "Failed to decode OAuth token") 65 return response.AccessToken 66 } 67 68 // getCreatedContacts queries the pardot-test-srv for the list of created 69 // contacts. 70 func getCreatedContacts(t *testing.T, token string) []string { 71 t.Helper() 72 73 httpClient := http.DefaultClient 74 req, err := http.NewRequest("GET", "http://localhost:9602/contacts", nil) 75 test.AssertNotError(t, err, "Failed to create request") 76 req.Header.Set("Authorization", "Bearer "+token) 77 78 resp, err := httpClient.Do(req) 79 test.AssertNotError(t, err, "Failed to query contacts") 80 test.AssertEquals(t, resp.StatusCode, http.StatusOK) 81 defer resp.Body.Close() 82 83 var got struct { 84 Contacts []string `json:"contacts"` 85 } 86 decoder := json.NewDecoder(resp.Body) 87 err = decoder.Decode(&got) 88 test.AssertNotError(t, err, "Failed to decode contacts") 89 return got.Contacts 90 } 91 92 // assertAllContactsReceived waits for the expected contacts to be received by 93 // pardot-test-srv. Retries every 50ms for up to 2 seconds and fails if the 94 // expected contacts are not received. 95 func assertAllContactsReceived(t *testing.T, token string, expect []string) { 96 t.Helper() 97 98 for attempt := range 20 { 99 if attempt > 0 { 100 time.Sleep(50 * time.Millisecond) 101 } 102 got := getCreatedContacts(t, token) 103 104 allFound := true 105 for _, e := range expect { 106 if !slices.Contains(got, e) { 107 allFound = false 108 break 109 } 110 } 111 if allFound { 112 break 113 } 114 if attempt >= 19 { 115 t.Fatalf("Expected contacts=%v to be received by pardot-test-srv, got contacts=%v", expect, got) 116 } 117 } 118 } 119 120 // TestContactsSentForNewAccount tests that contacts are dispatched to 121 // pardot-test-srv by the email-exporter when a new account is created. 122 func TestContactsSentForNewAccount(t *testing.T) { 123 t.Parallel() 124 125 if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { 126 t.Skip("Test requires WFE to be configured to use email-exporter") 127 } 128 129 token := getOAuthToken(t) 130 domain := randomDomain(t) 131 132 tests := []struct { 133 name string 134 contacts []string 135 expectContacts []string 136 }{ 137 { 138 name: "Single email", 139 contacts: []string{"mailto:example@" + domain}, 140 expectContacts: []string{"example@" + domain}, 141 }, 142 { 143 name: "Multiple emails", 144 contacts: []string{"mailto:example1@" + domain, "mailto:example2@" + domain}, 145 expectContacts: []string{"example1@" + domain, "example2@" + domain}, 146 }, 147 } 148 149 for _, tt := range tests { 150 t.Run(tt.name, func(t *testing.T) { 151 t.Parallel() 152 153 c, err := acme.NewClient("http://boulder.service.consul:4001/directory") 154 if err != nil { 155 t.Fatalf("failed to connect to acme directory: %s", err) 156 } 157 158 acctKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 159 if err != nil { 160 t.Fatalf("failed to generate account key: %s", err) 161 } 162 163 _, err = c.NewAccount(acctKey, false, true, tt.contacts...) 164 test.AssertNotError(t, err, "Failed to create initial account with contacts") 165 assertAllContactsReceived(t, token, tt.expectContacts) 166 }) 167 } 168 } 169 170 // getCreatedCases queries the pardot-test-srv for the list of created cases. 171 // Fails the test on error. 172 func getCreatedCases(t *testing.T, token string) []map[string]any { 173 t.Helper() 174 175 req, err := http.NewRequest("GET", "http://localhost:9601/cases", nil) 176 test.AssertNotError(t, err, "Failed to create cases request") 177 req.Header.Set("Authorization", "Bearer "+token) 178 179 resp, err := http.DefaultClient.Do(req) 180 test.AssertNotError(t, err, "Failed to query cases") 181 test.AssertEquals(t, resp.StatusCode, http.StatusOK) 182 defer resp.Body.Close() 183 184 var got struct { 185 Cases []map[string]any `json:"cases"` 186 } 187 err = json.NewDecoder(resp.Body).Decode(&got) 188 test.AssertNotError(t, err, "Failed to decode cases") 189 return got.Cases 190 } 191 192 // createCase sends a request to create a new case via pardot-test-srv and 193 // returns the HTTP status code and response body. Fails the test on error. 194 func createCase(t *testing.T, token string, payload map[string]any) (int, []byte) { 195 t.Helper() 196 197 b, err := json.Marshal(payload) 198 test.AssertNotError(t, err, "Failed to marshal case payload") 199 200 req, err := http.NewRequest( 201 "POST", 202 "http://localhost:9601/services/data/v64.0/sobjects/Case", 203 bytes.NewReader(b), 204 ) 205 test.AssertNotError(t, err, "Failed to create case POST request") 206 req.Header.Set("Authorization", "Bearer "+token) 207 req.Header.Set("Content-Type", "application/json") 208 209 resp, err := http.DefaultClient.Do(req) 210 test.AssertNotError(t, err, "Failed to POST case") 211 defer resp.Body.Close() 212 213 var body bytes.Buffer 214 _, err = body.ReadFrom(resp.Body) 215 test.AssertNotError(t, err, "Failed to read case response body") 216 217 return resp.StatusCode, body.Bytes() 218 } 219 220 func TestCasesAPISuccess(t *testing.T) { 221 t.Parallel() 222 223 token := getOAuthToken(t) 224 225 status, _ := createCase(t, token, map[string]any{ 226 "Subject": "Integration Test Case", 227 "Description": "Created by integration test", 228 "Origin": "Web", 229 }) 230 test.AssertEquals(t, status, http.StatusCreated) 231 232 // Verify it was recorded by the fake server. 233 cases := getCreatedCases(t, token) 234 found := false 235 for _, c := range cases { 236 if c["Subject"] == "Integration Test Case" && c["Origin"] == "Web" { 237 found = true 238 break 239 } 240 } 241 if !found { 242 t.Fatalf("Expected created case to be present; got cases=%s", cases) 243 } 244 } 245 246 func TestCasesAPIMissingOrigin(t *testing.T) { 247 t.Parallel() 248 249 token := getOAuthToken(t) 250 251 // Missing Origin should be rejected by the fake server. 252 status, body := createCase(t, token, map[string]any{ 253 "Subject": "Missing Origin Case", 254 "Description": "Should fail", 255 }) 256 test.AssertEquals(t, status, http.StatusBadRequest) 257 test.AssertContains(t, string(body), "Missing required field: Origin") 258 } 259 260 func TestCasesAPIUsingSFE(t *testing.T) { 261 t.Parallel() 262 263 if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { 264 t.Skip("Test requires SFE to be configured to use email-exporter") 265 } 266 267 token := getOAuthToken(t) 268 269 body, err := json.Marshal(map[string]any{ 270 "rateLimit": "NewOrdersPerAccount", 271 "fields": map[string]string{ 272 "subscriberAgreement": "true", 273 "privacyPolicy": "true", 274 "mailingList": "false", 275 "fundraising": "Yes, email me more information.", 276 "emailAddress": "test@foo.bar", 277 "organization": "Big Host Inc.", 278 "useCase": strings.Repeat("x", 60), 279 "tier": "1000", 280 "accountURI": "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 281 }, 282 }) 283 test.AssertNotError(t, err, "marshal override payload") 284 285 req, err := http.NewRequest(http.MethodPost, "http://localhost:4003/sfe/v1/overrides/submit-override-request", bytes.NewReader(body)) 286 test.AssertNotError(t, err, "creating override request") 287 req.Header.Set("Content-Type", "application/json") 288 289 resp, err := http.DefaultClient.Do(req) 290 test.AssertNotError(t, err, "POSTing override request to SFE") 291 defer resp.Body.Close() 292 293 if resp.StatusCode != http.StatusCreated { 294 var buf bytes.Buffer 295 _, err = buf.ReadFrom(resp.Body) 296 test.AssertNotError(t, err, "reading SFE response body") 297 t.Errorf("unexpected SFE status=%d with body=%s", resp.StatusCode, buf.String()) 298 } 299 300 timeout := 3 * time.Second 301 interval := 10 * time.Millisecond 302 303 ticker := time.NewTicker(interval) 304 defer ticker.Stop() 305 306 timer := time.NewTimer(timeout) 307 defer timer.Stop() 308 309 for { 310 select { 311 case <-ticker.C: 312 cases := getCreatedCases(t, token) 313 for _, c := range cases { 314 if c["Subject"] == "NewOrdersPerAccount rate limit override request for Big Host Inc." && 315 c["Origin"] == "Web" && 316 c["ContactEmail"] == "test@foo.bar" && 317 c["Organization__c"] == "Big Host Inc." && 318 c["Rate_Limit_Name__c"] == "NewOrdersPerAccount" && 319 c["Rate_Limit_Tier__c"] == "1000" { 320 return 321 } 322 } 323 case <-timer.C: 324 t.Fatalf("expected Case never created within %s", timeout) 325 } 326 } 327 }