github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/locking/azure/storage_account_test.go (about)

     1  package azure
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"os"
     7  	"testing"
     8  
     9  	"github.com/google/uuid"
    10  	"github.com/stretchr/testify/suite"
    11  )
    12  
    13  // Default values to connect to Azurite
    14  const (
    15  	AZURITE_SA_NAME     = "devstoreaccount1"
    16  	AZURITE_CONN_STRING = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
    17  	AZURITE_SHARED_KEY  = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
    18  )
    19  
    20  var (
    21  	usingRealSA = false // Whether we are running tests against a real storage account or not.
    22  	envNames    = []string{
    23  		"DIGGER_AZURE_CONNECTION_STRING",
    24  		"DIGGER_AZURE_SHARED_KEY",
    25  		"DIGGER_AZURE_SA_NAME",
    26  		"DIGGER_AZURE_TENANT_ID",
    27  		"DIGGER_AZURE_CLIENT_ID",
    28  		"DIGGER_AZURE_CLIENT_SECRET",
    29  	}
    30  
    31  	// Holds our current environment
    32  	envs = map[string]string{}
    33  )
    34  
    35  type tt struct {
    36  	loadEnv func(*SALockTestSuite)
    37  	name    string
    38  }
    39  
    40  // We use a test table so we can run our tests,
    41  // using multiple authentication variations.
    42  var (
    43  	testCases = []tt{
    44  		{
    45  			name: "Connection string authentication mode",
    46  			loadEnv: func(*SALockTestSuite) {
    47  				loadConnStringEnv()
    48  			},
    49  		},
    50  		{
    51  			name: "Shared Key authentication mode",
    52  			loadEnv: func(*SALockTestSuite) {
    53  				loadSharedKeyEnv()
    54  			},
    55  		},
    56  		{
    57  			name: "Client secret authentication mode",
    58  			loadEnv: func(s *SALockTestSuite) {
    59  				// Skip this test case if we are not testing on a real storage account
    60  				if !usingRealSA {
    61  					s.T().Skip("Client secret method can only be tested when used against a real storage account.")
    62  					return
    63  				}
    64  				loadClientSecretEnv()
    65  			},
    66  		},
    67  	}
    68  )
    69  
    70  type SALockTestSuite struct {
    71  	suite.Suite
    72  }
    73  
    74  // Runs once before the test suite starts
    75  func (suite *SALockTestSuite) SetupSuite() {
    76  	// Prepare environment variables
    77  	prepareEnv(suite)
    78  
    79  	// Make sure Azurite is started before the tests
    80  	// if we are not using a real storage account
    81  	if usingRealSA {
    82  		return
    83  	}
    84  	conn, err := net.Dial("tcp", "127.0.0.1:10002")
    85  	if err != nil {
    86  		suite.T().Fatalf("Please make sure 'Azurite' table service is started before running Azure tests, or use a real storage account.")
    87  	}
    88  	conn.Close()
    89  
    90  	cleanEnv()
    91  }
    92  
    93  // Runs after every test
    94  func (suite *SALockTestSuite) TearDownTest() {
    95  	cleanEnv()
    96  }
    97  
    98  // Runs after every sub-test
    99  func (suite *SALockTestSuite) TearDownSubTest() {
   100  	cleanEnv()
   101  }
   102  
   103  func (suite *SALockTestSuite) TestNewStorageAccountLock() {
   104  	for _, tc := range testCases {
   105  		suite.Run(tc.name, func() {
   106  			tc.loadEnv(suite)
   107  			sal, err := NewStorageAccountLock()
   108  
   109  			suite.NotNil(sal)
   110  			suite.NoError(err)
   111  		})
   112  	}
   113  }
   114  
   115  func (suite *SALockTestSuite) TestNewStorageAccountLock_NoAuthMethods() {
   116  	sal, err := NewStorageAccountLock()
   117  
   118  	suite.Nil(sal)
   119  	suite.Error(err, "expected an error, since no authentication mecanism was provided")
   120  }
   121  
   122  func (suite *SALockTestSuite) TestNewStorageAccountLock_WithSharedKey_MissingAccountName() {
   123  	loadSharedKeyEnv()
   124  	os.Setenv("DIGGER_AZURE_SA_NAME", "")
   125  
   126  	sal, err := NewStorageAccountLock()
   127  	suite.Nil(sal)
   128  	suite.Error(err, "should have got an error")
   129  }
   130  
   131  func (suite *SALockTestSuite) TestNewStorageAccountLock_WithClientSecret_MissingEnv() {
   132  	loadClientSecretEnv()
   133  	os.Setenv("DIGGER_AZURE_CLIENT_ID", "")
   134  
   135  	sal, err := NewStorageAccountLock()
   136  	suite.Nil(sal)
   137  	suite.Error(err, "should have got an error")
   138  }
   139  
   140  func (suite *SALockTestSuite) TestLock_WhenNotLockedYet() {
   141  	for _, tc := range testCases {
   142  		suite.Run(tc.name, func() {
   143  			tc.loadEnv(suite)
   144  			sal, _ := NewStorageAccountLock()
   145  			ok, err := sal.Lock(18, generateResourceName())
   146  
   147  			suite.True(ok, "lock acquisition should be true")
   148  			suite.NoError(err, "error while acquiring lock")
   149  		})
   150  	}
   151  }
   152  
   153  func (suite *SALockTestSuite) TestLock_WhenAlreadyLocked() {
   154  	for _, tc := range testCases {
   155  		suite.Run(tc.name, func() {
   156  			tc.loadEnv(suite)
   157  			sal, err := NewStorageAccountLock()
   158  			suite.NotNil(sal)
   159  			suite.NoError(err)
   160  			resourceName := generateResourceName()
   161  
   162  			// Locking the first time
   163  			ok, err := sal.Lock(18, resourceName)
   164  			suite.True(ok, "lock acquisition should be true")
   165  			suite.NoError(err, "should not have got an error")
   166  
   167  			// Lock the second time on the same resource name
   168  			ok, err = sal.Lock(18, resourceName)
   169  			suite.False(ok, "lock acquisition should be false")
   170  			suite.NoError(err, "should not have got an error")
   171  		})
   172  	}
   173  }
   174  
   175  func (suite *SALockTestSuite) TestUnlock() {
   176  	for _, tc := range testCases {
   177  		suite.Run(tc.name, func() {
   178  			tc.loadEnv(suite)
   179  			sal, _ := NewStorageAccountLock()
   180  			resourceName := generateResourceName()
   181  
   182  			// Locking
   183  			ok, err := sal.Lock(18, resourceName)
   184  			suite.True(ok, "lock acquisition should be true")
   185  			suite.NoError(err, "should not have got an error")
   186  
   187  			// Unlocking
   188  			ok, err = sal.Unlock(resourceName)
   189  			suite.True(ok)
   190  			suite.NoError(err, "should not have got an error")
   191  		})
   192  	}
   193  }
   194  
   195  func (suite *SALockTestSuite) TestUnlock_WhenLockDoesNotExist() {
   196  	for _, tc := range testCases {
   197  		suite.Run(tc.name, func() {
   198  			tc.loadEnv(suite)
   199  			sal, err := NewStorageAccountLock()
   200  			suite.NotNil(sal)
   201  			suite.Require().NoError(err)
   202  
   203  			// Unlocking
   204  			resourceName := generateResourceName()
   205  			ok, err := sal.Unlock(resourceName)
   206  			suite.False(ok)
   207  			suite.Error(err, "should have got an error")
   208  		})
   209  	}
   210  }
   211  
   212  func (suite *SALockTestSuite) TestUnlock_Twice_WhenLockExist() {
   213  	for _, tc := range testCases {
   214  		suite.Run(tc.name, func() {
   215  			tc.loadEnv(suite)
   216  			sal, _ := NewStorageAccountLock()
   217  			resourceName := generateResourceName()
   218  
   219  			// Locking
   220  			ok, err := sal.Lock(18, resourceName)
   221  			suite.True(ok, "lock acquisition should be true")
   222  			suite.NoError(err, "should not have got an error")
   223  
   224  			// Unlocking the first time
   225  			ok, err = sal.Unlock(resourceName)
   226  			suite.True(ok)
   227  			suite.NoError(err, "should not have got an error")
   228  
   229  			// Unlocking the second time
   230  			ok, err = sal.Unlock(resourceName)
   231  			suite.False(ok)
   232  			suite.Error(err, "should have got an error")
   233  		})
   234  	}
   235  }
   236  
   237  func (suite *SALockTestSuite) TestGetLock() {
   238  	for _, tc := range testCases {
   239  		suite.Run(tc.name, func() {
   240  			tc.loadEnv(suite)
   241  			sal, err := NewStorageAccountLock()
   242  			suite.NotNil(sal)
   243  			suite.Require().NoError(err)
   244  
   245  			// Locking
   246  			resourceName := generateResourceName()
   247  			ok, err := sal.Lock(21, resourceName)
   248  			suite.Require().True(ok, "lock acquisition should be true")
   249  			suite.Require().NoError(err, "should not have got an error")
   250  
   251  			// Get the lock
   252  			transactionId, err := sal.GetLock(resourceName)
   253  			suite.Equal(21, *transactionId, "transaction id mismatch")
   254  			suite.NoError(err, "should not have got an error")
   255  		})
   256  	}
   257  }
   258  
   259  func (suite *SALockTestSuite) TestGetLock_WhenLockDoesNotExist() {
   260  	for _, tc := range testCases {
   261  		suite.Run(tc.name, func() {
   262  			tc.loadEnv(suite)
   263  			sal, err := NewStorageAccountLock()
   264  			suite.Require().NotNil(sal)
   265  			suite.Require().NoError(err)
   266  
   267  			// Get a lock that doesn't exist
   268  			resourceName := generateResourceName()
   269  			transactionId, err := sal.GetLock(resourceName)
   270  			suite.Nil(transactionId, "transaction id should be nil")
   271  			suite.NoError(err, "should not have got an error")
   272  		})
   273  	}
   274  }
   275  
   276  func (suite *SALockTestSuite) TestGetLock_WithForbiddenCharacters() {
   277  	for _, tc := range testCases {
   278  		suite.Run(tc.name, func() {
   279  			tc.loadEnv(suite)
   280  			sal, err := NewStorageAccountLock()
   281  			suite.NotNil(sal)
   282  			suite.Require().NoError(err)
   283  
   284  			// Locking
   285  			resourceName := fmt.Sprintf("digger/diggerhq/%s#project/", generateResourceName())
   286  			ok, err := sal.Lock(21, resourceName)
   287  			suite.Require().True(ok, "lock acquisition should be true")
   288  			suite.Require().NoError(err, "should not have got an error")
   289  
   290  			// Get the lock
   291  			transactionId, err := sal.GetLock(resourceName)
   292  			suite.Equal(21, *transactionId, "transaction id mismatch")
   293  			suite.NoError(err, "should not have got an error")
   294  		})
   295  	}
   296  }
   297  
   298  // Entrypoint of the test suite
   299  func TestStorageAccountLockTestSuite(t *testing.T) {
   300  	// To test Azure Storage account we can either:
   301  	// 1. use a real storage account if we have one
   302  	// 2. or use a local storage account emulator (Azurite)
   303  
   304  	useRealSA := os.Getenv("DIGGER_TEST_USE_REAL_SA")
   305  
   306  	// Override the service url format
   307  	// if we are not using a real storage account
   308  	// We'll be relying on Azurite emulator which has
   309  	// a different url format
   310  	if useRealSA == "" || useRealSA == "0" {
   311  		SERVICE_URL_FORMAT = "http://127.0.0.1:10002/%s"
   312  		usingRealSA = false
   313  		suite.Run(t, new(SALockTestSuite))
   314  		return
   315  	}
   316  
   317  	usingRealSA = true
   318  	suite.Run(t, new(SALockTestSuite))
   319  }
   320  
   321  // Generate unique string to be used as resource name
   322  // so we don't have to delete the entity
   323  func generateResourceName() string {
   324  	return uuid.New().String()
   325  }
   326  
   327  // Initialize and save environment variables into a map.
   328  // This is useful so we can alter  environment variables
   329  // without losing their initial values.
   330  
   331  // This comes is handy when we want to test cases
   332  // where an environment variable is not defined.
   333  func prepareEnv(s *SALockTestSuite) {
   334  	// When using Azurite, environment variables are known
   335  	if !usingRealSA {
   336  		envs["DIGGER_AZURE_SHARED_KEY"] = AZURITE_SHARED_KEY
   337  		envs["DIGGER_AZURE_SA_NAME"] = AZURITE_SA_NAME
   338  		envs["DIGGER_AZURE_CONNECTION_STRING"] = AZURITE_CONN_STRING
   339  		return
   340  	}
   341  
   342  	// When using real storage account, environment
   343  	// variables are not known, and must be injected by the user
   344  	// before starting our tests.
   345  	for _, env := range envNames {
   346  		envValue, exists := os.LookupEnv(env)
   347  		if !exists {
   348  			s.T().Fatalf("Since 'DIGGER_TEST_USE_REAL_SA' has been set, '%s' environment variable must also be set before starting the tests.", env)
   349  		}
   350  
   351  		envs[env] = envValue
   352  	}
   353  }
   354  
   355  func loadSharedKeyEnv() {
   356  	os.Setenv("DIGGER_AZURE_AUTH_METHOD", "SHARED_KEY")
   357  
   358  	os.Setenv("DIGGER_AZURE_SHARED_KEY", envs["DIGGER_AZURE_SHARED_KEY"])
   359  	os.Setenv("DIGGER_AZURE_SA_NAME", envs["DIGGER_AZURE_SA_NAME"])
   360  }
   361  
   362  func loadConnStringEnv() {
   363  	os.Setenv("DIGGER_AZURE_AUTH_METHOD", "CONNECTION_STRING")
   364  	os.Setenv("DIGGER_AZURE_CONNECTION_STRING", envs["DIGGER_AZURE_CONNECTION_STRING"])
   365  }
   366  
   367  func loadClientSecretEnv() {
   368  	os.Setenv("DIGGER_AZURE_AUTH_METHOD", "CLIENT_SECRET")
   369  
   370  	os.Setenv("DIGGER_AZURE_TENANT_ID", envs["DIGGER_AZURE_TENANT_ID"])
   371  	os.Setenv("DIGGER_AZURE_CLIENT_ID", envs["DIGGER_AZURE_CLIENT_ID"])
   372  	os.Setenv("DIGGER_AZURE_CLIENT_SECRET", envs["DIGGER_AZURE_CLIENT_SECRET"])
   373  	os.Setenv("DIGGER_AZURE_SA_NAME", envs["DIGGER_AZURE_SA_NAME"])
   374  }
   375  
   376  // Clean environment variables
   377  func cleanEnv() {
   378  	for _, env := range envNames {
   379  		os.Setenv(env, "")
   380  	}
   381  }