github.com/secure-build/gitlab-runner@v12.5.0+incompatible/cache/gcs/adapter_test.go (about)

     1  package gcs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"testing"
     9  	"time"
    10  
    11  	"cloud.google.com/go/storage"
    12  	"github.com/sirupsen/logrus/hooks/test"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"gitlab.com/gitlab-org/gitlab-runner/common"
    17  )
    18  
    19  var (
    20  	accessID   = "test-access-id@X.iam.gserviceaccount.com"
    21  	privateKey = `-----BEGIN RSA PRIVATE KEY-----
    22  MIIEpQIBAAKCAQEAzIrvApxNX3VxH5eYe4vI2kLTqOA9uFTV4clGy8uzQsGQvMjl
    23  frTWCffayxaSvoKxPlvUYbecYpqqqaByLTE+kSDU/D44yrCiLAyWHWXYGZqfEMEG
    24  uHBg4fJK6KcIXlJ3Hp3EGTPw92sCKKzLXyoY7mNN9iP8mnshc39wjdrqm2YgKvQU
    25  ZWDxIL/MTtLcWyK07zJ2RamilcjpKtQL5GFgvHCsV1CvQHuKtmZF5kfHlD2E/e+I
    26  uEg+fntGkKJpDYtSn1fbLcg/ctFJKQBLfAaJ59Hgyewd8fKveJ6Vn1C7gCXagMPb
    27  q54RS8J0dolPaxUtRbzGMJ5Amag8m3dm6U3FbwIDAQABAoIBAQCxC+U8Vjymzwoe
    28  9WIYNnOhcMyy1X63Cj+j00wDZQuCUffNYPs8xJysPizVM3HLk2aF+oiIGJ01wHjO
    29  oMGTmpd0mX2h5N3VnDSTekWJprj52Jusrdf6V9OUX9w1KzeUJT9Ucezmf84o6ygQ
    30  OxlCAzdXSP+XeajRspjO11V+hCokXSICAMMnUYyqT+Yr34YldjpVJ3VWFHipByww
    31  1BCHBveJuH4wgVW4QICDKBzzYyFCqi8kFFv8ijQ9QOAD2xkVYiP8sOR1K6h/FuHN
    32  KV+axHtQjkYgOlyYN7/oe9L0XroCa4h7XibcWLuLQ56G3oBzTFur0la3A1SuKLGm
    33  LwBfeVpxAoGBAPCKUiqan24h8RgscEXtbACVa3WmEmOe4qqjnEChof8U5xP4YdfZ
    34  cg+k7eBqXBgVtmxozJOQxcPwkZrHIRP59d2h8vjcjOBrMeI3D9BCjTKGYySv0iRT
    35  FI0akA0c0Ec7utN4t7AfY7sUpx+wvX/klYy5bsIzOceU/9rYYoudXLnZAoGBANmw
    36  VWykOgJZLv8aSTLCDEl2WV6nsl1jRYONVzlthcgQ1wpdgAJvLoTJMuXuSzOQQbUa
    37  08Zm2LhbDErX7YA8MslaiQERSfedV/EXjZn86CBw6wB4IPv8uWh9zSK7E4IH4Den
    38  Ow2RE5XjEDiyMA2PUCAGqVEmF/V4nRCFvEfS52SHAoGBAI56MA9CRTsz6Z3a/Km+
    39  5yE1YFBwjSXq//H5NV1nIBB6riE7F6GGEDTKCYjLFz/A5Kw0KzEhKLNV9LkMSECP
    40  551fBw93fA6WEBchbEF8miwaQ/GAH2Yau+qUmEzcC1aWP6RxNcSh4y32HsP7qVNu
    41  71JKqBtpwkjArghP8ZcnH7yJAoGBAJnHDxFoEfKGvcRH9V195uAeUpOjM0T1U63S
    42  ssNGszLZco9H7Z3KnLoAx4vWAhmy1jfxc5i8HmxdJRnZ31SvMdE7u3ydkfrxk6Yk
    43  VUtqdTA1lE0Ij4Ryyycdd0QJk4ZPufyWjgjPa15+wH7MoVVy388/5WwF1Pb69Tku
    44  wAqc2gkRAoGAcj8a+peaNKa1d5EPE0CtTBUypupZh/R1ewTC9y7OyBPczYhxN5NQ
    45  vvm6J1WGbnxmuhzzvGNNExeZx9dfGLmcvSAvrweiFbi2yHAc1cBLBkc5/CqfS6QW
    46  336Qe2lgsM61/jrYYYqu7W8l6W2juCz0SPqml6rugsP8r6IMJxfziO8=
    47  -----END RSA PRIVATE KEY-----`
    48  
    49  	bucketName     = "test"
    50  	objectName     = "key"
    51  	defaultTimeout = 1 * time.Hour
    52  )
    53  
    54  func defaultGCSCache() *common.CacheConfig {
    55  	return &common.CacheConfig{
    56  		Type: "gcs",
    57  		GCS: &common.CacheGCSConfig{
    58  			BucketName: bucketName,
    59  		},
    60  	}
    61  }
    62  
    63  type adapterOperationInvalidConfigTestCase struct {
    64  	noGCSConfig bool
    65  
    66  	errorOnCredentialsResolverInitialization bool
    67  	credentialsResolverResolveError          bool
    68  
    69  	accessID      string
    70  	privateKey    string
    71  	bucketName    string
    72  	expectedError string
    73  }
    74  
    75  func prepareMockedCredentialsResolverInitializer(tc adapterOperationInvalidConfigTestCase) func() {
    76  	oldCredentialsResolverInitializer := credentialsResolverInitializer
    77  	credentialsResolverInitializer = func(config *common.CacheGCSConfig) (*defaultCredentialsResolver, error) {
    78  		if tc.errorOnCredentialsResolverInitialization {
    79  			return nil, errors.New("test error")
    80  		}
    81  
    82  		return newDefaultCredentialsResolver(config)
    83  	}
    84  
    85  	return func() {
    86  		credentialsResolverInitializer = oldCredentialsResolverInitializer
    87  	}
    88  }
    89  
    90  func prepareMockedCredentialsResolverForInvalidConfig(adapter *gcsAdapter, tc adapterOperationInvalidConfigTestCase) {
    91  	cr := &mockCredentialsResolver{}
    92  
    93  	resolveCall := cr.On("Resolve")
    94  	if tc.credentialsResolverResolveError {
    95  		resolveCall.Return(fmt.Errorf("test error"))
    96  	} else {
    97  		resolveCall.Return(nil)
    98  	}
    99  
   100  	cr.On("Credentials").Return(&common.CacheGCSCredentials{
   101  		AccessID:   tc.accessID,
   102  		PrivateKey: tc.privateKey,
   103  	})
   104  
   105  	adapter.credentialsResolver = cr
   106  }
   107  
   108  func testAdapterOperationWithInvalidConfig(t *testing.T, name string, tc adapterOperationInvalidConfigTestCase, adapter *gcsAdapter, operation func() *url.URL) {
   109  	t.Run(name, func(t *testing.T) {
   110  		prepareMockedCredentialsResolverForInvalidConfig(adapter, tc)
   111  		hook := test.NewGlobal()
   112  
   113  		u := operation()
   114  		assert.Nil(t, u)
   115  
   116  		message, err := hook.LastEntry().String()
   117  		require.NoError(t, err)
   118  		assert.Contains(t, message, tc.expectedError)
   119  	})
   120  }
   121  
   122  func TestAdapterOperation_InvalidConfig(t *testing.T) {
   123  	tests := map[string]adapterOperationInvalidConfigTestCase{
   124  		"no-gcs-config": {
   125  			noGCSConfig:   true,
   126  			bucketName:    bucketName,
   127  			expectedError: "Missing GCS configuration",
   128  		},
   129  		"error-on-credentials-resolver-initialization": {
   130  			errorOnCredentialsResolverInitialization: true,
   131  		},
   132  		"credentials-resolver-resolve-error": {
   133  			credentialsResolverResolveError: true,
   134  			bucketName:                      bucketName,
   135  			expectedError:                   "error while resolving GCS credentials: test error",
   136  		},
   137  		"no-credentials": {
   138  			bucketName:    bucketName,
   139  			expectedError: "storage: missing required GoogleAccessID",
   140  		},
   141  		"no-access-id": {
   142  			privateKey:    privateKey,
   143  			bucketName:    bucketName,
   144  			expectedError: "storage: missing required GoogleAccessID",
   145  		},
   146  		"no-private-key": {
   147  			accessID:      accessID,
   148  			bucketName:    bucketName,
   149  			expectedError: "storage: exactly one of PrivateKey or SignedBytes must be set",
   150  		},
   151  		"bucket-not-specified": {
   152  			accessID:      "access-id",
   153  			privateKey:    privateKey,
   154  			expectedError: "BucketName can't be empty",
   155  		},
   156  	}
   157  
   158  	for name, tc := range tests {
   159  		t.Run(name, func(t *testing.T) {
   160  			cleanupCredentialsResolverInitializerMock := prepareMockedCredentialsResolverInitializer(tc)
   161  			defer cleanupCredentialsResolverInitializerMock()
   162  
   163  			config := defaultGCSCache()
   164  			if tc.noGCSConfig {
   165  				config.GCS = nil
   166  			} else {
   167  				config.GCS.BucketName = tc.bucketName
   168  			}
   169  
   170  			a, err := New(config, defaultTimeout, objectName)
   171  			if tc.noGCSConfig {
   172  				assert.Nil(t, a)
   173  				assert.EqualError(t, err, "missing GCS configuration")
   174  				return
   175  			}
   176  
   177  			if tc.errorOnCredentialsResolverInitialization {
   178  				assert.Nil(t, a)
   179  				assert.EqualError(t, err, "error while initializing GCS credentials resolver: test error")
   180  				return
   181  			}
   182  
   183  			require.NotNil(t, a)
   184  			require.NoError(t, err)
   185  
   186  			adapter, ok := a.(*gcsAdapter)
   187  			require.True(t, ok, "Adapter should be properly casted to *adapter type")
   188  
   189  			testAdapterOperationWithInvalidConfig(t, "GetDownloadURL", tc, adapter, a.GetDownloadURL)
   190  			testAdapterOperationWithInvalidConfig(t, "GetUploadURL", tc, adapter, a.GetUploadURL)
   191  		})
   192  	}
   193  }
   194  
   195  type adapterOperationTestCase struct {
   196  	returnedURL   string
   197  	returnedError error
   198  	expectedError string
   199  }
   200  
   201  func prepareMockedCredentialsResolver(adapter *gcsAdapter) func(t *testing.T) {
   202  	cr := &mockCredentialsResolver{}
   203  	cr.On("Resolve").Return(nil)
   204  	cr.On("Credentials").Return(&common.CacheGCSCredentials{
   205  		AccessID:   accessID,
   206  		PrivateKey: privateKey,
   207  	})
   208  
   209  	adapter.credentialsResolver = cr
   210  
   211  	return func(t *testing.T) {
   212  		cr.AssertExpectations(t)
   213  	}
   214  }
   215  
   216  func prepareMockedSignedURLGenerator(t *testing.T, tc adapterOperationTestCase, expectedMethod string, expectedContentType string, adapter *gcsAdapter) {
   217  	adapter.generateSignedURL = func(bucket string, name string, opts *storage.SignedURLOptions) (string, error) {
   218  		require.Equal(t, accessID, opts.GoogleAccessID)
   219  		require.Equal(t, privateKey, string(opts.PrivateKey))
   220  		require.Equal(t, expectedMethod, opts.Method)
   221  		require.Equal(t, expectedContentType, opts.ContentType)
   222  
   223  		return tc.returnedURL, tc.returnedError
   224  	}
   225  }
   226  
   227  func testAdapterOperation(t *testing.T, tc adapterOperationTestCase, name string, expectedMethod string, expectedContentType string, adapter *gcsAdapter, operation func() *url.URL) {
   228  	t.Run(name, func(t *testing.T) {
   229  		cleanupCredentialsResolverMock := prepareMockedCredentialsResolver(adapter)
   230  		defer cleanupCredentialsResolverMock(t)
   231  
   232  		prepareMockedSignedURLGenerator(t, tc, expectedMethod, expectedContentType, adapter)
   233  		hook := test.NewGlobal()
   234  
   235  		u := operation()
   236  
   237  		if tc.expectedError != "" {
   238  			message, err := hook.LastEntry().String()
   239  			require.NoError(t, err)
   240  			assert.Contains(t, message, tc.expectedError)
   241  			return
   242  		}
   243  
   244  		require.Len(t, hook.AllEntries(), 0)
   245  
   246  		assert.Equal(t, tc.returnedURL, u.String())
   247  	})
   248  }
   249  
   250  func TestAdapterOperation(t *testing.T) {
   251  	tests := map[string]adapterOperationTestCase{
   252  		"error-on-URL-signing": {
   253  			returnedURL:   "",
   254  			returnedError: fmt.Errorf("test error"),
   255  			expectedError: "error while generating GCS pre-signed URL: test error",
   256  		},
   257  		"invalid-URL-returned": {
   258  			returnedURL:   "://test",
   259  			returnedError: nil,
   260  			expectedError: "error while parsing generated URL: parse ://test: missing protocol scheme",
   261  		},
   262  		"valid-configuration": {
   263  			returnedURL:   "https://storage.googleapis.com/test/key?Expires=123456789&GoogleAccessId=test-access-id%40X.iam.gserviceaccount.com&Signature=XYZ",
   264  			returnedError: nil,
   265  			expectedError: "",
   266  		},
   267  	}
   268  
   269  	for name, tc := range tests {
   270  		t.Run(name, func(t *testing.T) {
   271  			config := defaultGCSCache()
   272  
   273  			a, err := New(config, defaultTimeout, objectName)
   274  			require.NoError(t, err)
   275  
   276  			adapter, ok := a.(*gcsAdapter)
   277  			require.True(t, ok, "Adapter should be properly casted to *adapter type")
   278  
   279  			testAdapterOperation(t, tc, "GetDownloadURL", http.MethodGet, "", adapter, a.GetDownloadURL)
   280  			testAdapterOperation(t, tc, "GetUploadURL", http.MethodPut, "application/octet-stream", adapter, a.GetUploadURL)
   281  		})
   282  	}
   283  }