1// +build !integration 2 3package azure 4 5import ( 6 "encoding/base64" 7 "errors" 8 "fmt" 9 "net/http" 10 "net/url" 11 "testing" 12 "time" 13 14 "github.com/sirupsen/logrus/hooks/test" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 18 "gitlab.com/gitlab-org/gitlab-runner/common" 19) 20 21var ( 22 accountName = "azuretest" 23 accountKey = base64.StdEncoding.EncodeToString([]byte("12345")) 24 containerName = "test" 25 objectName = "key" 26 storageDomain = "example.com" 27 defaultTimeout = 1 * time.Hour 28) 29 30func defaultAzureCache() *common.CacheConfig { 31 return &common.CacheConfig{ 32 Type: "azure", 33 Azure: &common.CacheAzureConfig{ 34 CacheAzureCredentials: common.CacheAzureCredentials{ 35 AccountName: accountName, 36 AccountKey: accountKey, 37 }, 38 ContainerName: containerName, 39 StorageDomain: storageDomain, 40 }, 41 } 42} 43 44type adapterOperationInvalidConfigTestCase struct { 45 provideAzureConfig bool 46 47 errorOnCredentialsResolverInitialization bool 48 credentialsResolverResolveError bool 49 50 accountName string 51 accountKey string 52 containerName string 53 expectedErrorMsg string 54 expectedGoCloudURL string 55} 56 57func prepareMockedCredentialsResolverInitializer(tc adapterOperationInvalidConfigTestCase) func() { 58 oldCredentialsResolverInitializer := credentialsResolverInitializer 59 credentialsResolverInitializer = func(config *common.CacheAzureConfig) (*defaultCredentialsResolver, error) { 60 if tc.errorOnCredentialsResolverInitialization { 61 return nil, errors.New("test error") 62 } 63 64 return newDefaultCredentialsResolver(config) 65 } 66 67 return func() { 68 credentialsResolverInitializer = oldCredentialsResolverInitializer 69 } 70} 71 72func prepareMockedCredentialsResolverForInvalidConfig(adapter *azureAdapter, tc adapterOperationInvalidConfigTestCase) { 73 cr := &mockCredentialsResolver{} 74 75 resolveCall := cr.On("Resolve") 76 if tc.credentialsResolverResolveError { 77 resolveCall.Return(fmt.Errorf("test error")) 78 } else { 79 resolveCall.Return(nil) 80 } 81 82 cr.On("Credentials").Return(&common.CacheAzureCredentials{ 83 AccountName: tc.accountName, 84 AccountKey: tc.accountKey, 85 }) 86 87 adapter.credentialsResolver = cr 88} 89 90func testAdapterOperationWithInvalidConfig( 91 t *testing.T, 92 name string, 93 tc adapterOperationInvalidConfigTestCase, 94 adapter *azureAdapter, 95 operation func() *url.URL, 96) { 97 t.Run(name, func(t *testing.T) { 98 prepareMockedCredentialsResolverForInvalidConfig(adapter, tc) 99 hook := test.NewGlobal() 100 101 u := operation() 102 assert.Nil(t, u) 103 104 message, err := hook.LastEntry().String() 105 require.NoError(t, err) 106 assert.Contains(t, message, tc.expectedErrorMsg) 107 }) 108} 109 110func testGoCloudURLWithInvalidConfig( 111 t *testing.T, 112 name string, 113 tc adapterOperationInvalidConfigTestCase, 114 adapter *azureAdapter, 115 operation func() *url.URL, 116) { 117 t.Run(name, func(t *testing.T) { 118 prepareMockedCredentialsResolverForInvalidConfig(adapter, tc) 119 120 u := operation() 121 122 if u != nil { 123 assert.Equal(t, tc.expectedGoCloudURL, u.String()) 124 } else { 125 assert.Empty(t, tc.expectedGoCloudURL) 126 } 127 }) 128} 129 130func testUploadEnvWithInvalidConfig( 131 t *testing.T, 132 name string, 133 tc adapterOperationInvalidConfigTestCase, 134 adapter *azureAdapter, 135 operation func() map[string]string, 136) { 137 t.Run(name, func(t *testing.T) { 138 prepareMockedCredentialsResolverForInvalidConfig(adapter, tc) 139 140 u := operation() 141 assert.Empty(t, u) 142 }) 143} 144 145func TestAdapterOperation_InvalidConfig(t *testing.T) { 146 tests := map[string]adapterOperationInvalidConfigTestCase{ 147 "no-azure-config": { 148 containerName: containerName, 149 expectedErrorMsg: "Missing Azure configuration", 150 }, 151 "error-on-credentials-resolver-initialization": { 152 provideAzureConfig: true, 153 errorOnCredentialsResolverInitialization: true, 154 }, 155 "credentials-resolver-resolve-error": { 156 provideAzureConfig: true, 157 credentialsResolverResolveError: true, 158 containerName: containerName, 159 expectedErrorMsg: `error resolving Azure credentials" error="test error"`, 160 expectedGoCloudURL: "azblob://test/key", 161 }, 162 "no-credentials": { 163 provideAzureConfig: true, 164 containerName: containerName, 165 expectedErrorMsg: "error generating Azure pre-signed URL\" error=\"missing Azure storage account name\"", 166 expectedGoCloudURL: "azblob://test/key", 167 }, 168 "no-account-name": { 169 provideAzureConfig: true, 170 accountKey: accountKey, 171 containerName: containerName, 172 expectedErrorMsg: "error generating Azure pre-signed URL\" error=\"missing Azure storage account name\"", 173 expectedGoCloudURL: "azblob://test/key", 174 }, 175 "no-account-key": { 176 provideAzureConfig: true, 177 accountName: accountName, 178 containerName: containerName, 179 expectedErrorMsg: "error generating Azure pre-signed URL\" error=\"missing Azure storage account key\"", 180 expectedGoCloudURL: "azblob://test/key", 181 }, 182 "invalid-container-name-and-no-account-key": { 183 provideAzureConfig: true, 184 accountName: accountName, 185 containerName: "\x00", 186 expectedErrorMsg: "error generating Azure pre-signed URL\" error=\"missing Azure storage account key\"", 187 }, 188 "container-not-specified": { 189 provideAzureConfig: true, 190 accountName: "access-id", 191 accountKey: accountKey, 192 expectedErrorMsg: "ContainerName can't be empty", 193 }, 194 } 195 196 for name, tc := range tests { 197 t.Run(name, func(t *testing.T) { 198 cleanupCredentialsResolverInitializerMock := prepareMockedCredentialsResolverInitializer(tc) 199 defer cleanupCredentialsResolverInitializerMock() 200 201 config := defaultAzureCache() 202 config.Azure.ContainerName = tc.containerName 203 if !tc.provideAzureConfig { 204 config.Azure = nil 205 } 206 207 a, err := New(config, defaultTimeout, objectName) 208 if !tc.provideAzureConfig { 209 assert.Nil(t, a) 210 assert.EqualError(t, err, "missing Azure configuration") 211 return 212 } 213 214 if tc.errorOnCredentialsResolverInitialization { 215 assert.Nil(t, a) 216 assert.EqualError(t, err, "error while initializing Azure credentials resolver: test error") 217 return 218 } 219 220 require.NotNil(t, a) 221 assert.NoError(t, err) 222 223 adapter, ok := a.(*azureAdapter) 224 require.True(t, ok, "Adapter should be properly casted to *adapter type") 225 226 testAdapterOperationWithInvalidConfig(t, "GetDownloadURL", tc, adapter, a.GetDownloadURL) 227 testAdapterOperationWithInvalidConfig(t, "GetUploadURL", tc, adapter, a.GetUploadURL) 228 testGoCloudURLWithInvalidConfig(t, "GetGoCloudURL", tc, adapter, a.GetGoCloudURL) 229 testUploadEnvWithInvalidConfig(t, "GetUploadEnv", tc, adapter, a.GetUploadEnv) 230 }) 231 } 232} 233 234type adapterOperationTestCase struct { 235 objectName string 236 returnedURL string 237 returnedError error 238 expectedError string 239} 240 241func prepareMockedCredentialsResolver(adapter *azureAdapter) func(t *testing.T) { 242 cr := &mockCredentialsResolver{} 243 cr.On("Resolve").Return(nil) 244 cr.On("Credentials").Return(&common.CacheAzureCredentials{ 245 AccountName: accountName, 246 AccountKey: accountKey, 247 }) 248 249 adapter.credentialsResolver = cr 250 251 return func(t *testing.T) { 252 cr.AssertExpectations(t) 253 } 254} 255 256func prepareMockedSignedURLGenerator( 257 t *testing.T, 258 tc adapterOperationTestCase, 259 expectedMethod string, 260 adapter *azureAdapter, 261) { 262 adapter.generateSignedURL = func(name string, opts *signedURLOptions) (*url.URL, error) { 263 assert.Equal(t, containerName, opts.ContainerName) 264 assert.Equal(t, accountName, opts.Credentials.AccountName) 265 assert.Equal(t, accountKey, opts.Credentials.AccountKey) 266 assert.Equal(t, expectedMethod, opts.Method) 267 268 u, err := url.Parse(tc.returnedURL) 269 if err != nil { 270 return nil, err 271 } 272 273 return u, tc.returnedError 274 } 275} 276 277func testAdapterOperation( 278 t *testing.T, 279 tc adapterOperationTestCase, 280 name string, 281 expectedMethod string, 282 adapter *azureAdapter, 283 operation func() *url.URL, 284) { 285 t.Run(name, func(t *testing.T) { 286 cleanupCredentialsResolverMock := prepareMockedCredentialsResolver(adapter) 287 defer cleanupCredentialsResolverMock(t) 288 289 prepareMockedSignedURLGenerator(t, tc, expectedMethod, adapter) 290 hook := test.NewGlobal() 291 292 u := operation() 293 294 if tc.expectedError != "" { 295 message, err := hook.LastEntry().String() 296 require.NoError(t, err) 297 assert.Contains(t, message, tc.expectedError) 298 return 299 } 300 301 assert.Empty(t, hook.AllEntries()) 302 303 assert.Equal(t, tc.returnedURL, u.String()) 304 }) 305} 306 307func TestAdapterOperation(t *testing.T) { 308 tests := map[string]adapterOperationTestCase{ 309 "error-on-URL-signing": { 310 objectName: objectName, 311 returnedURL: "", 312 returnedError: fmt.Errorf("test error"), 313 expectedError: "error generating Azure pre-signed URL\" error=\"test error\"", 314 }, 315 "invalid-URL-returned": { 316 objectName: objectName, 317 returnedURL: "://test", 318 returnedError: nil, 319 expectedError: "error generating Azure pre-signed URL\" error=\"parse", 320 }, 321 "valid-configuration": { 322 objectName: objectName, 323 returnedURL: "https://myaccount.blob.core.windows.net/mycontainer/mydirectory/myfile.txt?sig=XYZ&sp=r", 324 returnedError: nil, 325 expectedError: "", 326 }, 327 "valid-configuration-with-leading-slash": { 328 objectName: "/" + objectName, 329 returnedURL: "https://myaccount.blob.core.windows.net/mycontainer/mydirectory/myfile.txt?sig=XYZ&sp=r", 330 returnedError: nil, 331 expectedError: "", 332 }, 333 } 334 335 for name, tc := range tests { 336 t.Run(name, func(t *testing.T) { 337 config := defaultAzureCache() 338 339 a, err := New(config, defaultTimeout, tc.objectName) 340 require.NoError(t, err) 341 342 adapter, ok := a.(*azureAdapter) 343 require.True(t, ok, "Adapter should be properly casted to *adapter type") 344 345 testAdapterOperation( 346 t, 347 tc, 348 "GetDownloadURL", 349 http.MethodGet, 350 adapter, 351 a.GetDownloadURL, 352 ) 353 testAdapterOperation( 354 t, 355 tc, 356 "GetUploadURL", 357 http.MethodPut, 358 adapter, 359 a.GetUploadURL, 360 ) 361 362 headers := adapter.GetUploadHeaders() 363 require.NotNil(t, headers) 364 assert.Len(t, headers, 2) 365 assert.Equal(t, "application/octet-stream", headers.Get("Content-Type")) 366 assert.Equal(t, "BlockBlob", headers.Get("x-ms-blob-type")) 367 368 u := adapter.GetGoCloudURL() 369 assert.Equal(t, "azblob://test/key", u.String()) 370 371 env := adapter.GetUploadEnv() 372 assert.Len(t, env, 3) 373 assert.Equal(t, accountName, env["AZURE_STORAGE_ACCOUNT"]) 374 assert.NotEmpty(t, env["AZURE_STORAGE_SAS_TOKEN"]) 375 assert.Empty(t, env["AZURE_STORAGE_KEY"]) 376 assert.Equal(t, storageDomain, env["AZURE_STORAGE_DOMAIN"]) 377 }) 378 } 379} 380