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 }