github.com/letsencrypt/boulder@v0.20251208.0/sfe/zendesk/zendesk_test.go (about) 1 package zendesk 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/http/httptest" 7 "net/url" 8 "strings" 9 "sync/atomic" 10 "testing" 11 12 "github.com/letsencrypt/boulder/test/zendeskfake" 13 ) 14 15 const ( 16 apiTokenEmail = "tester@example.com" 17 apiToken = "someToken" 18 ) 19 20 func startMockClient(t *testing.T) (*Client, *zendeskfake.Server) { 21 t.Helper() 22 23 st := zendeskfake.NewStore(0) 24 srv := zendeskfake.NewServer(apiTokenEmail, apiToken, st) 25 ts := httptest.NewServer(srv.Handler()) 26 t.Cleanup(ts.Close) 27 28 nameToID := map[string]int64{ 29 "reviewStatus": 111, 30 "organization": 222, 31 "kind": 333, 32 } 33 34 c, err := NewClient(ts.URL, apiTokenEmail, apiToken, nameToID) 35 if err != nil { 36 t.Errorf("NewClient(%q) returned error: %s", ts.URL, err) 37 } 38 39 return c, srv 40 } 41 42 func TestNewClientWithDuplicateFieldID(t *testing.T) { 43 t.Parallel() 44 45 ts := httptest.NewServer(http.NewServeMux()) 46 defer ts.Close() 47 nameToID := map[string]int64{ 48 "a": 1, 49 "b": 1, 50 } 51 _, err := NewClient(ts.URL, apiTokenEmail, apiToken, nameToID) 52 if err == nil || !strings.Contains(err.Error(), "duplicate field ID") { 53 t.Errorf("expected duplicate field ID error, got: %s", err) 54 } 55 } 56 57 func TestNewClientBaseURLJoin(t *testing.T) { 58 t.Parallel() 59 60 base := "http://example.test" 61 _, err := NewClient(base+"/", apiTokenEmail, apiToken, map[string]int64{}) 62 if err != nil { 63 t.Errorf("NewClient with trailing slash failed: %s", err) 64 } 65 _, err = NewClient(base, apiTokenEmail, apiToken, map[string]int64{}) 66 if err != nil { 67 t.Errorf("NewClient without trailing slash failed: %s", err) 68 } 69 } 70 71 func TestCreateTicketOK(t *testing.T) { 72 t.Parallel() 73 74 c, srv := startMockClient(t) 75 76 id, err := c.CreateTicket("alice@example.com", "Subject", "Body text", map[string]string{ 77 "reviewStatus": "pending", 78 "organization": "Acme", 79 }) 80 if err != nil { 81 t.Errorf("CreateTicket(alice@example.com, Subject) error: %s", err) 82 } 83 if id == 0 { 84 t.Errorf("CreateTicket returned id=0; want non-zero") 85 } 86 87 got, ok := srv.GetTicket(id) 88 if !ok { 89 t.Errorf("ticket id %d not stored in mock server", id) 90 } 91 if got.Subject != "Subject" { 92 t.Errorf("subject mismatch: got %q, want %q", got.Subject, "Subject") 93 } 94 if len(got.Comments) != 1 || got.Comments[0].Body != "Body text" || !got.Comments[0].Public { 95 t.Errorf("comments stored incorrectly: %#v (want one public comment with body %q)", got.Comments, "Body text") 96 } 97 if got.CustomFields[111] != "pending" || got.CustomFields[222] != "Acme" { 98 t.Errorf("custom fields mismatch: %#v (want 111=%q, 222=%q)", got.CustomFields, "pending", "Acme") 99 } 100 } 101 102 func TestCreateTicketHTTPError(t *testing.T) { 103 t.Parallel() 104 105 mux := http.NewServeMux() 106 mux.HandleFunc("/api/v2/tickets.json", func(w http.ResponseWriter, r *http.Request) { 107 http.Error(w, "boom", http.StatusInternalServerError) 108 }) 109 ts := httptest.NewServer(mux) 110 defer ts.Close() 111 112 c, err := NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{}) 113 if err != nil { 114 t.Errorf("NewClient(%q): %s", ts.URL, err) 115 } 116 117 _, err = c.CreateTicket("bob@example.com", "cause500", "x", nil) 118 if err == nil || !strings.Contains(err.Error(), "status 500") { 119 t.Errorf("expected HTTP 500 error creating ticket, got: %s", err) 120 } 121 } 122 123 func TestCreateTicketUnknownField(t *testing.T) { 124 t.Parallel() 125 126 c, _ := startMockClient(t) 127 128 _, err := c.CreateTicket("x@example.com", "s", "b", map[string]string{"nope": "v"}) 129 if err == nil || !strings.Contains(err.Error(), "unknown custom field") { 130 t.Errorf("expected unknown custom field error, got: %s", err) 131 } 132 } 133 134 func TestCreateTicketSetsRequesterNameToEmail(t *testing.T) { 135 t.Parallel() 136 137 c, srv := startMockClient(t) 138 139 id, err := c.CreateTicket("alice@example.com", "S", "B", nil) 140 if err != nil { 141 t.Errorf("CreateTicket(alice@example.com): %s", err) 142 } 143 144 got, ok := srv.GetTicket(id) 145 if !ok { 146 t.Errorf("ticket id %d not found in server", id) 147 return 148 } 149 if got.Requester.Email != "alice@example.com" || got.Requester.Name != "alice@example.com" { 150 t.Errorf("requester mismatch for ticket %d: %#v (want Email=%q Name=%q)", id, got.Requester, "alice@example.com", "alice@example.com") 151 } 152 } 153 154 func TestAddCommentOK(t *testing.T) { 155 t.Parallel() 156 157 c, srv := startMockClient(t) 158 159 id, err := c.CreateTicket("a@example.com", "s", "first", nil) 160 if err != nil { 161 t.Errorf("CreateTicket(a@example.com): %s", err) 162 } 163 164 err = c.AddComment(id, "second-private", false) 165 if err != nil { 166 t.Errorf("AddComment(id=%d): %s", id, err) 167 } 168 169 got, ok := srv.GetTicket(id) 170 if !ok { 171 t.Errorf("ticket id %d not stored after AddComment", id) 172 } 173 if len(got.Comments) != 2 { 174 t.Errorf("want 2 comments after AddComment, got %d: %#v", len(got.Comments), got.Comments) 175 } 176 if got.Comments[1].Body != "second-private" || got.Comments[1].Public { 177 t.Errorf("second comment incorrect: %#v (want body=%q, public=false)", got.Comments[1], "second-private") 178 } 179 } 180 181 func TestAddComment404(t *testing.T) { 182 t.Parallel() 183 184 c, _ := startMockClient(t) 185 186 err := c.AddComment(99999, "x", true) 187 if err == nil || !strings.Contains(err.Error(), "status 404") { 188 t.Errorf("expected HTTP 404 when adding comment to unknown ticket, got: %s", err) 189 } 190 } 191 192 func TestAddCommentEmptyBody422(t *testing.T) { 193 t.Parallel() 194 195 c, _ := startMockClient(t) 196 197 id, err := c.CreateTicket("a@example.com", "s", "init", nil) 198 if err != nil { 199 t.Errorf("CreateTicket(a@example.com): %s", err) 200 } 201 202 err = c.AddComment(id, "", true) 203 if err == nil || !strings.Contains(err.Error(), "status 422") { 204 t.Errorf("expected HTTP 422 for empty comment body on ticket %d, got: %s", id, err) 205 } 206 } 207 208 func TestUpdateTicketStatus(t *testing.T) { 209 t.Parallel() 210 211 type tc struct { 212 name string 213 status string 214 comment *comment 215 expectErr bool 216 expectStatus string 217 expectComment *comment 218 } 219 220 cases := []tc{ 221 { 222 name: "Update to open without comment", 223 status: "open", 224 expectErr: false, 225 expectStatus: "open", 226 }, 227 { 228 name: "Update to pending with comment", 229 status: "solved", 230 comment: &comment{Body: "Resolved", Public: true}, 231 expectErr: false, 232 expectStatus: "solved", 233 expectComment: &comment{Body: "Resolved", Public: true}, 234 }, 235 { 236 name: "Update from new to foo (invalid status)", 237 status: "foo", 238 expectErr: true, 239 expectStatus: "new", 240 }, 241 { 242 name: "unknown id", 243 status: "open", 244 expectErr: true, 245 expectStatus: "new", 246 }, 247 } 248 249 for _, tc := range cases { 250 t.Run(tc.name, func(t *testing.T) { 251 t.Parallel() 252 253 fake := zendeskfake.NewServer(apiTokenEmail, apiToken, nil) 254 ts := httptest.NewServer(fake.Handler()) 255 t.Cleanup(ts.Close) 256 257 client, err := NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{}) 258 if err != nil { 259 t.Errorf("Unexpected error from NewClient(%q): %s", ts.URL, err) 260 } 261 262 client.updateURL, err = url.JoinPath(ts.URL, "/api/v2/tickets") 263 if err != nil { 264 t.Errorf("Failed to join update URL: %s", err) 265 } 266 267 id, err := client.CreateTicket("foo@bar.co", "Some subject", "Some comment", nil) 268 if err != nil { 269 t.Errorf("Unexpected error from CreateTicket: %s", err) 270 } 271 272 updateID := id 273 if tc.name == "unknown id" { 274 updateID = 999999 275 } 276 277 var commentBody string 278 var public bool 279 if tc.comment != nil { 280 commentBody = tc.comment.Body 281 public = tc.comment.Public 282 } 283 err = client.UpdateTicketStatus(updateID, tc.status, commentBody, public) 284 if tc.expectErr { 285 if err == nil { 286 t.Errorf("Expected error for status %q, got nil", tc.status) 287 } 288 } else { 289 if err != nil { 290 t.Errorf("Unexpected error for UpdateTicketStatus(%d, %q): %s", updateID, tc.status, err) 291 } 292 } 293 294 got, ok := fake.GetTicket(id) 295 if !ok { 296 t.Errorf("Ticket with id %d not found after update", id) 297 } 298 299 if got.Status != tc.expectStatus { 300 t.Errorf("Expected status %q, got %q", tc.expectStatus, got.Status) 301 } 302 if tc.expectComment != nil { 303 found := false 304 for _, c := range got.Comments { 305 if c.Body == tc.expectComment.Body && c.Public == tc.expectComment.Public { 306 found = true 307 break 308 } 309 } 310 if !found { 311 t.Errorf("Expected comment not found: %#v in %#v", tc.expectComment, got.Comments) 312 } 313 } else if len(got.Comments) > 1 { 314 t.Errorf("Expected no additional comment, got %d: %#v", len(got.Comments), got.Comments) 315 } 316 }) 317 } 318 } 319 320 func TestFindTicketsSimple(t *testing.T) { 321 t.Parallel() 322 323 c, _ := startMockClient(t) 324 325 _, err := c.CreateTicket("u1@example.com", "s1", "b", map[string]string{"reviewStatus": "pending", "organization": "Acme"}) 326 if err != nil { 327 t.Errorf("creating ticket 1: %s", err) 328 } 329 _, err = c.CreateTicket("u2@example.com", "s2", "b", map[string]string{"reviewStatus": "approved", "organization": "Acme"}) 330 if err != nil { 331 t.Errorf("creating ticket 2: %s", err) 332 } 333 id3, err := c.CreateTicket("u3@example.com", "s3", "b", map[string]string{"reviewStatus": "pending", "organization": "Beta"}) 334 if err != nil { 335 t.Errorf("creating ticket 3: %s", err) 336 } 337 338 got, err := c.FindTickets(map[string]string{"reviewStatus": "pending"}, "new") 339 if err != nil { 340 t.Errorf("FindTickets(reviewStatus=pending): %s", err) 341 } 342 if len(got) != 2 { 343 t.Errorf("expected 2 results for reviewStatus=pending, got %d: %#v", len(got), got) 344 } 345 fields, ok := got[id3] 346 if ok { 347 if fields["reviewStatus"] != "pending" || fields["organization"] != "Beta" { 348 t.Errorf("field name/value mapping wrong for ticket %d: %#v (want reviewStatus=%q, organization=%q)", id3, fields, "pending", "Beta") 349 } 350 } 351 } 352 353 func TestFindTicketsQuotedValueReturnsAll(t *testing.T) { 354 t.Parallel() 355 356 c, _ := startMockClient(t) 357 358 for i := range 5 { 359 _, err := c.CreateTicket("x@example.com", fmt.Sprintf("s%d", i), "b", 360 map[string]string{"reviewStatus": "needs review"}) 361 if err != nil { 362 t.Errorf("create ticket %d: %s", i, err) 363 } 364 } 365 366 got, err := c.FindTickets(map[string]string{"reviewStatus": "needs review"}, "new") 367 if err != nil { 368 t.Errorf("FindTickets(needs review): %s", err) 369 } 370 if len(got) != 5 { 371 t.Errorf("expected 5 results for quoted value search, got %d: %#v", len(got), got) 372 } 373 } 374 375 func TestFindTicketsNoMatchFieldsError(t *testing.T) { 376 t.Parallel() 377 378 c, _ := startMockClient(t) 379 380 _, err := c.FindTickets(map[string]string{}, "new") 381 if err == nil || !strings.Contains(err.Error(), "no match fields") { 382 t.Errorf("expected error for empty match fields, got: %s", err) 383 } 384 } 385 386 func TestFindTicketsUnknownFieldName(t *testing.T) { 387 t.Parallel() 388 389 c, _ := startMockClient(t) 390 391 _, err := c.FindTickets(map[string]string{"unknown": "v"}, "new") 392 if err == nil || !strings.Contains(err.Error(), "unknown custom field") { 393 t.Errorf("expected unknown custom field error, got: %s", err) 394 } 395 } 396 397 func TestFindTicketsNoResults(t *testing.T) { 398 t.Parallel() 399 400 c, _ := startMockClient(t) 401 402 _, err := c.CreateTicket("u@example.com", "s", "b", map[string]string{"reviewStatus": "approved"}) 403 if err != nil { 404 t.Errorf("creating ticket with reviewStatus=approved: %s", err) 405 } 406 got, err := c.FindTickets(map[string]string{"reviewStatus": "pending"}, "new") 407 if err != nil { 408 t.Errorf("FindTickets(reviewStatus=pending): %s", err) 409 } 410 if len(got) != 0 { 411 t.Errorf("expected 0 results, got %d: %#v", len(got), got) 412 } 413 } 414 415 func TestFindTicketsPaginationFollowed(t *testing.T) { 416 t.Parallel() 417 418 store := zendeskfake.NewStore(0) 419 fake := zendeskfake.NewServer(apiTokenEmail, apiToken, store) 420 421 inner := fake.Handler() 422 var searchHits int32 423 424 mux := http.NewServeMux() 425 mux.HandleFunc(zendeskfake.SearchJSONPath, func(w http.ResponseWriter, r *http.Request) { 426 atomic.AddInt32(&searchHits, 1) 427 inner.ServeHTTP(w, r) 428 }) 429 mux.Handle(zendeskfake.TicketsJSONPath, inner) 430 mux.Handle(zendeskfake.TicketsPath, inner) 431 432 ts := httptest.NewServer(mux) 433 defer ts.Close() 434 435 c, err := NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{"reviewStatus": 111}) 436 if err != nil { 437 t.Errorf("NewClient(%q): %s", ts.URL, err) 438 } 439 440 for i := range 5 { 441 if _, err := c.CreateTicket( 442 fmt.Sprintf("u%d@example.com", i), 443 fmt.Sprintf("s%d", i), 444 "body", 445 map[string]string{"reviewStatus": "needs review"}, 446 ); err != nil { 447 t.Errorf("create ticket %d: %s", i, err) 448 } 449 } 450 451 got, err := c.FindTickets(map[string]string{"reviewStatus": "needs review"}, "") 452 if err != nil { 453 t.Errorf("FindTickets(needs review): %s", err) 454 } 455 if len(got) != 5 { 456 t.Errorf("expected 5 merged results from paginated search, got %d: %#v", len(got), got) 457 } 458 459 if atomic.LoadInt32(&searchHits) < 3 { 460 t.Errorf("expected >= 3 /search.json requests due to pagination, got %d", searchHits) 461 } 462 } 463 464 func TestFindTicketsHTTP400(t *testing.T) { 465 t.Parallel() 466 467 mux := http.NewServeMux() 468 mux.HandleFunc("/api/v2/search.json", func(w http.ResponseWriter, r *http.Request) { 469 http.Error(w, "bad query", http.StatusBadRequest) 470 }) 471 ts := httptest.NewServer(mux) 472 defer ts.Close() 473 474 c, err := NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{"reviewStatus": 111}) 475 if err != nil { 476 t.Errorf("NewClient(%q): %s", ts.URL, err) 477 } 478 _, err = c.FindTickets(map[string]string{"reviewStatus": "needs review"}, "new") 479 if err == nil || !strings.Contains(err.Error(), "status 400") { 480 t.Errorf("expected HTTP 400 from search, got: %s", err) 481 } 482 } 483 484 func TestFindTicketsHTTP500(t *testing.T) { 485 t.Parallel() 486 487 mux := http.NewServeMux() 488 mux.HandleFunc("/api/v2/search.json", func(w http.ResponseWriter, r *http.Request) { 489 http.Error(w, "boom", http.StatusInternalServerError) 490 }) 491 ts := httptest.NewServer(mux) 492 defer ts.Close() 493 494 c, err := NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{"reviewStatus": 111}) 495 if err != nil { 496 t.Errorf("NewClient(%q): %s", ts.URL, err) 497 } 498 _, err = c.FindTickets(map[string]string{"reviewStatus": "needs review"}, "new") 499 if err == nil || !strings.Contains(err.Error(), "status 500") { 500 t.Errorf("expected HTTP 500 from search, got: %s", err) 501 } 502 }