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  }