11using System ;
2- using System . Runtime . InteropServices . ComTypes ;
2+ using System . Diagnostics ;
3+ using System . IO ;
4+ using System . Linq ;
5+ using System . Reflection ;
6+ using System . Threading ;
37using Azure . Identity ;
48using Azure . Security . KeyVault . Secrets ;
59using DataPipelineTools . Tests . Common ;
6- using Microsoft . Extensions . Logging ;
10+ using Microsoft . VisualBasic . FileIO ;
711using NUnit . Framework ;
12+ using SearchOption = System . IO . SearchOption ;
813
914namespace DataPipelineTools . Functions . Tests
1015{
1116 /// <summary>
12- /// Base class for functions tests. Exposes the
17+ /// Base class for functions integration tests. Exposes the run settings as properties, along with secrets from either the .runsettings file directly, or
18+ /// Azure Key Vault as specified by the .runsettings file.
1319 /// </summary>
1420 public abstract class IntegrationTestBase : TestBase
1521 {
16- public IntegrationTestBase ( )
22+ protected IntegrationTestBase ( )
1723 {
1824 if ( TestContext . Parameters . Count == 0 )
1925 throw new ArgumentException ( "No setting file is configured for the integration tests." ) ;
@@ -29,22 +35,80 @@ protected bool UseFunctionsEmulator
2935 }
3036 }
3137
32- protected string FunctionsAppName => TestContext . Parameters [ "FunctionsAppName" ] ;
33- protected string FunctionsAppUrl => TestContext . Parameters [ "FunctionsAppUrl" ] ;
38+ protected string FunctionsAppName => UseFunctionsEmulator ? "localhost" : TestContext . Parameters [ "FunctionsAppName" ] ;
39+ protected string FunctionsAppUrl => UseFunctionsEmulator ? "http://localhost:7071" : $ "https:// { TestContext . Parameters [ "FunctionsAppUrl" ] } " ;
3440 protected string StorageAccountName => TestContext . Parameters [ "StorageAccountName" ] ;
3541 protected string StorageContainerName => TestContext . Parameters [ "StorageContainerName" ] ;
3642 protected string KeyVaultName => TestContext . Parameters [ "KeyVaultName" ] ;
3743 protected string ServicePrincipalName => TestContext . Parameters [ "ServicePrincipalName" ] ;
3844 protected string ApplicationInsightsName => TestContext . Parameters [ "ApplicationInsightsName" ] ;
39-
40-
41-
42-
43- protected string FunctionsAppKey => TestContext . Parameters [ "FunctionsAppKey" ] ?? GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretFunctionsAppKey" ] ) ;
44- protected string ServicePrincipalSecretKey => TestContext . Parameters [ "ServicePrincipalSecretKey" ] ?? GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretServicePrincipalSecretKey" ] ) ;
45- protected string StorageContainerSasToken => TestContext . Parameters [ "StorageContainerSasToken" ] ?? GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretStorageContainerSasToken" ] ) ;
46- protected string StorageAccountAccessKey => TestContext . Parameters [ "StorageAccountAccessKey" ] ?? GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretStorageAccountAccessKey" ] ) ;
47- protected string ApplicationInsightsKey => TestContext . Parameters [ "ApplicationInsightsKey" ] ?? GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretApplicationInsightsKey" ] ) ;
45+
46+
47+ // The properties that we get from Azure Key Vault are cached for reuse
48+ private string _functionsAppKey ;
49+ protected string FunctionsAppKey
50+ {
51+ get
52+ {
53+ if ( _functionsAppKey == null )
54+ _functionsAppKey = TestContext . Parameters [ "FunctionsAppKey" ] ??
55+ GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretFunctionsAppKey" ] ) ;
56+
57+ return _functionsAppKey ;
58+ }
59+ }
60+
61+ private string _servicePrincipalSecretKey ;
62+ protected string ServicePrincipalSecretKey
63+ {
64+ get
65+ {
66+ if ( _servicePrincipalSecretKey == null )
67+ _servicePrincipalSecretKey = TestContext . Parameters [ "ServicePrincipalSecretKey" ] ??
68+ GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretServicePrincipalSecretKey" ] ) ;
69+
70+ return _servicePrincipalSecretKey ;
71+ }
72+ }
73+
74+ private string _storageContainerSasToken ;
75+ protected string StorageContainerSasToken
76+ {
77+ get
78+ {
79+ if ( _storageContainerSasToken == null )
80+ _storageContainerSasToken = TestContext . Parameters [ "StorageContainerSasToken" ] ??
81+ GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretStorageContainerSasToken" ] ) ;
82+
83+ return _storageContainerSasToken ;
84+ }
85+ }
86+
87+ private string _storageAccountAccessKey ;
88+ protected string StorageAccountAccessKey
89+ {
90+ get
91+ {
92+ if ( _storageAccountAccessKey == null )
93+ _storageAccountAccessKey = TestContext . Parameters [ "StorageAccountAccessKey" ] ??
94+ GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretStorageAccountAccessKey" ] ) ;
95+
96+ return _storageAccountAccessKey ;
97+ }
98+ }
99+
100+ private string _applicationInsightsKey ;
101+ protected string ApplicationInsightsKey
102+ {
103+ get
104+ {
105+ if ( _applicationInsightsKey == null )
106+ _applicationInsightsKey = TestContext . Parameters [ "ApplicationInsightsKey" ] ??
107+ GetKeyVaultSecretValue ( TestContext . Parameters [ "KeyVaultSecretApplicationInsightsKey" ] ) ;
108+
109+ return _applicationInsightsKey ;
110+ }
111+ }
48112
49113
50114 [ Test ]
@@ -59,12 +123,13 @@ public void Test_RunSettingsLoadedOk()
59123 Assert . IsNotNull ( ServicePrincipalName ) ;
60124 Assert . IsNotNull ( ApplicationInsightsName ) ;
61125 Assert . IsNotNull ( FunctionsAppKey ) ;
126+
62127 Assert . IsNotNull ( ServicePrincipalSecretKey ) ;
63128 Assert . IsNotNull ( StorageContainerSasToken ) ;
64129 Assert . IsNotNull ( StorageAccountAccessKey ) ;
65130 Assert . IsNotNull ( ApplicationInsightsKey ) ;
66131
67- // Check that the StorageContainerSasToken got unquoted correctly
132+ // Check that the StorageContainerSasToken got unquoted correctly from the .runsettings XML file.
68133 Assert . IsFalse ( StorageContainerSasToken . Contains ( "&" ) ) ;
69134 }
70135
@@ -84,12 +149,7 @@ protected bool IsRunningOnCIServer
84149 return false ;
85150 }
86151 }
87-
88- protected void StartLocalFunctionsInstance ( )
89- {
90- throw new NotImplementedException ( ) ;
91- }
92-
152+
93153 protected string GetKeyVaultSecretValue ( string secretName )
94154 {
95155 /* For some reason the DefaultAzureCredential (SharedTokenCacheCredential / VisualStudioCredential) returns a 403 trying to access the key vault, even when access policies are configured correctly
@@ -111,7 +171,101 @@ protected string GetKeyVaultSecretValue(string secretName)
111171 return result ? . Value ? . Value ;
112172 }
113173
114-
115174
175+
176+
177+
178+
179+
180+
181+ #region Azure Functions Local Host
182+ // We use one time setup and teardown to generate a single instance of the emulator across all classes that implement this base class
183+
184+ private static object _functionsProcessLock = new object ( ) ;
185+
186+ [ OneTimeSetUp ]
187+ public void StartFunctionsEmulator ( )
188+ {
189+ lock ( _functionsProcessLock )
190+ {
191+ if ( UseFunctionsEmulator )
192+ {
193+ if ( InstanceCount == 0 )
194+ StartFunctionsEmulatorInternal ( ) ;
195+
196+ InstanceCount ++ ;
197+ }
198+ }
199+ }
200+
201+ [ OneTimeTearDown ]
202+ public void StopFunctionsEmulator ( )
203+ {
204+ lock ( _functionsProcessLock )
205+ {
206+ if ( UseFunctionsEmulator )
207+ {
208+ InstanceCount -- ;
209+
210+ if ( InstanceCount == 0 )
211+ StopFunctionsEmulatorInternal ( ) ;
212+ }
213+ }
214+ }
215+
216+
217+ protected static bool IsEmulatorRunning => LocalFunctionsHostProcess != null ;
218+ private static Process LocalFunctionsHostProcess { get ; set ; }
219+ private static int InstanceCount { get ; set ; }
220+
221+
222+
223+ private void StartFunctionsEmulatorInternal ( )
224+ {
225+ if ( IsEmulatorRunning )
226+ return ;
227+
228+ string appData = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ;
229+ string toolsPath = Path . Join ( appData , "AzureFunctionsTools" , "Releases" ) ;
230+
231+ var toolsVersions = Directory . GetFiles ( toolsPath , "func.exe" , SearchOption . AllDirectories ) ;
232+ var latestToolsVersion = toolsVersions . OrderBy ( x => x ) . FirstOrDefault ( ) ;
233+
234+ if ( latestToolsVersion == null )
235+ throw new FileNotFoundException ( "The Azure Functions Core tools are not installed. Run the functions app locally to install the tools." ) ;
236+
237+ const string args = "host start" ;
238+ string binDir = Path . GetDirectoryName ( Assembly . GetExecutingAssembly ( ) . Location ) ;
239+
240+ binDir = binDir . Replace ( "DataPipelineTools.Functions.Tests" , "DataPipelineTools.Functions" ) ;
241+
242+ binDir = @"C:\Users\Niall\src\sqlcollaborative\AzureDataPipelineTools\DataPipelineTools.Functions\bin\Debug" ;
243+
244+ ProcessStartInfo hostProcess = new ProcessStartInfo
245+ {
246+ FileName = latestToolsVersion ,
247+ Arguments = args ,
248+ WorkingDirectory = binDir ,
249+ CreateNoWindow = false ,
250+ WindowStyle = ProcessWindowStyle . Normal
251+ } ;
252+
253+ LocalFunctionsHostProcess = Process . Start ( hostProcess ) ;
254+
255+ // Sleep for 5 seconds to allow the emulated functions app to start
256+ Thread . Sleep ( 5000 ) ;
257+ }
258+
259+ private void StopFunctionsEmulatorInternal ( )
260+ {
261+ if ( ! IsEmulatorRunning )
262+ return ;
263+
264+ LocalFunctionsHostProcess . Kill ( ) ;
265+ LocalFunctionsHostProcess . WaitForExit ( ) ;
266+ LocalFunctionsHostProcess . Dispose ( ) ;
267+ }
268+
269+ #endregion Azure Functions Local Host
116270 }
117271}
0 commit comments