github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/lfs/http_client_test.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package lfs
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"io"
    12  	"net/http"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/gitbundle/modules/json"
    17  
    18  	"github.com/stretchr/testify/assert"
    19  )
    20  
    21  type RoundTripFunc func(req *http.Request) *http.Response
    22  
    23  func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
    24  	return f(req), nil
    25  }
    26  
    27  type DummyTransferAdapter struct{}
    28  
    29  func (a *DummyTransferAdapter) Name() string {
    30  	return "dummy"
    31  }
    32  
    33  func (a *DummyTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCloser, error) {
    34  	return io.NopCloser(bytes.NewBufferString("dummy")), nil
    35  }
    36  
    37  func (a *DummyTransferAdapter) Upload(ctx context.Context, l *Link, p Pointer, r io.Reader) error {
    38  	return nil
    39  }
    40  
    41  func (a *DummyTransferAdapter) Verify(ctx context.Context, l *Link, p Pointer) error {
    42  	return nil
    43  }
    44  
    45  func lfsTestRoundtripHandler(req *http.Request) *http.Response {
    46  	var batchResponse *BatchResponse
    47  	url := req.URL.String()
    48  
    49  	if strings.Contains(url, "status-not-ok") {
    50  		return &http.Response{StatusCode: http.StatusBadRequest}
    51  	} else if strings.Contains(url, "invalid-json-response") {
    52  		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString("invalid json"))}
    53  	} else if strings.Contains(url, "valid-batch-request-download") {
    54  		batchResponse = &BatchResponse{
    55  			Transfer: "dummy",
    56  			Objects: []*ObjectResponse{
    57  				{
    58  					Actions: map[string]*Link{
    59  						"download": {},
    60  					},
    61  				},
    62  			},
    63  		}
    64  	} else if strings.Contains(url, "valid-batch-request-upload") {
    65  		batchResponse = &BatchResponse{
    66  			Transfer: "dummy",
    67  			Objects: []*ObjectResponse{
    68  				{
    69  					Actions: map[string]*Link{
    70  						"upload": {},
    71  					},
    72  				},
    73  			},
    74  		}
    75  	} else if strings.Contains(url, "response-no-objects") {
    76  		batchResponse = &BatchResponse{Transfer: "dummy"}
    77  	} else if strings.Contains(url, "unknown-transfer-adapter") {
    78  		batchResponse = &BatchResponse{Transfer: "unknown_adapter"}
    79  	} else if strings.Contains(url, "error-in-response-objects") {
    80  		batchResponse = &BatchResponse{
    81  			Transfer: "dummy",
    82  			Objects: []*ObjectResponse{
    83  				{
    84  					Error: &ObjectError{
    85  						Code:    http.StatusNotFound,
    86  						Message: "Object not found",
    87  					},
    88  				},
    89  			},
    90  		}
    91  	} else if strings.Contains(url, "empty-actions-map") {
    92  		batchResponse = &BatchResponse{
    93  			Transfer: "dummy",
    94  			Objects: []*ObjectResponse{
    95  				{
    96  					Actions: map[string]*Link{},
    97  				},
    98  			},
    99  		}
   100  	} else if strings.Contains(url, "download-actions-map") {
   101  		batchResponse = &BatchResponse{
   102  			Transfer: "dummy",
   103  			Objects: []*ObjectResponse{
   104  				{
   105  					Actions: map[string]*Link{
   106  						"download": {},
   107  					},
   108  				},
   109  			},
   110  		}
   111  	} else if strings.Contains(url, "upload-actions-map") {
   112  		batchResponse = &BatchResponse{
   113  			Transfer: "dummy",
   114  			Objects: []*ObjectResponse{
   115  				{
   116  					Actions: map[string]*Link{
   117  						"upload": {},
   118  					},
   119  				},
   120  			},
   121  		}
   122  	} else if strings.Contains(url, "verify-actions-map") {
   123  		batchResponse = &BatchResponse{
   124  			Transfer: "dummy",
   125  			Objects: []*ObjectResponse{
   126  				{
   127  					Actions: map[string]*Link{
   128  						"verify": {},
   129  					},
   130  				},
   131  			},
   132  		}
   133  	} else if strings.Contains(url, "unknown-actions-map") {
   134  		batchResponse = &BatchResponse{
   135  			Transfer: "dummy",
   136  			Objects: []*ObjectResponse{
   137  				{
   138  					Actions: map[string]*Link{
   139  						"unknown": {},
   140  					},
   141  				},
   142  			},
   143  		}
   144  	} else {
   145  		return nil
   146  	}
   147  
   148  	payload := new(bytes.Buffer)
   149  	json.NewEncoder(payload).Encode(batchResponse)
   150  
   151  	return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(payload)}
   152  }
   153  
   154  func TestHTTPClientDownload(t *testing.T) {
   155  	p := Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041", Size: 6}
   156  
   157  	hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
   158  		assert.Equal(t, "POST", req.Method)
   159  		assert.Equal(t, MediaType, req.Header.Get("Content-type"))
   160  		assert.Equal(t, MediaType, req.Header.Get("Accept"))
   161  
   162  		var batchRequest BatchRequest
   163  		err := json.NewDecoder(req.Body).Decode(&batchRequest)
   164  		assert.NoError(t, err)
   165  
   166  		assert.Equal(t, "download", batchRequest.Operation)
   167  		assert.Equal(t, 1, len(batchRequest.Objects))
   168  		assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
   169  		assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
   170  
   171  		return lfsTestRoundtripHandler(req)
   172  	})}
   173  	dummy := &DummyTransferAdapter{}
   174  
   175  	cases := []struct {
   176  		endpoint      string
   177  		expectederror string
   178  	}{
   179  		// case 0
   180  		{
   181  			endpoint:      "https://status-not-ok.io",
   182  			expectederror: "Unexpected server response: ",
   183  		},
   184  		// case 1
   185  		{
   186  			endpoint:      "https://invalid-json-response.io",
   187  			expectederror: "invalid json",
   188  		},
   189  		// case 2
   190  		{
   191  			endpoint:      "https://valid-batch-request-download.io",
   192  			expectederror: "",
   193  		},
   194  		// case 3
   195  		{
   196  			endpoint:      "https://response-no-objects.io",
   197  			expectederror: "",
   198  		},
   199  		// case 4
   200  		{
   201  			endpoint:      "https://unknown-transfer-adapter.io",
   202  			expectederror: "TransferAdapter not found: ",
   203  		},
   204  		// case 5
   205  		{
   206  			endpoint:      "https://error-in-response-objects.io",
   207  			expectederror: "Object not found",
   208  		},
   209  		// case 6
   210  		{
   211  			endpoint:      "https://empty-actions-map.io",
   212  			expectederror: "Missing action 'download'",
   213  		},
   214  		// case 7
   215  		{
   216  			endpoint:      "https://download-actions-map.io",
   217  			expectederror: "",
   218  		},
   219  		// case 8
   220  		{
   221  			endpoint:      "https://upload-actions-map.io",
   222  			expectederror: "Missing action 'download'",
   223  		},
   224  		// case 9
   225  		{
   226  			endpoint:      "https://verify-actions-map.io",
   227  			expectederror: "Missing action 'download'",
   228  		},
   229  		// case 10
   230  		{
   231  			endpoint:      "https://unknown-actions-map.io",
   232  			expectederror: "Missing action 'download'",
   233  		},
   234  	}
   235  
   236  	for n, c := range cases {
   237  		client := &HTTPClient{
   238  			client:    hc,
   239  			endpoint:  c.endpoint,
   240  			transfers: make(map[string]TransferAdapter),
   241  		}
   242  		client.transfers["dummy"] = dummy
   243  
   244  		err := client.Download(context.Background(), []Pointer{p}, func(p Pointer, content io.ReadCloser, objectError error) error {
   245  			if objectError != nil {
   246  				return objectError
   247  			}
   248  			b, err := io.ReadAll(content)
   249  			assert.NoError(t, err)
   250  			assert.Equal(t, []byte("dummy"), b)
   251  			return nil
   252  		})
   253  		if len(c.expectederror) > 0 {
   254  			assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
   255  		} else {
   256  			assert.NoError(t, err, "case %d", n)
   257  		}
   258  	}
   259  }
   260  
   261  func TestHTTPClientUpload(t *testing.T) {
   262  	p := Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041", Size: 6}
   263  
   264  	hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
   265  		assert.Equal(t, "POST", req.Method)
   266  		assert.Equal(t, MediaType, req.Header.Get("Content-type"))
   267  		assert.Equal(t, MediaType, req.Header.Get("Accept"))
   268  
   269  		var batchRequest BatchRequest
   270  		err := json.NewDecoder(req.Body).Decode(&batchRequest)
   271  		assert.NoError(t, err)
   272  
   273  		assert.Equal(t, "upload", batchRequest.Operation)
   274  		assert.Equal(t, 1, len(batchRequest.Objects))
   275  		assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
   276  		assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
   277  
   278  		return lfsTestRoundtripHandler(req)
   279  	})}
   280  	dummy := &DummyTransferAdapter{}
   281  
   282  	cases := []struct {
   283  		endpoint      string
   284  		expectederror string
   285  	}{
   286  		// case 0
   287  		{
   288  			endpoint:      "https://status-not-ok.io",
   289  			expectederror: "Unexpected server response: ",
   290  		},
   291  		// case 1
   292  		{
   293  			endpoint:      "https://invalid-json-response.io",
   294  			expectederror: "invalid json",
   295  		},
   296  		// case 2
   297  		{
   298  			endpoint:      "https://valid-batch-request-upload.io",
   299  			expectederror: "",
   300  		},
   301  		// case 3
   302  		{
   303  			endpoint:      "https://response-no-objects.io",
   304  			expectederror: "",
   305  		},
   306  		// case 4
   307  		{
   308  			endpoint:      "https://unknown-transfer-adapter.io",
   309  			expectederror: "TransferAdapter not found: ",
   310  		},
   311  		// case 5
   312  		{
   313  			endpoint:      "https://error-in-response-objects.io",
   314  			expectederror: "Object not found",
   315  		},
   316  		// case 6
   317  		{
   318  			endpoint:      "https://empty-actions-map.io",
   319  			expectederror: "",
   320  		},
   321  		// case 7
   322  		{
   323  			endpoint:      "https://download-actions-map.io",
   324  			expectederror: "Missing action 'upload'",
   325  		},
   326  		// case 8
   327  		{
   328  			endpoint:      "https://upload-actions-map.io",
   329  			expectederror: "",
   330  		},
   331  		// case 9
   332  		{
   333  			endpoint:      "https://verify-actions-map.io",
   334  			expectederror: "Missing action 'upload'",
   335  		},
   336  		// case 10
   337  		{
   338  			endpoint:      "https://unknown-actions-map.io",
   339  			expectederror: "Missing action 'upload'",
   340  		},
   341  	}
   342  
   343  	for n, c := range cases {
   344  		client := &HTTPClient{
   345  			client:    hc,
   346  			endpoint:  c.endpoint,
   347  			transfers: make(map[string]TransferAdapter),
   348  		}
   349  		client.transfers["dummy"] = dummy
   350  
   351  		err := client.Upload(context.Background(), []Pointer{p}, func(p Pointer, objectError error) (io.ReadCloser, error) {
   352  			return io.NopCloser(new(bytes.Buffer)), objectError
   353  		})
   354  		if len(c.expectederror) > 0 {
   355  			assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
   356  		} else {
   357  			assert.NoError(t, err, "case %d", n)
   358  		}
   359  	}
   360  }