github.com/release-engineering/exodus-rsync@v1.11.2/internal/gw/client_publish_errors_test.go (about)

     1  package gw
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/release-engineering/exodus-rsync/internal/args"
    12  	"github.com/release-engineering/exodus-rsync/internal/log"
    13  )
    14  
    15  type (
    16  	mockReadCloser struct {
    17  		expectedData []byte
    18  		expectedErr  error
    19  	}
    20  )
    21  
    22  func (mrc *mockReadCloser) Read(p []byte) (n int, err error) {
    23  	copy(p, mrc.expectedData)
    24  	return 0, mrc.expectedErr
    25  }
    26  func (mrc *mockReadCloser) Close() error { return nil }
    27  
    28  func TestClientPublishErrors(t *testing.T) {
    29  	cfg := testConfig(t)
    30  
    31  	clientIface, err := Package.NewClient(context.Background(), cfg)
    32  	if clientIface == nil {
    33  		t.Errorf("failed to create client, err = %v", err)
    34  	}
    35  
    36  	ctx := context.Background()
    37  	ctx = log.NewContext(ctx, log.Package.NewLogger(args.Config{}))
    38  
    39  	gw := newFakeGw(t, clientIface.(*client))
    40  
    41  	t.Run("unable to prepare request", func(t *testing.T) {
    42  		// Passing a nil context is used here as a way to make http.NewRequestWithContext fail.
    43  		_, err := clientIface.NewPublish(nil)
    44  
    45  		if err == nil {
    46  			t.Error("Unexpectedly failed to return an error")
    47  		}
    48  	})
    49  
    50  	t.Run("low-level error from request", func(t *testing.T) {
    51  		gw.nextHTTPError = fmt.Errorf("simulated error")
    52  
    53  		_, err := clientIface.NewPublish(ctx)
    54  
    55  		if err == nil {
    56  			t.Error("Unexpectedly failed to return an error")
    57  		}
    58  		if !strings.Contains(err.Error(), "simulated error") {
    59  			t.Errorf("Did not get expected error, got: %v", err)
    60  		}
    61  		if !strings.Contains(err.Error(), "https://exodus-gw.example.com/env/publish") {
    62  			t.Errorf("Error did not include URL, got: %v", err)
    63  		}
    64  	})
    65  
    66  	t.Run("unsuccessful HTTP response", func(t *testing.T) {
    67  		gw.nextHTTPResponse = &http.Response{
    68  			Status:     "418 I'm a teapot",
    69  			StatusCode: 418,
    70  			Body: io.NopCloser(&mockReadCloser{
    71  				expectedData: []byte(""),
    72  				expectedErr:  fmt.Errorf("Failed to read"),
    73  			}),
    74  		}
    75  
    76  		_, err := clientIface.NewPublish(ctx)
    77  
    78  		if err == nil {
    79  			t.Error("Unexpectedly failed to return an error")
    80  		}
    81  		if !strings.Contains(err.Error(), "I'm a teapot") {
    82  			t.Errorf("Did not get expected error, got: %v", err)
    83  		}
    84  		if !strings.Contains(err.Error(), "https://exodus-gw.example.com/env/publish") {
    85  			t.Errorf("Error did not include URL, got: %v", err)
    86  		}
    87  	})
    88  
    89  	t.Run("invalid JSON in response", func(t *testing.T) {
    90  		gw.nextHTTPResponse = &http.Response{
    91  			Status:     "200 OK",
    92  			StatusCode: 200,
    93  			Body:       io.NopCloser(strings.NewReader("['oops', this is not valid JSON")),
    94  		}
    95  
    96  		_, err := clientIface.NewPublish(ctx)
    97  
    98  		if err == nil {
    99  			t.Error("Unexpectedly failed to return an error")
   100  		}
   101  		if !strings.Contains(err.Error(), "invalid character") {
   102  			t.Errorf("Did not get expected error, got: %v", err)
   103  		}
   104  		if !strings.Contains(err.Error(), "https://exodus-gw.example.com/env/publish") {
   105  			t.Errorf("Error did not include URL, got: %v", err)
   106  		}
   107  	})
   108  
   109  	t.Run("get request fails", func(t *testing.T) {
   110  		gw.nextHTTPResponse = &http.Response{
   111  			Status:     "404 Publish not found",
   112  			StatusCode: 404,
   113  			Body:       io.NopCloser(strings.NewReader("'oops', publish not found")),
   114  		}
   115  
   116  		_, err := clientIface.GetPublish(ctx, "3e0a4539-be4a-437e-a45f-6d72f7192f17")
   117  		if err == nil {
   118  			t.Error("Unexpectedly failed to return an error")
   119  		}
   120  	})
   121  
   122  	t.Run("can recover by retrying", func(t *testing.T) {
   123  		// Force an EOF, then a 500 error, and finally a successful
   124  		// response. The caller should only see the success.
   125  		gw.nextHTTPError = io.EOF
   126  		gw.nextHTTPResponse = &http.Response{
   127  			Status:     "500 Internal Server Error",
   128  			StatusCode: 500,
   129  			Body:       io.NopCloser(strings.NewReader("some error")),
   130  		}
   131  
   132  		gw.publishes["some-id"] = &fakePublish{id: "some-id"}
   133  
   134  		p, err := clientIface.GetPublish(ctx, "some-id")
   135  		if err != nil {
   136  			t.Errorf("failed to get publish, err = %v", err)
   137  		}
   138  		id := p.ID()
   139  		if id != "some-id" {
   140  			t.Errorf("got unexpected id %s", id)
   141  		}
   142  	})
   143  
   144  	t.Run("missing link for commit", func(t *testing.T) {
   145  		// Create a publish object directly without filling in any Links.
   146  		publish := publish{client: clientIface.(*client)}
   147  
   148  		err := publish.Commit(ctx, "")
   149  
   150  		if err == nil {
   151  			t.Error("Unexpectedly failed to return an error")
   152  		}
   153  		if !strings.Contains(err.Error(), "not eligible for commit") {
   154  			t.Errorf("Did not get expected error, got: %v", err)
   155  		}
   156  	})
   157  
   158  	t.Run("missing link for update", func(t *testing.T) {
   159  		publish := publish{client: clientIface.(*client)}
   160  
   161  		err := publish.AddItems(ctx, []ItemInput{})
   162  
   163  		if err == nil {
   164  			t.Error("Unexpectedly failed to return an error")
   165  		}
   166  		if !strings.Contains(err.Error(), "missing 'self'") {
   167  			t.Errorf("Did not get expected error, got: %v", err)
   168  		}
   169  	})
   170  
   171  	t.Run("HTTP error during AddItems", func(t *testing.T) {
   172  		publish := publish{client: clientIface.(*client)}
   173  		publish.raw.Links = make(map[string]string)
   174  		publish.raw.Links["self"] = "/publish/1234"
   175  
   176  		gw.nextHTTPResponse = &http.Response{
   177  			Status:     "409 Conflict",
   178  			StatusCode: 409,
   179  			Body: io.NopCloser(strings.NewReader(
   180  				"{\"detail\": \"Publish in unexpected state\"}",
   181  			)),
   182  		}
   183  
   184  		err := publish.AddItems(ctx, []ItemInput{{"/some/uri", "abc123", "mime/type", ""}})
   185  
   186  		if err == nil {
   187  			t.Error("Unexpectedly failed to return an error")
   188  		}
   189  		if !strings.Contains(err.Error(), "Publish in unexpected state") {
   190  			t.Errorf("Did not get expected error, got: %v", err)
   191  		}
   192  	})
   193  
   194  	t.Run("commit request fails", func(t *testing.T) {
   195  		publish := publish{client: clientIface.(*client)}
   196  
   197  		// Putting an incorrect 'commit' URL is enough to make the GW fake
   198  		// return a 404 error.
   199  		publish.raw.Links = make(map[string]string)
   200  		publish.raw.Links["commit"] = "/some/invalid/url"
   201  
   202  		err := publish.Commit(ctx, "")
   203  
   204  		if err == nil {
   205  			t.Error("Unexpectedly failed to return an error")
   206  		}
   207  		if !strings.Contains(err.Error(), "404 Not Found") {
   208  			t.Errorf("Did not get expected error, got: %v", err)
   209  		}
   210  	})
   211  
   212  }