code.gitea.io/gitea@v1.19.3/modules/lfs/http_client_test.go (about)

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