From a59f94d75362f82cf7f85ae8dee1ba78cfd8791e Mon Sep 17 00:00:00 2001 From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com> Date: Mon, 13 May 2024 15:52:30 -0700 Subject: [PATCH 01/40] Adding MEF Extensibility model to Test Engine (#322) * Adding MEF module support * Add module filter * Update to read from assembly location by default * BrowserContext in TestInfraFunctions for modules * Diagnostics update * Network Request module extension update * Adding ability to extend BrowsernewContextOptions * Update to Playwright 1.33 * Add Enable with Allow/Deny settings * Adding assembly checks and docs for modules * Update InstallSource * Additional call checks * Addition tests cases * Adding support for Non Power Apps tests * Unit test updates * Change to Copy task * Format updates * Refactor IUserManager to a MEF plugin * Adding support for Static Browser state * Updated to add Certificate validation for MEF plugins for #1 * Adding ITestWebProvider with canvas apps implementation * Adding Pause() and MEF docs * Docs update * Review update * Docs update * Adding Provider state * Adding ability to specify provider * Testing update * Update README.md --- .gitignore | 1 + README.md | 24 +- docs/Extensions/README.md | 74 +++ .../media/TestEngineOverview-E2E.svg | 1 + docs/Extensions/media/TestEngineOverview.svg | 1 + docs/Extensions/modules.md | 68 +++ docs/PowerFX/Pause.md | 9 + docs/PowerFX/README.md | 49 ++ .../extensions/testPlan-denyCommand.fx.yaml | 28 ++ .../extensions/testPlan-denyModule.fx.yaml | 28 ++ .../testPlan-enableOnlyWriteLine.fx.yaml | 30 ++ samples/extensions/testPlan.fx.yaml | 26 + samples/modules/testPlan.fx.yaml | 25 + samples/pause/testPlan.fx.yaml | 27 ++ .../Config/TestStateTests.cs | 28 +- .../Helpers/ExceptionHandlingHelperTest.cs | 4 +- .../Helpers/LoggingHelpersTest.cs | 38 +- .../Helpers/TestData.cs | 2 +- ...icrosoft.PowerApps.TestEngine.Tests.csproj | 2 + .../TestEngineExtensionCheckerTests.cs | 226 +++++++++ .../Modules/TestEngineModuleMEFLoaderTests.cs | 82 ++++ .../PowerApps/PowerAppsUrlMapperTests.cs | 76 --- .../PowerFx/Functions/AssertFunctionTests.cs | 2 +- .../PowerFx/Functions/SelectFunctionTests.cs | 108 ++--- .../Functions/SetPropertyFunctionTests.cs | 76 +-- .../Functions/SomeOtherUntypedObject.cs | 2 +- .../PowerFx/Functions/WaitFunctionTests.cs | 124 ++--- .../PowerFx/PowerFxEngineTests.cs | 299 ++++++++---- .../PowerFXModel/ControlRecordValueTests.cs | 42 +- .../PowerFXModel/ControlTableSourceTests.cs | 10 +- .../PowerFXModel/ControlTableValueTests.cs | 22 +- .../PowerFXModel/TypeMappingTests.cs | 6 +- .../Reporting/TestReporterTests.cs | 4 +- .../SingleTestRunnerTests.cs | 159 +++--- .../TestEngineTests.cs | 6 +- .../PlaywrightTestInfraFunctionTests.cs | 224 +++++---- .../Users/UserManagerTests.cs | 203 -------- .../Config/BrowserConfiguration.cs | 5 + .../Config/ITestState.cs | 49 ++ .../Config/NetworkRequestMock.cs | 12 + .../Config/TestSettingExtensionSource.cs | 21 + .../Config/TestSettingExtensions.cs | 48 ++ .../Config/TestSettings.cs | 5 + .../Config/TestState.cs | 95 ++++ .../Helpers/ExceptionHandlingHelper.cs | 2 +- .../Helpers/LoggingHelper.cs | 10 +- .../ISingleTestRunner.cs | 1 + .../Microsoft.PowerApps.TestEngine.csproj | 12 +- .../Modules/ITestEngineModule.cs | 19 + .../Modules/MefComponents.cs | 26 + .../Modules/TestEngineExtensionChecker.cs | 458 ++++++++++++++++++ .../Modules/TestEngineModuleMEFLoader.cs | 164 +++++++ .../Modules/TestEngineTrustSource.cs | 46 ++ .../PowerApps/IUrlMapper.cs | 18 - .../PowerApps/PowerAppsUrlMapper.cs | 67 --- .../Select/SelectOneParamFunction.cs | 12 +- .../Select/SelectThreeParamsFunction.cs | 14 +- .../Select/SelectTwoParamsFunction.cs | 14 +- .../PowerFx/Functions/SetPropertyFunction.cs | 12 +- .../PowerFx/Functions/WaitFunction.cs | 2 +- .../PowerFx/IPowerFxEngine.cs | 11 +- .../PowerFx/PowerFxEngine.cs | 89 +++- .../Providers/ITestProviderState.cs | 11 + .../ITestWebProvider.cs} | 48 +- .../{PowerApps => Providers}/ItemPath.cs | 2 +- .../JSControlModel.cs | 2 +- .../{PowerApps => Providers}/JSObjectModel.cs | 2 +- .../JSPropertyModel.cs | 2 +- .../JSPropertyValueModel.cs | 2 +- .../PowerFxModel/ControlRecordValue.cs | 20 +- .../PowerFxModel/ControlTableRowSchema.cs | 2 +- .../PowerFxModel/ControlTableSource.cs | 10 +- .../PowerFxModel/ControlTableValue.cs | 10 +- .../PowerFxModel/TypeMapping.cs | 2 +- .../RecordValueObject.cs | 2 +- .../SingleTestRunner.cs | 72 +-- .../TestEngine.cs | 20 +- .../TestEngineEventHandler.cs | 2 +- .../TestInfra/ITestInfraFunctions.cs | 26 +- .../TestInfra/PlaywrightTestInfraFunctions.cs | 210 ++++---- .../Users/IUserManager.cs | 37 +- .../Users/UserManager.cs | 106 ---- src/PowerAppsTestEngine.sln | 82 ++++ src/PowerAppsTestEngine/InputOptions.cs | 3 + .../PowerAppsTestEngine.csproj | 1 + src/PowerAppsTestEngine/Program.cs | 76 ++- .../Properties/launchSettings.json | 8 + .../PauseFunctionTests.cs | 102 ++++ .../PauseModuleTests.cs | 89 ++++ src/testengine.module.pause.tests/Usings.cs | 1 + .../testengine.module.pause.tests.csproj | 36 ++ src/testengine.module.pause/PauseFunction.cs | 50 ++ src/testengine.module.pause/PauseModule.cs | 36 ++ .../testengine.module.pause.csproj | 51 ++ .../SampleFunction.cs | 18 + src/testengine.module.sample/SampleModule.cs | 34 ++ .../testengine.module.sample.csproj | 43 ++ .../PowerAppFunctionsTest.cs | 110 ++++- .../PowerAppsUrlMapperTests.cs | 28 ++ .../Usings.cs | 1 + .../testengine.provider.canvas.tests.csproj | 33 ++ .../JS/CanvasAppSdk.js | 0 .../JS/PublishedAppTesting.js | 0 .../PowerAppFunctions.cs | 175 ++++--- .../testengine.provider.canvas.csproj | 48 ++ .../BrowserUserManagerModuleTests.cs | 70 +++ src/testengine.user.browser.tests/Usings.cs | 1 + .../testengine.user.browser.tests.csproj | 37 ++ .../BrowserUserManagerModule.cs | 67 +++ .../testengine.user.browser.csproj | 26 + .../EnvironmentUserManagerModuleTests.cs | 354 ++++++++++++++ .../Usings.cs | 1 + .../testengine.user.environment.tests.csproj | 37 ++ .../EnvironmentUserManagerModule.cs | 219 +++++++++ .../testengine.user.environment.csproj | 26 + 115 files changed, 4450 insertions(+), 1349 deletions(-) create mode 100644 docs/Extensions/README.md create mode 100644 docs/Extensions/media/TestEngineOverview-E2E.svg create mode 100644 docs/Extensions/media/TestEngineOverview.svg create mode 100644 docs/Extensions/modules.md create mode 100644 docs/PowerFX/Pause.md create mode 100644 samples/extensions/testPlan-denyCommand.fx.yaml create mode 100644 samples/extensions/testPlan-denyModule.fx.yaml create mode 100644 samples/extensions/testPlan-enableOnlyWriteLine.fx.yaml create mode 100644 samples/extensions/testPlan.fx.yaml create mode 100644 samples/modules/testPlan.fx.yaml create mode 100644 samples/pause/testPlan.fx.yaml create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineModuleMEFLoaderTests.cs delete mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppsUrlMapperTests.cs rename src/Microsoft.PowerApps.TestEngine.Tests/{PowerApps => Provider}/PowerFXModel/ControlRecordValueTests.cs (75%) rename src/Microsoft.PowerApps.TestEngine.Tests/{PowerApps => Provider}/PowerFXModel/ControlTableSourceTests.cs (76%) rename src/Microsoft.PowerApps.TestEngine.Tests/{PowerApps => Provider}/PowerFXModel/ControlTableValueTests.cs (75%) rename src/Microsoft.PowerApps.TestEngine.Tests/{PowerApps => Provider}/PowerFXModel/TypeMappingTests.cs (97%) delete mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/Users/UserManagerTests.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensionSource.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Modules/ITestEngineModule.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Modules/TestEngineTrustSource.cs delete mode 100644 src/Microsoft.PowerApps.TestEngine/PowerApps/IUrlMapper.cs delete mode 100644 src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppsUrlMapper.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Providers/ITestProviderState.cs rename src/Microsoft.PowerApps.TestEngine/{PowerApps/IPowerAppFunctions.cs => Providers/ITestWebProvider.cs} (66%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/ItemPath.cs (95%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/JSControlModel.cs (91%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/JSObjectModel.cs (87%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/JSPropertyModel.cs (81%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/JSPropertyValueModel.cs (88%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/PowerFxModel/ControlRecordValue.cs (90%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/PowerFxModel/ControlTableRowSchema.cs (91%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/PowerFxModel/ControlTableSource.cs (84%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/PowerFxModel/ControlTableValue.cs (69%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/PowerFxModel/TypeMapping.cs (98%) rename src/Microsoft.PowerApps.TestEngine/{PowerApps => Providers}/RecordValueObject.cs (86%) delete mode 100644 src/Microsoft.PowerApps.TestEngine/Users/UserManager.cs create mode 100644 src/PowerAppsTestEngine/Properties/launchSettings.json create mode 100644 src/testengine.module.pause.tests/PauseFunctionTests.cs create mode 100644 src/testengine.module.pause.tests/PauseModuleTests.cs create mode 100644 src/testengine.module.pause.tests/Usings.cs create mode 100644 src/testengine.module.pause.tests/testengine.module.pause.tests.csproj create mode 100644 src/testengine.module.pause/PauseFunction.cs create mode 100644 src/testengine.module.pause/PauseModule.cs create mode 100644 src/testengine.module.pause/testengine.module.pause.csproj create mode 100644 src/testengine.module.sample/SampleFunction.cs create mode 100644 src/testengine.module.sample/SampleModule.cs create mode 100644 src/testengine.module.sample/testengine.module.sample.csproj rename src/{Microsoft.PowerApps.TestEngine.Tests/PowerApps => testengine.provider.canvas.tests}/PowerAppFunctionsTest.cs (92%) create mode 100644 src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs create mode 100644 src/testengine.provider.canvas.tests/Usings.cs create mode 100644 src/testengine.provider.canvas.tests/testengine.provider.canvas.tests.csproj rename src/{Microsoft.PowerApps.TestEngine => testengine.provider.canvas}/JS/CanvasAppSdk.js (100%) rename src/{Microsoft.PowerApps.TestEngine => testengine.provider.canvas}/JS/PublishedAppTesting.js (100%) rename src/{Microsoft.PowerApps.TestEngine/PowerApps => testengine.provider.canvas}/PowerAppFunctions.cs (66%) create mode 100644 src/testengine.provider.canvas/testengine.provider.canvas.csproj create mode 100644 src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs create mode 100644 src/testengine.user.browser.tests/Usings.cs create mode 100644 src/testengine.user.browser.tests/testengine.user.browser.tests.csproj create mode 100644 src/testengine.user.browser/BrowserUserManagerModule.cs create mode 100644 src/testengine.user.browser/testengine.user.browser.csproj create mode 100644 src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs create mode 100644 src/testengine.user.environment.tests/Usings.cs create mode 100644 src/testengine.user.environment.tests/testengine.user.environment.tests.csproj create mode 100644 src/testengine.user.environment/EnvironmentUserManagerModule.cs create mode 100644 src/testengine.user.environment/testengine.user.environment.csproj diff --git a/.gitignore b/.gitignore index a1f516cc8..1075530c0 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ # Power Apps Test Engine files /src/PowerAppsTestEngine/TestOutput /src/PowerAppsTestEngine/config.dev.json +**/TestOutput \ No newline at end of file diff --git a/README.md b/README.md index b90334464..b1419722b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Power Apps Test Engine is an open source project with the aim of providing maker - DOM abstraction - Tests are authored using references to control names that are defined at design-time. Test authors do not need to write JavaScript, and do not need to be familiar with the browser DOM of the app's rendered output. - Connector mocking - Test authors can optionally create mocks of network calls, typically used when Power Apps make calls to connectors. This allows the app to be tested without modification to the app itself while avoiding any unwanted side-effects of the external APIs. - Screenshot and video recording support - Test Engine can take screenshots at any point during your test execution, and records videos of the test run. This can be very helpful to diagnose failed tests and to understand what the actual experience of the failed test case was. +- Managed Extensibility Framework (MEF) - Test Engine can take advantage of defined MEF interfaces to provide authetication, providers and Power FX actions using [extension](./docs/Extensions/README.md) assemblies. Build this project using the instructions below. This will create a local executable that can be used to run tests from your machine. @@ -38,7 +39,7 @@ Run the commands below in PowerShell. These commands will clone the repo to your git clone https://github.com/microsoft/PowerApps-TestEngine.git # Change to the PowerAppsTestEngine folder -cd PowerApps-TestEngine\src\PowerAppsTestEngine +cd PowerApps-TestEngine\src # Build dotnet build @@ -102,9 +103,26 @@ For more information about the config and the inputs to the command, please view ### Set up user authentication -This refers to the account that Test Engine will use to execute the test. +This refers to the account that Test Engine will use to execute the test. Test Engine supports an [Extension Model](./docs/Extensions/README.md) to allow different methods to authenticate the test system account using the UserAuth configuration parameter. -Test Engine does not support multi-factor authentication. Use an account that requires only a username and password to sign in for your tests. +The default UserAuth provider is BrowserContext. + +#### Browser context authentication + +Test Engine using Browser context authentication can support multi-factor authentication. Usinbg this approach an interactive login is first required to successfully authenticate with the Power Platform. + +Once the BrowserContext folder with the authentication tokens is available. You ccan use the Pause sample to generate a **BrowserContext** folder that you can use to login for other tests + +```bash +# Change to the compiled PowerAppsTestEngine folder +cd bin\Debug\PowerAppsTestEngine +# Run the pause sample +dotnet PowerAppsTestEngine.dll -i ..\..\..\samples\pause\testPlan.fx.yaml -e 12345678-1111-2222-3333-444444444444 -t aaaaaaaa-1111-2222-3333-444444444444 +``` + +#### Environment variable authentication + +Test Engine using the environment variable based login does not support multi-factor authentication. Use an account that requires only a username and password to sign in for your tests. Test credentials cannot be stored in test plan files. Rather, they are stored in PowerShell environment variables. The test plan file contains references to which environment variables are used for credentials. For example, the following snippet indicates that the `user1Email` and `user1Password` environment variables will be used: diff --git a/docs/Extensions/README.md b/docs/Extensions/README.md new file mode 100644 index 000000000..1f95e30a6 --- /dev/null +++ b/docs/Extensions/README.md @@ -0,0 +1,74 @@ +# Overview + +Test Engine provides an extension model using [modules](./modules.md). Modules provide the following key benefits: + +- A "no cliffs" extensibility model that code first developer can implement to extend the functionality of the test engine +- An extensible method of defining login credentials for the test session +- A provider module that different web resources can implement to that Power Fx expressions can be used +- An action model that allows new Power Fx test functions to be defined to simplify testing +- A security model where extensions can be signed by a trusted certificate provider +- A allow/deny security model that provides granular control on how actions are implemented + +## Architecture + +The extension model of test engine can be visualized in the following way + +![Overview of Test Engine extension with Security checks for signed assemblies and Allow/Deny and MEF contracts for user authentication, providers and actions](./media/TestEngineOverview.svg) + +### Security Checks + +Where a possible MEF extension does not meet the defined security checks, it will not be made available as part of test engine test runs. + +#### Signed Assemblies + +When using assemblies compiled in release mode the Test Engine will validate that: + +- Assemblies are signed by a trusted Root certificate provider +- Assemblies and intermediate certificates are currently valid. + +#### Allow / Deny List + +As defined in [modules](./modules.md) optional allow deny lists can be used to control the following: + +- Which MEF modules to allow / deny +- Which .Net namespaces are allow / deny +- Which .Net methods and properties and allow / deny. + +Using these settings can provide a granular control of what modules are available and what code can be implemented in the a ITestEngineModule implementations. + +### MEF Extensions + +Test Engine will search the same folder as the Test Engine executables for the following MEF contracts + +- **[IUserManager](..\..\src\Microsoft.PowerApps.TestEngine\Users\IUserManager.cs)** provide implementations to interact with the provider to authenticate the test session. +- **[ITestWebProvider](..\..\src\Microsoft.PowerApps.TestEngine\Providers\ITestWebProvider.cs)** allow extensions to build on the authenticated Playwright session to present the object model of the provider to and from the Power Fx test state. +- **[ITestEngineModule](..\..\src\Microsoft.PowerApps.TestEngine\Modules\ITestEngineModule.cs)** allow extensions to interact with network calls and define Power Fx functions used in a test + +## No Cliffs Extensibility + +The MEF extensibility model provides a method of extending the range of scenarios that Test Engine can cover. By implementing the defined MEF interfaces .Net assemblies can be implemented that provide alternative user authentication and web based tests. + +The ITestEngineModule interface allows new Power FX functions to be defined that simplify testing by extending the provider or adding low code functions that are implemented in .Net. + +## Local Development and ALM Process + +![Overview of end to end test process from local development to hosted build](./media/TestEngineOverview-E2E.svg) + +The end to end process for test engine could be th following: + +1. **Local Development** - Develop tests on local PC. At this stage the permissions of the maker can be used. + +2. **Share Test Assets** - Share the Power Apps with test user account. Provide access to Managed Identity or Service principal to the definition of the Power Apps, Power Automate flows or Co-pilot. + + The user account that is setup with User permissions only. The user account must have created connection id to the required connectors. + +3. **Commit Changes** - Save the changes to source control which will trigger build. + +4. **Run Tests** - For unit tests they should be run with least privilege. Verify that: + + - User accounts only have permissions to start the application + - By default that all Dataverse and Custom Connectors are mocked so that they cannot update source systems + +### Test Types + +Typically the majority of tests should be unit tests. Integration tests should be a smaller percentage of tests as the take longer to create and need to manage the state of system and secure access to secure resources. Finally UI tests conducted manual should be the smalled amount of tests as they require the greatest amount of review and human intervention to run and validate test results. diff --git a/docs/Extensions/media/TestEngineOverview-E2E.svg b/docs/Extensions/media/TestEngineOverview-E2E.svg new file mode 100644 index 000000000..a19ecfe08 --- /dev/null +++ b/docs/Extensions/media/TestEngineOverview-E2E.svg @@ -0,0 +1 @@ +Test EngineLocalDevelopment andALMProcessRun TestsValidate least privilegeUploadresultsQuality gateLocal DevelopmentExecute with userpermissionsRun tests with InteractiveUser LoginShare Test AssetsShare Apps and Flowswith Test UserLeast privileges usersetupSource ControlCommit to SourceControlTriggerBuildUnit TestsMock dependenciesData, Connectors, Triggers ActionsIntegration TestsKnown data stateUI Tests \ No newline at end of file diff --git a/docs/Extensions/media/TestEngineOverview.svg b/docs/Extensions/media/TestEngineOverview.svg new file mode 100644 index 000000000..81cdf8771 --- /dev/null +++ b/docs/Extensions/media/TestEngineOverview.svg @@ -0,0 +1 @@ +Test Engine Extension ModelTest Engine[Export(typeof(IUserManager))][Export(typeof(ITestWebProvider))][Export(typeof(ITestEngineModule))]Security Checks(SignedAssemblies, Allow/Deny)browserenvironmentcanvasPause() \ No newline at end of file diff --git a/docs/Extensions/modules.md b/docs/Extensions/modules.md new file mode 100644 index 000000000..b8dacb796 --- /dev/null +++ b/docs/Extensions/modules.md @@ -0,0 +1,68 @@ +# Test Engine Modules + +Test Engine extensibility modules are an experimental features that is being considered to allow extension of Test Engine tests by making use of [Managed Extensibility Framework (MEF)](https://learn.microsoft.com/en-us/dotnet/framework/mef/) to allow .Net libraries to be created to extend test execution. + +NOTE: This feature is subject to change and review and is very likely to change. + +## Execution + +Execution of any defined plugins is disabled by default and must be enabled in test settings. The extension sample [testPlan.fx.yaml](../../samples/extensions/testPlan.fx.yaml) demonstrates enable extensions to run the Sample PowerFx function. + +By convention testengine.module.*.dll files in the same folder as the Power Apps Test engine folder will be loaded is extenions modules are enabled. Using the [Test Settings Extensions](..\..\src\Microsoft.PowerApps.TestEngine\Config\TestSettingExtensions.cs) you can enable and control allowed plugins and namespaces with the plugins. + +## Getting Started + +To run the sample extension + +1. Ensure that you have compiled the solution. For example to build the debug version of the Test Engine + +```powershell +cd src +dotnet build +``` + +2. Ensure you have correct version of playwright installed + +```powershell +cd ..\bin\PowerAppTestEngine +.\playwright.ps1 install +``` + +3. Get the values for your environment and tenant id from the [Power Apps Portal](http://make.powerapps.com). See [Get the session ID for Power Apps](https://learn.microsoft.com/power-apps/maker/canvas-apps/get-sessionid#get-the-session-id-for-power-apps-makepowerappscom) for more information. + +4. Ensure you have the [button clicker solution](..\..\samples\buttonclicker\ButtonClicker_1_0_0_4.zip) imported into your environment + +5. Update the [config.dev.json](..\..\src\PowerAppsTestEngine\config.dev.json) environment id and tenant id + +5. Run the sample + +```powershell +cd samples\pause +dotnet ..\..\bin\Debug\PowerAppsTestEngine\PowerAppsTestEngine.dll -i testPlan.fx.yaml +``` + +## Exploring Samples + +To enable the Pause PowerFx function the testengine.module.pause library uses the [PauseModule](..\..\src\testengine.module.pause\PauseModule.cs) class to implement a MEF module to register the Power Fx function. + +## Configuring extensions + +The configuration settings allow you to have finer grained control of what modules/actions you want to allow or deny. + +### Deny Module Load + +Extensions have a number of test settings that can be provided to control which extensions can be executed. The [testPlan-denyModule.fx.yaml](..\..\samples\extensions\testPlan-denyModule.fx.yaml) example demonstrates how to deny loading the sample module. When this sample is run it will fail as the Sample() Power Fx function will not be loaded. + +### Deny .Net Namespace + +In some cases you may want to deny specific commands. The [testPlan-denyCommand.fx.yaml](..\..\samples\extensions\testPlan-denyModule.fx.yaml) example demonstrates load of modules that do not match the defined rule. When this sample is run it will fail as the Sample() Power Fx function will not be loaded as it uses System.Console. + +This example can also be extended to specific methods for example System.Conole::WriteLine as part of the namespace rules to deny (or allow) a specific method. The [testPlan-enableOnlyWriteLine.fx.yaml](..\..\samples\extensions\testPlan-enableOnlyWriteLine.fx.yaml) example demonstrates this configuration to to have a passing test. + +## Further Extensions + +Once you have explored Power Fx and Test Settings the following additional areas can be explored: + +- Extending the Network Mocking with a module using [RouteAsync](https://playwright.dev/dotnet/docs/api/class-browsercontext#browser-context-route) to Abort, Fulfill from a local file or Continue + +- Additional extensions could be considered by implementing the defined interfaces an making contributions and pull requests to increase use of Test Engine to cover additional test scenarios. diff --git a/docs/PowerFX/Pause.md b/docs/PowerFX/Pause.md new file mode 100644 index 000000000..599aff545 --- /dev/null +++ b/docs/PowerFX/Pause.md @@ -0,0 +1,9 @@ +# Pause + +`Pause()` + +This will open the interactive Playwright Inspector and wait for the user to resume execution. + +## Example + +`Pause()` diff --git a/docs/PowerFX/README.md b/docs/PowerFX/README.md index 7cdfa007d..76d6edc21 100644 --- a/docs/PowerFX/README.md +++ b/docs/PowerFX/README.md @@ -4,6 +4,55 @@ There are several specifically defined functions for the test framework. - [Assert](./Assert.md) - [Screenshot](./Screenshot.md) +- [Pause](./Pause.md) - [Select](./Select.md) - [SetProperty](./SetProperty.md) - [Wait](./Wait.md) + +## Naming + +When creating additional functions using [modules](../modules.md) for Power Fx in the Test Engine, it's important to follow naming standards to ensure consistency and readability. + +Here are some guidelines for naming your functions in Power Fx: + +1. Use descriptive names that accurately describe the function's purpose. +2. Use PascalCase, where the first letter of each word is capitalized, for function names. +3. Avoid using abbreviations or acronyms unless they are widely understood and commonly used. +4. Use verbs at the beginning of function names to indicate what the function does. +5. Use nouns at the end of function names to indicate what the function operates on. + +By following these naming standards, your Power Fx code will be easier to read and maintain, and other developers will be able to understand your code more easily. + +### Using Descriptive Names + +Using descriptive names is important because it makes it easier for others (and yourself) to understand what the function or service does. A good name should be concise but also convey the function's or service's purpose. For example, instead of naming a function "Calculate," you could name it "CalculateTotalCost" to make it clear what the function is doing. + +Anti-pattern: Using vague or ambiguous names that don't clearly convey the function's or service's purpose. For example, naming a function "ProcessData" doesn't give any indication of what the function actually does. + +### Use Pascal Case + +Using PascalCase is a convention that is widely used in many programming languages, and it helps make your code more readable and consistent. By capitalizing the first letter of each word, you can more easily distinguish between different words in the name. For example, instead of naming a function "calculateTotalcost," you could name it "CalculateTotalCost" to make it easier to read. + +Anti-pattern: Using inconsistent capitalization or other naming conventions. For example, naming a function "Calculate_total_cost" or "calculateTotalCost" would be inconsistent and harder to read. + +### Avoid Abbreviations + +Using abbreviations or acronyms can make it harder for others to understand what your code does, especially if they are not familiar with the specific terminology you are using. If you do use abbreviations or acronyms, make sure they are widely understood and commonly used in your field. For example, using "GUI" for "Graphical User Interface" is a widely understood and commonly used abbreviation. + +Anti-pattern: Using obscure or uncommon abbreviations or acronyms that are not widely understood. For example, using "NLP" for "Natural Language Processing" might be confusing for someone who is not familiar with that term. + +### Use Verbs at start + +Using verbs at the beginning of function names helps to make it clear what the function does. This is especially important when you have many functions that operate on similar data or perform similar tasks. For example, instead of naming a function simply "TotalCost," you could name it "CalculateTotalCost" to indicate that the function calculates the total cost. + +Good practice: Using clear and concise verbs that accurately describe what the function does. For example, using verbs like "calculate," "validate," "filter," or "sort" can help make the function's purpose clear. + +Anti-pattern: Using vague or misleading verbs that don't accurately describe what the function does. For example, using a verb like "execute" or "perform" doesn't give any indication of what the function actually does. + +### Use Nouns at end + +Using nouns at the end of function or service names helps to make it clear what the function or service operates on. For example, instead of naming a function "CalculateTotal" you could name it "CalculateTotalCost" to indicate that the function operates on cost data. + +Good practice: Using clear and concise nouns that accurately describe what the function or service operates on. For example, using nouns like "cost," "customer," "order," or "invoice" can help make the function or service's purpose clear. + +Anti-pattern: Using vague or misleading nouns that don't accurately describe what the function or service operates on. For example, using a noun like "data" or "information" doesn't give any indication of what the function or service actually operates on. diff --git a/samples/extensions/testPlan-denyCommand.fx.yaml b/samples/extensions/testPlan-denyCommand.fx.yaml new file mode 100644 index 000000000..2e3df4a12 --- /dev/null +++ b/samples/extensions/testPlan-denyCommand.fx.yaml @@ -0,0 +1,28 @@ +testSuite: + testSuiteName: Extension example + testSuiteDescription: Demonstrate the use of PowerFx extension + persona: User1 + appLogicalName: new_buttonclicker_0a877 + + testCases: + - testCaseName: Run Sample + testCaseDescription: Test case will fail as the Sample command uses System.Console + testSteps: | + = Sample(); + +testSettings: + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + denyNamespaces: + - System.Console + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password + diff --git a/samples/extensions/testPlan-denyModule.fx.yaml b/samples/extensions/testPlan-denyModule.fx.yaml new file mode 100644 index 000000000..ba767becf --- /dev/null +++ b/samples/extensions/testPlan-denyModule.fx.yaml @@ -0,0 +1,28 @@ +testSuite: + testSuiteName: Extension example + testSuiteDescription: Demonstrate the use of PowerFx extension + persona: User1 + appLogicalName: new_buttonclicker_0a877 + + testCases: + - testCaseName: Run Sample + testCaseDescription: Test case will fail as the Sample Power Fx function is not loaded + testSteps: | + = Sample(); + +testSettings: + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + denyModule: + - sample + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password + diff --git a/samples/extensions/testPlan-enableOnlyWriteLine.fx.yaml b/samples/extensions/testPlan-enableOnlyWriteLine.fx.yaml new file mode 100644 index 000000000..514137bfb --- /dev/null +++ b/samples/extensions/testPlan-enableOnlyWriteLine.fx.yaml @@ -0,0 +1,30 @@ +testSuite: + testSuiteName: Extension example + testSuiteDescription: Demonstrate the use of PowerFx extension + persona: User1 + appLogicalName: new_buttonclicker_0a877 + + testCases: + - testCaseName: Run Sample + testCaseDescription: Test case will pass as WriteLine method has been allowed + testSteps: | + = Sample(); + +testSettings: + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + allowNamespaces: + - System.Console::WriteLine + denyNamespaces: + - System.Console + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password + diff --git a/samples/extensions/testPlan.fx.yaml b/samples/extensions/testPlan.fx.yaml new file mode 100644 index 000000000..66389fb5c --- /dev/null +++ b/samples/extensions/testPlan.fx.yaml @@ -0,0 +1,26 @@ +testSuite: + testSuiteName: Extension example + testSuiteDescription: Demonstrate the use of PowerFx extension + persona: User1 + appLogicalName: new_buttonclicker_0a877 + + testCases: + - testCaseName: Run Sample + testCaseDescription: Run the Sample Command + testSteps: | + = Sample(); + +testSettings: + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password + diff --git a/samples/modules/testPlan.fx.yaml b/samples/modules/testPlan.fx.yaml new file mode 100644 index 000000000..529ea81d8 --- /dev/null +++ b/samples/modules/testPlan.fx.yaml @@ -0,0 +1,25 @@ +testSuite: + testSuiteName: Button Clicker + testSuiteDescription: Verifies that counter increments when the button is clicked + persona: User1 + appLogicalName: new_buttonclicker_0a877 + onTestCaseStart: Screenshot("buttonclicker_onTestCaseStart.png"); + onTestCaseComplete: Select(ResetButton); + onTestSuiteComplete: Screenshot("buttonclicker_onTestSuiteComplete.png"); + + testCases: + - testCaseName: Case1 + testCaseDescription: Run sample action + testSteps: | + = Sample(); + +testSettings: + locale: "en-US" + headless: false + recordVideo: true + enableExtensionModules: true + browserConfigurations: + - browser: Firefox + +environmentVariables: + filePath: ../../samples/environmentVariables.yaml diff --git a/samples/pause/testPlan.fx.yaml b/samples/pause/testPlan.fx.yaml new file mode 100644 index 000000000..2fe3d5a53 --- /dev/null +++ b/samples/pause/testPlan.fx.yaml @@ -0,0 +1,27 @@ +testSuite: + testSuiteName: Pause tests + testSuiteDescription: Pause the browser and open the Playwright Inspector inside the Power App using browser authentication method + persona: User1 + appLogicalName: new_buttonclicker_0a877 + onTestSuiteComplete: Screenshot("pause_onTestSuiteComplete.png"); + + testCases: + - testCaseName: Pause + testCaseDescription: Pause example + testSteps: | + = Pause(); + +testSettings: + headless: false + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: NotNeeded + passwordKey: NotNeeded diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs index 9b02594a1..94be9c184 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs @@ -211,7 +211,7 @@ public void ParseAndSetTestStateThrowsOnNoTestSuiteDefinition() // Act and Arrange var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -230,7 +230,7 @@ public void ParseAndSetTestStateThrowsOnNoNameInTestSuiteDefinition(string testN MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -249,7 +249,7 @@ public void ParseAndSetTestStateThrowsOnNoPersonaInTestSuiteDefinition(string pe MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -271,7 +271,7 @@ public void ParseAndSetTestStateThrowsOnNoAppLogicalNameOrAppIdInTestSuiteDefini MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -307,7 +307,7 @@ public void ParseAndSetTestStateThrowsOnNoTestCaseInTestSuiteDefinition() MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -326,7 +326,7 @@ public void ParseAndSetTestStateThrowsOnNoTestCaseNameInTestCase(string testCase MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -345,7 +345,7 @@ public void ParseAndSetTestStateThrowsOnNoTestStepsInTestCase(string testSteps) MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -362,7 +362,7 @@ public void ParseAndSetTestStateThrowsOnNoTestSettings() MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -379,7 +379,7 @@ public void ParseAndSetTestStateThrowsOnNullBrowserConfigurationInTestSettings() MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -396,7 +396,7 @@ public void ParseAndSetTestStateThrowsOnNoBrowserConfigurationInTestSettings() MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -415,7 +415,7 @@ public void ParseAndSetTestStateThrowsOnNoBrowserInBrowserConfigurationInTestSet MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -439,7 +439,7 @@ public void ParseAndSetTestStateThrowsOnInvalidScreenConfigInBrowserConfiguratio MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } @@ -579,8 +579,8 @@ public void ParseAndSetTestStateThrowsOnMulitpleMissingValues() // setting testcases to null testPlanDefinition.TestSuite.TestCases = null; testPlanDefinition.TestSettings = null; - testPlanDefinition.TestSuite.Persona = Guid.NewGuid().ToString(); - + testPlanDefinition.TestSuite.Persona = Guid.NewGuid().ToString(); + MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); LoggingTestHelper.SetupMock(MockLogger); MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/ExceptionHandlingHelperTest.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/ExceptionHandlingHelperTest.cs index c3f6823c5..0cfc1daa1 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/ExceptionHandlingHelperTest.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/ExceptionHandlingHelperTest.cs @@ -18,9 +18,9 @@ public ExceptionHandlingHelperTest() [Fact] public void CheckIfOutDatedPublishedAppTrue() { - Exception exception= new Exception(ExceptionHandlingHelper.PublishedAppWithoutJSSDKErrorCode); + Exception exception = new Exception(ExceptionHandlingHelper.PublishedAppWithoutJSSDKErrorCode); LoggingTestHelper.SetupMock(MockLogger); - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(exception,MockLogger.Object); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(exception, MockLogger.Object); // Verify the message is logged in this case LoggingTestHelper.VerifyLogging(MockLogger, ExceptionHandlingHelper.PublishedAppWithoutJSSDKMessage, LogLevel.Error, Times.Once()); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/LoggingHelpersTest.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/LoggingHelpersTest.cs index a45f4b58d..18ed7c5ce 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/LoggingHelpersTest.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/LoggingHelpersTest.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; using Moq; using Xunit; @@ -15,16 +15,16 @@ namespace Microsoft.PowerApps.TestEngine.Tests.Helpers public class LoggingHelpersTest { private Mock MockLogger; - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockSingleTestInstanceState; private Mock MockTestEngineEventHandler; public LoggingHelpersTest() - { - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + { + MockTestWebProvider = new Mock(MockBehavior.Strict); MockLogger = new Mock(MockBehavior.Strict); MockSingleTestInstanceState = new Mock(MockBehavior.Strict); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestEngineEventHandler = new Mock(MockBehavior.Strict); LoggingTestHelper.SetupMock(MockLogger); @@ -33,11 +33,11 @@ public LoggingHelpersTest() [Fact] public async Task DebugInfoNullSessionTest() { - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)null)); - var loggingHelper = new LoggingHelper(MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)null)); + var loggingHelper = new LoggingHelper(MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); loggingHelper.DebugInfo(); - MockPowerAppFunctions.Verify(x => x.GetDebugInfo(), Times.Once()); + MockTestWebProvider.Verify(x => x.GetDebugInfo(), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n Debug Info \n------------------------------", LogLevel.Information, Times.Never()); } @@ -47,11 +47,11 @@ public async Task DebugInfoWithSessionTest() var obj = new ExpandoObject(); obj.TryAdd("sessionId", "somesessionId"); - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); - var loggingHelper = new LoggingHelper(MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); + var loggingHelper = new LoggingHelper(MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); loggingHelper.DebugInfo(); - MockPowerAppFunctions.Verify(x => x.GetDebugInfo(), Times.Once()); + MockTestWebProvider.Verify(x => x.GetDebugInfo(), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n Debug Info \n------------------------------", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "sessionId:\tsomesessionId", LogLevel.Information, Times.Once()); } @@ -65,11 +65,11 @@ public async Task DebugInfoReturnDetailsTest() obj.TryAdd("environmentId", "someEnvironmentId"); obj.TryAdd("sessionId", "someSessionId"); - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); - var loggingHelper = new LoggingHelper(MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); + var loggingHelper = new LoggingHelper(MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); loggingHelper.DebugInfo(); - MockPowerAppFunctions.Verify(x => x.GetDebugInfo(), Times.Once()); + MockTestWebProvider.Verify(x => x.GetDebugInfo(), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n Debug Info \n------------------------------", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "appId:\tsomeAppId", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "appVersion:\tsomeAppVersionId", LogLevel.Information, Times.Once()); @@ -86,12 +86,12 @@ public async Task DebugInfoWithNullValuesTest() obj.TryAdd("environmentId", null); obj.TryAdd("sessionId", "someSessionId"); - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)obj)); MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); - var loggingHelper = new LoggingHelper(MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); + var loggingHelper = new LoggingHelper(MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); loggingHelper.DebugInfo(); - MockPowerAppFunctions.Verify(x => x.GetDebugInfo(), Times.Once()); + MockTestWebProvider.Verify(x => x.GetDebugInfo(), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n Debug Info \n------------------------------", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "appId:\tsomeAppId", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "appVersion:\t", LogLevel.Information, Times.Once()); @@ -102,9 +102,9 @@ public async Task DebugInfoWithNullValuesTest() [Fact] public async Task DebugInfoThrowsTest() { - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Throws(new Exception()); ; + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Throws(new Exception()); ; MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); - var loggingHelper = new LoggingHelper(MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); + var loggingHelper = new LoggingHelper(MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestEngineEventHandler.Object); loggingHelper.DebugInfo(); // Verify UserAppException is passed to TestEngineEventHandler diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/TestData.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/TestData.cs index 08e5e9fa3..7c81b725e 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/TestData.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Helpers/TestData.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerFx.Types; namespace Microsoft.PowerApps.TestEngine.Tests.Helpers diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj b/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj index 378ab16a2..e19ad1be6 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj @@ -11,9 +11,11 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs new file mode 100644 index 000000000..2aa458c60 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography.X509Certificates; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Moq; +using Xunit; +using CertificateRequest = System.Security.Cryptography.X509Certificates.CertificateRequest; + +namespace Microsoft.PowerApps.TestEngine.Tests.Modules +{ + public class TestEngineExtensionCheckerTests + { + Mock MockLogger; + string _template; + + public TestEngineExtensionCheckerTests() + { + MockLogger = new Mock(); + _template = @" +#r ""Microsoft.PowerFx.Interpreter.dll"" +#r ""System.ComponentModel.Composition.dll"" +#r ""Microsoft.PowerApps.TestEngine.dll"" +#r ""Microsoft.Playwright.dll"" +using System.Threading.Tasks; +using Microsoft.PowerFx; +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Playwright; +%USING% + +[Export(typeof(ITestEngineModule))] +public class SampleModule : ITestEngineModule +{ + public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings) + { + + } + + public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider webTestProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) + { + var temp = new TestScript(); + } + + public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock) + { + await Task.CompletedTask; + } +} +public class TestScript { + public TestScript() { + %CODE% + } + +}"; + } + + [Theory] + [InlineData("", "System.Console.WriteLine(\"Hello World\");", true, "", "", true)] + [InlineData("", "System.Console.WriteLine(\"Hello World\");", true, "", "System.", false)] // Deny all System namespace + [InlineData("using System;", "Console.WriteLine(\"Hello World\");", true, "System.Console::WriteLine", "System.Console", true)] + [InlineData("using System;", "Console.WriteLine(\"A\");", true, "System.Console::WriteLine(\"A\")", "System.Console::WriteLine", true)] // Allow System.Console.WriteLine only with a argument of A + [InlineData("using System;", "Console.WriteLine(\"B\");", true, "System.Console::WriteLine(\"A\")", "System.Console::WriteLine", false)] // Allow System.Console.WriteLine only with a argument of A - Deny + [InlineData("using System.IO;", @"File.Exists(""c:\\test.txt"");", true, "", "System.IO", false)] // Deny all System.IO + [InlineData("", @"IPage page = null; page.EvaluateAsync(""alert()"").Wait();", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Constructor code - deny + [InlineData("", @"} public string Foo { get { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); return ""a""; }", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Get Property Code deny + [InlineData("", @"} private int _foo; public int Foo { set { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); _foo = value; }", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Set property deny + [InlineData(@"using System; public class Other { private Action Foo {get;set; } = () => { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); }; }", "", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Action deny property + [InlineData(@"using System; public class Other { private Action Foo = () => { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); }; }", "", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Action deny field + [InlineData("using System; public class Other { private string Foo = String.Format(\"A\"); }", "", true, "", "System.String::Format", false)] // Action deny field - Inline Function + [InlineData(@"using System; public class Other { private static Action Foo {get;set; } = () => { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); }; }", "", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Static Action deny property + [InlineData(@"using System; public class Other { private static Action Foo = () => { IPage page = null; page.EvaluateAsync(""alert()"").Wait(); }; }", "", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Static Action deny field + [InlineData(@"using System; public class Other { static Other() { page.EvaluateAsync(""alert()"").Wait(); } static readonly IPage page = null; }", "", true, "", "Microsoft.Playwright.IPage::EvaluateAsync", false)] // Static Constructor + public void IsValid(string usingStatements, string script, bool useTemplate, string allow, string deny, bool expected) + { + var assembly = CompileScript(useTemplate ? _template + .Replace("%USING%", usingStatements) + .Replace("%CODE%", script) : script); + + var checker = new TestEngineExtensionChecker(MockLogger.Object); + checker.GetExtentionContents = (file) => assembly; + + var settings = new TestSettingExtensions() + { + Enable = true, + AllowNamespaces = new List() { allow }, + DenyNamespaces = new List() { deny } + }; + + var result = checker.Validate(settings, "testengine.module.test.dll"); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(false, true, false, "CN=Test", "CN=Test", 0, 1, true)] + [InlineData(false, false, false, "", "", 0, 1, true)] + [InlineData(true, true, true, "CN=Test", "CN=Test", -1, 1, true)] // Valid certificate + [InlineData(true, true, false, "CN=Test", "CN=Test", -1, 1, false)] // Valid certificate but with untrusted root + [InlineData(true, true, true, "CN=Test, O=Match", "CN=Test, O=Match", -1, 1, true)] // Valid certificate with O + [InlineData(true, true, true, "CN=Test, O=Match", "CN=Test, O=Other", -1, 1, false)] // Organization mismatch + [InlineData(true, true, true, "CN=Test, O=Match, S=WA", "CN=Test, O=Match, S=XX", -1, 1, false)] // State mismatch + [InlineData(true, true, true, "CN=Test, O=Match, S=WA, C=US", "CN=Test, O=Match, S=WA, C=XX", -1, 1, false)] // Country mismatch + [InlineData(true, true, true, "CN=Test", "CN=Test", -100, -1, false)] // Expired certificate + public void Verify(bool checkCertificates, bool sign, bool allowUntrustedRoot, string signWith, string trustedSource, int start, int end, bool expectedResult) + { + var assembly = CompileScript("var i = 1;"); + + byte[] dllBytes = assembly; + if (sign) + { + // Generate a key pair + X509Certificate2 certificate = GenerateSelfSignedCertificate(signWith, DateTime.Now.AddDays(start), DateTime.Now.AddDays(end)); + + // Create a ContentInfo object from the DLL bytes + ContentInfo contentInfo = new ContentInfo(dllBytes); + + // Create a SignedCms object + SignedCms signedCms = new SignedCms(contentInfo); + + // Create a CmsSigner object + CmsSigner cmsSigner = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, certificate); + + // Sign the DLL bytes + signedCms.ComputeSignature(cmsSigner); + + // Get the signed bytes + byte[] signedBytes = signedCms.Encode(); + + dllBytes = signedBytes; + } + + var checker = new TestEngineExtensionChecker(MockLogger.Object); + checker.CheckCertificates = () => checkCertificates; + checker.GetExtentionContents = (file) => assembly; + + var settings = new TestSettingExtensions() + { + Enable = true + }; + if (!string.IsNullOrEmpty(trustedSource)) + { + settings.Parameters.Add("TrustedSource", trustedSource); + } + if (allowUntrustedRoot) + { + settings.Parameters.Add("AllowUntrustedRoot", "True"); + } + + var valid = false; + try + { + var tempFile = $"test.deleteme.{Guid.NewGuid()}.dll"; + File.WriteAllBytes(tempFile, dllBytes); + if (checker.Verify(settings, tempFile)) + { + valid = true; + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + Assert.Equal(expectedResult, valid); + } + + // Method to generate a self-signed X509Certificate2 + static X509Certificate2 GenerateSelfSignedCertificate(string subjectName, DateTime validFrom, DateTime validTo) + { + using (RSA rsa = RSA.Create(2048)) // Generate a 2048-bit RSA key + { + var certRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Set certificate properties + certRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + certRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false)); + certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.3") }, false)); + certRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(certRequest.PublicKey, false)); + + // Create the self-signed certificate + X509Certificate2 certificate = certRequest.CreateSelfSigned(validFrom, validTo); + + return certificate; + } + } + + private byte[] CompileScript(string script) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script); + ScriptOptions options = ScriptOptions.Default; + var roslynScript = CSharpScript.Create(script, options); + var compilation = roslynScript.GetCompilation(); + + compilation = compilation.WithOptions(compilation.Options + .WithOptimizationLevel(OptimizationLevel.Release) + .WithOutputKind(OutputKind.DynamicallyLinkedLibrary)); + + using (var assemblyStream = new MemoryStream()) + { + var result = compilation.Emit(assemblyStream); + if (!result.Success) + { + var errors = string.Join(Environment.NewLine, result.Diagnostics.Select(x => x)); + throw new Exception("Compilation errors: " + Environment.NewLine + errors); + } + + GC.Collect(); + return assemblyStream.ToArray(); + } + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineModuleMEFLoaderTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineModuleMEFLoaderTests.cs new file mode 100644 index 000000000..046d26704 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineModuleMEFLoaderTests.cs @@ -0,0 +1,82 @@ +using System.ComponentModel.Composition.Hosting; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Moq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.Modules +{ + public class TestEngineModuleMEFLoaderTests + { + Mock MockLogger; + + public TestEngineModuleMEFLoaderTests() + { + MockLogger = new Mock(); + } + + [Fact] + public void DisabledEmptyCatalog() + { + var setting = new TestSettingExtensions() { Enable = false }; + var loader = new TestEngineModuleMEFLoader(MockLogger.Object); + + var catalog = loader.LoadModules(setting); + + Assert.NotNull(catalog); + Assert.Equal(0, catalog.Catalogs.Count); + } + + [Theory] + [InlineData(false, false, "", "", "", "AssemblyCatalog")] + [InlineData(false, false, "*", "foo", "testengine.module.foo.dll", "AssemblyCatalog")] + [InlineData(false, false, "foo", "*", "testengine.module.foo.dll", "AssemblyCatalog")] + [InlineData(false, false, "Foo", "*", "testengine.module.foo.dll", "AssemblyCatalog")] + [InlineData(false, false, "*", "foo*", "testengine.module.foo1.dll", "AssemblyCatalog")] + [InlineData(true, false, "*", "", "testengine.module.foo.dll", "AssemblyCatalog")] + [InlineData(true, true, "*", "", "testengine.module.foo.dll", "AssemblyCatalog,AssemblyCatalog")] + public void ModuleMatch(bool checkAssemblies, bool checkResult, string allow, string deny, string files, string expected) + { + var setting = new TestSettingExtensions() + { + Enable = true, + CheckAssemblies = checkAssemblies + }; + Mock mockChecker = new Mock(); + + var loader = new TestEngineModuleMEFLoader(MockLogger.Object); + loader.DirectoryGetFiles = (location, pattern) => + { + var searchPattern = Regex.Escape(pattern).Replace(@"\*", ".*?"); + return files.Split(",").Where(f => Regex.IsMatch(f, searchPattern)).ToArray(); + }; + // Use current test assembly as test + loader.LoadAssembly = (file) => new AssemblyCatalog(this.GetType().Assembly); + loader.Checker = mockChecker.Object; + + if (checkAssemblies) + { + mockChecker.Setup(x => x.Validate(It.IsAny(), files)).Returns(checkResult); + } + + if (!string.IsNullOrEmpty(allow)) + { + setting.AllowModule.Add(allow); + } + + if (!string.IsNullOrEmpty(deny)) + { + setting.DenyModule.Add(deny); + } + + var catalog = loader.LoadModules(setting); + + Assert.NotNull(catalog); + Assert.Equal(expected, string.Join(",", catalog.Catalogs.Select(c => c.GetType().Name))); + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppsUrlMapperTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppsUrlMapperTests.cs deleted file mode 100644 index f4d042d11..000000000 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppsUrlMapperTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.Tests.Helpers; -using Moq; -using Xunit; - -namespace Microsoft.PowerApps.TestEngine.Tests.PowerApps -{ - public class PowerAppsUrlMapperTests - { - private Mock MockTestState; - private Mock MockSingleTestInstanceState; - private Mock MockLogger; - - public PowerAppsUrlMapperTests() - { - MockTestState = new Mock(MockBehavior.Strict); - MockSingleTestInstanceState = new Mock(MockBehavior.Strict); - MockLogger = new Mock(MockBehavior.Strict); - } - - [Theory] - [InlineData("myEnvironment", "apps.powerapps.com", "myApp", "appId", "myTenant", "https://apps.powerapps.com/play/e/myEnvironment/an/myApp?tenantId=myTenant&source=testengine&enablePATest=true&patestSDKVersion=0.0.1", "&enablePATest=true&patestSDKVersion=0.0.1")] - [InlineData("myEnvironment", "apps.powerapps.com", "myApp", "appId", "myTenant", "https://apps.powerapps.com/play/e/myEnvironment/an/myApp?tenantId=myTenant&source=testengine", "")] - [InlineData("defaultEnvironment", "apps.test.powerapps.com", "defaultApp", "appId", "defaultTenant", "https://apps.test.powerapps.com/play/e/defaultEnvironment/an/defaultApp?tenantId=defaultTenant&source=testengine", "")] - [InlineData("defaultEnvironment", "apps.powerapps.com", null, "appId", "defaultTenant", "https://apps.powerapps.com/play/e/defaultEnvironment/a/appId?tenantId=defaultTenant&source=testengine", "")] - public void GenerateAppUrlTest(string environmentId, string domain, string appLogicalName, string appId, string tenantId, string expectedAppUrl, string queryParams) - { - MockTestState.Setup(x => x.GetEnvironment()).Returns(environmentId); - MockTestState.Setup(x => x.GetDomain()).Returns(domain); - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(new TestSuiteDefinition() { AppLogicalName = appLogicalName, AppId = appId }); - MockTestState.Setup(x => x.GetTenant()).Returns(tenantId); - var powerAppUrlMapper = new PowerAppsUrlMapper(MockTestState.Object, MockSingleTestInstanceState.Object); - Assert.Equal(expectedAppUrl, powerAppUrlMapper.GenerateTestUrl(domain, queryParams)); - MockTestState.Verify(x => x.GetEnvironment(), Times.Once()); - MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); - MockTestState.Verify(x => x.GetTenant(), Times.Once()); - } - - [Theory] - [InlineData("", "appLogicalName", "appId","tenantId")] - [InlineData(null, "appLogicalName", "appId", "tenantId")] - [InlineData("environmentId", "", "", "tenantId")] - [InlineData("environmentId", null, null, "tenantId")] - [InlineData("environmentId", "appLogicalName", "appId", "")] - [InlineData("environmentId", "appLogicalName", "appId", null)] - public void GenerateLoginUrlThrowsOnInvalidSetupTest(string environmentId, string appLogicalName, string appId, string tenantId) - { - MockTestState.Setup(x => x.GetEnvironment()).Returns(environmentId); - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(new TestSuiteDefinition() { AppLogicalName = appLogicalName, AppId = appId }); - MockTestState.Setup(x => x.GetTenant()).Returns(tenantId); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - var powerAppUrlMapper = new PowerAppsUrlMapper(MockTestState.Object, MockSingleTestInstanceState.Object); - Assert.Throws(() => powerAppUrlMapper.GenerateTestUrl("", "")); - } - - [Fact] - public void GenerateLoginUrlThrowsOnInvalidTestDefinitionTest() - { - TestSuiteDefinition testDefinition = null; - MockTestState.Setup(x => x.GetEnvironment()).Returns("environmentId"); - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testDefinition); - MockTestState.Setup(x => x.GetTenant()).Returns("tenantId"); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - var powerAppUrlMapper = new PowerAppsUrlMapper(MockTestState.Object, MockSingleTestInstanceState.Object); - Assert.Throws(() => powerAppUrlMapper.GenerateTestUrl("", "")); - } - } -} diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertFunctionTests.cs index 8a15a0913..19068fcfc 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertFunctionTests.cs @@ -4,8 +4,8 @@ using System; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; -using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; using Moq; using Xunit; diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs index 337e3aad0..99f74d634 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Tests.Helpers; @@ -19,13 +19,13 @@ namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions { public class SelectFunctionTests { - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockTestState; private Mock MockLogger; public SelectFunctionTests() { - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); MockTestState = new Mock(MockBehavior.Strict); MockLogger = new Mock(MockBehavior.Strict); } @@ -33,9 +33,9 @@ public SelectFunctionTests() [Fact] public void SelectFunctionThrowsOnNullObjectTest() { - var selectOneParamFunction = new SelectOneParamFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); - var selectTwoParamsFunction = new SelectTwoParamsFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); - var selectThreeParamsFunction = new SelectThreeParamsFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); + var selectOneParamFunction = new SelectOneParamFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); + var selectTwoParamsFunction = new SelectTwoParamsFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); + var selectThreeParamsFunction = new SelectThreeParamsFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); Assert.ThrowsAny(() => selectOneParamFunction.Execute(null)); Assert.ThrowsAny(() => selectTwoParamsFunction.Execute(null, null)); @@ -46,9 +46,9 @@ public void SelectFunctionThrowsOnNullObjectTest() public void SelectFunctionThrowsOnNonPowerAppsRecordValuetTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var selectOneParamFunction = new SelectOneParamFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); - var selectTwoParamsFunction = new SelectTwoParamsFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); - var selectThreeParamsFunction = new SelectThreeParamsFunction(MockPowerAppFunctions.Object, () => Task.CompletedTask, MockLogger.Object); + var selectOneParamFunction = new SelectOneParamFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); + var selectTwoParamsFunction = new SelectTwoParamsFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); + var selectThreeParamsFunction = new SelectThreeParamsFunction(MockTestWebProvider.Object, () => Task.CompletedTask, MockLogger.Object); var someOtherRecordValue = new SomeOtherRecordValue(recordType); Assert.ThrowsAny(() => selectOneParamFunction.Execute(someOtherRecordValue)); @@ -59,23 +59,23 @@ public void SelectFunctionThrowsOnNonPowerAppsRecordValuetTest() [Fact] public void SelectOneParamFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + var updaterFunctionCallCount = 0; var updaterFunction = () => { updaterFunctionCallCount++; return Task.CompletedTask; }; - var selectFunction = new SelectOneParamFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); - + var selectFunction = new SelectOneParamFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); + var result = selectFunction.Execute(recordValue); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Successfully finished executing Select function.", LogLevel.Information, Times.Once()); Assert.Equal(1, updaterFunctionCallCount); @@ -84,11 +84,11 @@ public void SelectOneParamFunctionTest() [Fact] public void SelectTwoParamFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Gallery1", RecordType.Empty()); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Gallery1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Gallery1"); var rowOrColumn = NumberValue.New(1.0); var updaterFunctionCallCount = 0; @@ -97,11 +97,11 @@ public void SelectTwoParamFunctionTest() updaterFunctionCallCount++; return Task.CompletedTask; }; - var selectFunction = new SelectTwoParamsFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + var selectFunction = new SelectTwoParamsFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); var result = selectFunction.Execute(recordValue, rowOrColumn); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Successfully finished executing Select function.", LogLevel.Information, Times.Once()); Assert.Equal(1, updaterFunctionCallCount); @@ -110,14 +110,14 @@ public void SelectTwoParamFunctionTest() [Fact] public void SelectThreeParamFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); var parentRecordType = RecordType.Empty().Add("Gallery1", RecordType.Empty()); var childRecordType = RecordType.Empty().Add("Button1", RecordType.Empty()); - var parentValue = new ControlRecordValue(parentRecordType, MockPowerAppFunctions.Object, "Gallery1"); + var parentValue = new ControlRecordValue(parentRecordType, MockTestWebProvider.Object, "Gallery1"); var rowOrColumn = NumberValue.New(1.0); - var childValue = new ControlRecordValue(childRecordType, MockPowerAppFunctions.Object,"Button1"); + var childValue = new ControlRecordValue(childRecordType, MockTestWebProvider.Object, "Button1"); var updaterFunctionCallCount = 0; var updaterFunction = () => @@ -125,11 +125,11 @@ public void SelectThreeParamFunctionTest() updaterFunctionCallCount++; return Task.CompletedTask; }; - var selectFunction = new SelectThreeParamsFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); - + var selectFunction = new SelectThreeParamsFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); + var result = selectFunction.Execute(parentValue, rowOrColumn, childValue); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Successfully finished executing Select function.", LogLevel.Information, Times.Once()); Assert.Equal(1, updaterFunctionCallCount); @@ -139,22 +139,22 @@ public void SelectThreeParamFunctionTest() public void SelectOneParamFunctionFailsTest() { LoggingTestHelper.SetupMock(MockLogger); - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); - + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); + var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); var updaterFunctionCallCount = 0; var updaterFunction = () => { updaterFunctionCallCount++; return Task.CompletedTask; - }; - var selectFunction = new SelectOneParamFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + }; + var selectFunction = new SelectOneParamFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); Assert.ThrowsAny(() => selectFunction.Execute(recordValue)); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Control name: Button1", LogLevel.Trace, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Unable to select control", LogLevel.Error, Times.Once()); @@ -164,11 +164,11 @@ public void SelectOneParamFunctionFailsTest() [Fact] public void SelectTwoParamFunctionFailsTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Gallery1", RecordType.Empty()); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Gallery1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Gallery1"); var rowOrColumn = NumberValue.New(1.0); var updaterFunctionCallCount = 0; @@ -177,19 +177,19 @@ public void SelectTwoParamFunctionFailsTest() updaterFunctionCallCount++; return Task.CompletedTask; }; - var selectFunction = new SelectTwoParamsFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + var selectFunction = new SelectTwoParamsFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); // Testing Scenarios where members(control, row or column) are null Assert.ThrowsAny(() => selectFunction.Execute(null, rowOrColumn)); Assert.ThrowsAny(() => selectFunction.Execute(recordValue, null)); // Adding test where control names are null - recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, null); + recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, null); Assert.ThrowsAny(() => selectFunction.Execute(recordValue, rowOrColumn)); - recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Gallery1"); + recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Gallery1"); Assert.ThrowsAny(() => selectFunction.Execute(recordValue, rowOrColumn)); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == recordValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Control name: Gallery1", LogLevel.Trace, Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Unable to select control", LogLevel.Error, Times.AtLeastOnce()); @@ -199,14 +199,14 @@ public void SelectTwoParamFunctionFailsTest() [Fact] public void SelectThreeParamFunctionFailsTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); LoggingTestHelper.SetupMock(MockLogger); var parentRecordType = RecordType.Empty().Add("Gallery1", RecordType.Empty()); var childRecordType = RecordType.Empty().Add("Button1", RecordType.Empty()); - var parentValue = new ControlRecordValue(parentRecordType, MockPowerAppFunctions.Object, "Gallery1"); + var parentValue = new ControlRecordValue(parentRecordType, MockTestWebProvider.Object, "Gallery1"); var rowOrColumn = NumberValue.New(1.0); - var childValue = new ControlRecordValue(childRecordType, MockPowerAppFunctions.Object, "Button1"); + var childValue = new ControlRecordValue(childRecordType, MockTestWebProvider.Object, "Button1"); var updaterFunctionCallCount = 0; var updaterFunction = () => @@ -214,7 +214,7 @@ public void SelectThreeParamFunctionFailsTest() updaterFunctionCallCount++; return Task.CompletedTask; }; - var selectFunction = new SelectThreeParamsFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + var selectFunction = new SelectThreeParamsFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); // Testing Scenarios where members(parent control, row or column and child control) are null Assert.ThrowsAny(() => selectFunction.Execute(null, rowOrColumn, childValue)); @@ -222,14 +222,14 @@ public void SelectThreeParamFunctionFailsTest() Assert.ThrowsAny(() => selectFunction.Execute(parentValue, rowOrColumn, null)); // Adding test where control names are null - parentValue = new ControlRecordValue(parentRecordType, MockPowerAppFunctions.Object, null); - childValue = new ControlRecordValue(childRecordType, MockPowerAppFunctions.Object, null); + parentValue = new ControlRecordValue(parentRecordType, MockTestWebProvider.Object, null); + childValue = new ControlRecordValue(childRecordType, MockTestWebProvider.Object, null); Assert.ThrowsAny(() => selectFunction.Execute(parentValue, rowOrColumn, childValue)); - parentValue = new ControlRecordValue(parentRecordType, MockPowerAppFunctions.Object, "Gallery1"); - childValue = new ControlRecordValue(childRecordType, MockPowerAppFunctions.Object, "Button1"); + parentValue = new ControlRecordValue(parentRecordType, MockTestWebProvider.Object, "Gallery1"); + childValue = new ControlRecordValue(childRecordType, MockTestWebProvider.Object, "Button1"); Assert.ThrowsAny(() => selectFunction.Execute(parentValue, rowOrColumn, childValue)); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Once()); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "------------------------------\n\n" + "Executing Select function.", LogLevel.Information, Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Control name: Button1", LogLevel.Trace, Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Unable to select control", LogLevel.Error, Times.AtLeastOnce()); @@ -239,7 +239,7 @@ public void SelectThreeParamFunctionFailsTest() [Fact] public void SelectGalleryTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); var parentRecordType = RecordType.Empty().Add("Gallery1", RecordType.Empty()); var childRecordType = RecordType.Empty().Add("Button1", RecordType.Empty()); @@ -269,23 +269,23 @@ public void SelectGalleryTest() }; var recordType = RecordType.Empty().Add("Button1", RecordType.Empty()); - var powerAppControlModel = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1", parentItemPath); + var powerAppControlModel = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1", parentItemPath); - var selectFunction = new SelectOneParamFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + var selectFunction = new SelectOneParamFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); var result = selectFunction.Execute(powerAppControlModel); Assert.IsType(result); // Select gallery item using threeparams select function // `Select(Gallery1, 1, Button1);` - var parentValue = new ControlRecordValue(parentRecordType, MockPowerAppFunctions.Object, "Gallery1"); + var parentValue = new ControlRecordValue(parentRecordType, MockTestWebProvider.Object, "Gallery1"); var rowOrColumn = NumberValue.New(1.0); - var childValue = new ControlRecordValue(childRecordType, MockPowerAppFunctions.Object, "Button1"); + var childValue = new ControlRecordValue(childRecordType, MockTestWebProvider.Object, "Button1"); - var selectthreeParamsFunction = new SelectThreeParamsFunction(MockPowerAppFunctions.Object, updaterFunction, MockLogger.Object); + var selectthreeParamsFunction = new SelectThreeParamsFunction(MockTestWebProvider.Object, updaterFunction, MockLogger.Object); result = selectthreeParamsFunction.Execute(parentValue, rowOrColumn, childValue); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.SelectControlAsync(It.Is((item) => item.ControlName == childValue.Name)), Times.Exactly(2)); Assert.Equal(2, updaterFunctionCallCount); } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs index b506dfde6..cb30d931b 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; @@ -18,13 +18,13 @@ namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions { public class SetPropertyFunctionTests { - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockTestState; private Mock MockLogger; public SetPropertyFunctionTests() { - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); MockTestState = new Mock(MockBehavior.Strict); MockLogger = new Mock(MockBehavior.Strict); } @@ -33,7 +33,7 @@ public SetPropertyFunctionTests() public void SetPropertyFunctionThrowsOnNonPowerAppsRecordValueTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var SetPropertyFunctionString = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var SetPropertyFunctionString = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); var someOtherRecordValue = new SomeOtherRecordValue(recordType); Assert.ThrowsAny(() => SetPropertyFunctionString.Execute(someOtherRecordValue, StringValue.New("Test"), StringValue.New("10"))); @@ -42,16 +42,16 @@ public void SetPropertyFunctionThrowsOnNonPowerAppsRecordValueTest() [Fact] public void SetPropertyFunctionFailsTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - var setPropertyFunctionString = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + var setPropertyFunctionString = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // This tests the SetPropertyAsync returning false and hence throwing exception Assert.ThrowsAny(() => setPropertyFunctionString.Execute(recordValue, StringValue.New("Text"), StringValue.New("5"))); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "5")), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "5")), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, (string)"Error occurred on DataType of type Microsoft.PowerFx.Types.StringValue", LogLevel.Debug, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, (string)"Property name: Text", LogLevel.Trace, Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, (string)"Unable to set property with SetProperty function.", LogLevel.Error, Times.Once()); @@ -60,84 +60,84 @@ public void SetPropertyFunctionFailsTest() [Fact] public void SetPropertyStringFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); // Make setPropertyFunction contain a text component called Button1 var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - var setPropertyFunctionString = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + var setPropertyFunctionString = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // Set the value of Button1's 'Text' property to 5 var result = setPropertyFunctionString.Execute(recordValue, StringValue.New("Text"), StringValue.New("5")); // check to see if the value of Button1's 'Text' property is 5 Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "5")), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "5")), Times.Once()); // Set the value of Button1's 'Text' property to 10 result = setPropertyFunctionString.Execute(recordValue, StringValue.New("Text"), StringValue.New("10")); // check to see if the value of Button1's 'Text' property is 10 Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "10")), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "10")), Times.Once()); // Set the value of Button1's 'Text' property to 'abc' result = setPropertyFunctionString.Execute(recordValue, StringValue.New("Text"), StringValue.New("abc")); // check to see if the value of Button1's 'Text' property is abc Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "abc")), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(stringVal => stringVal.Value == "abc")), Times.Once()); } [Fact] public void SetPropertyNumberFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); // Make setPropertyFunction contain a component called Rating1 var recordType = RecordType.Empty().Add("Value", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Rating1"); - var setPropertyFunction = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Rating1"); + var setPropertyFunction = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // Set the value of Rating1's 'Value' property to 5 var result = setPropertyFunction.Execute(recordValue, StringValue.New("Value"), NumberValue.New(5d)); // check to see if the value of Rating1's 'Value' property is 5 Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(numVal => numVal.Value == 5)), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(numVal => numVal.Value == 5)), Times.Once()); } [Fact] public void SetPropertyBooleanFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); // Make setPropertyFunction contain a component called Toggle1 var recordType = RecordType.Empty().Add("Value", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Toggle1"); - var setPropertyFunction = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Toggle1"); + var setPropertyFunction = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // Set the value of Toggle1's 'Value' property to true var result = setPropertyFunction.Execute(recordValue, StringValue.New("Value"), BooleanValue.New(true)); // check to see if the value of Toggle1's 'Value' property is true Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(boolVal => boolVal.Value == true)), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(boolVal => boolVal.Value == true)), Times.Once()); } [Fact] public void SetPropertyDateFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); // Make setPropertyFunction contain a component called DatePicker1 var recordType = RecordType.Empty().Add("Value", FormulaType.Date); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); - var setPropertyFunction = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); + var setPropertyFunction = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // Set the value of DatePicker1's 'Value' property to the datetime (01/01/2030) var dt = new DateTime(2030, 1, 1, 0, 0, 0); @@ -145,19 +145,19 @@ public void SetPropertyDateFunctionTest() // check to see if the value of DatePicker1's 'Value' property is the correct datetime (01/01/2030) Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(dateVal => dateVal.GetConvertedValue(null) == dt)), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(dateVal => dateVal.GetConvertedValue(null) == dt)), Times.Once()); } [Fact] public void SetPropertyRecordFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); LoggingTestHelper.SetupMock(MockLogger); // Make setPropertyFunction contain a component called Dropdown1 var recordType = RecordType.Empty().Add("Selected", RecordType.Empty()); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Dropdown1"); - var setPropertyFunction = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Dropdown1"); + var setPropertyFunction = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); // Set the value of Dropdown1's 'Selected' property to {"Value":"2"} var pair = new KeyValuePair("Value", StringValue.New("2")); @@ -166,18 +166,18 @@ public void SetPropertyRecordFunctionTest() // check to see if the value of Dropdown1's 'Selected' property is "2" Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(recordVal => ((StringValue)recordVal.GetField("Value")).Value == "2")), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(recordVal => ((StringValue)recordVal.GetField("Value")).Value == "2")), Times.Once()); } [Fact] public void SetPropertyTableFunctionTest() { - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.GetItemCount(It.IsAny())).Returns(2); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.GetItemCount(It.IsAny())).Returns(2); LoggingTestHelper.SetupMock(MockLogger); - var setPropertyFunction = new SetPropertyFunction(MockPowerAppFunctions.Object, MockLogger.Object); + var setPropertyFunction = new SetPropertyFunction(MockTestWebProvider.Object, MockLogger.Object); var control1Name = Guid.NewGuid().ToString(); var control2Name = Guid.NewGuid().ToString(); var control1Type = RecordType.Empty().Add("Value", FormulaType.String); @@ -190,14 +190,14 @@ public void SetPropertyTableFunctionTest() }; var recordType = tableType.ToRecord(); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "ComboBox1"); - var tableSource = new ControlTableSource(MockPowerAppFunctions.Object, itemPath, recordType); - var tableValue = new ControlTableValue(recordType, tableSource, MockPowerAppFunctions.Object); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "ComboBox1"); + var tableSource = new ControlTableSource(MockTestWebProvider.Object, itemPath, recordType); + var tableValue = new ControlTableValue(recordType, tableSource, MockTestWebProvider.Object); var result = setPropertyFunction.Execute(recordValue, StringValue.New("SelectedItems"), tableValue); Assert.IsType(result); Assert.Equal(2, tableSource.Count); - MockPowerAppFunctions.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(tableVal => tableVal.Count() == 2)), Times.Once()); + MockTestWebProvider.Verify(x => x.SetPropertyAsync(It.Is((item) => item.ControlName == recordValue.Name), It.Is(tableVal => tableVal.Count() == 2)), Times.Once()); } } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SomeOtherUntypedObject.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SomeOtherUntypedObject.cs index e281b3c91..b6a68f992 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SomeOtherUntypedObject.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SomeOtherUntypedObject.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Collections.Generic; using Microsoft.PowerFx.Types; using NotImplementedException = System.NotImplementedException; -using System.Collections.Generic; namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions { diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs index 060c5aad0..8fadc932b 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; @@ -19,7 +19,7 @@ namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions { public class WaitFunctionTests { - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockTestState; private Mock MockLogger; @@ -28,7 +28,7 @@ public class WaitFunctionTests public WaitFunctionTests() { Timeout = 30000; - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); MockTestState = new Mock(MockBehavior.Strict); MockLogger = new Mock(MockBehavior.Strict); } @@ -39,7 +39,7 @@ public void WaitFunctionStringThrowsOnInvalidArgumentsTest() LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Text", FormulaType.String); var waitFunction = new WaitFunctionString(Timeout, MockLogger.Object); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); Assert.Throws(() => waitFunction.Execute(null, FormulaValue.New("Text"), FormulaValue.New("1"))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), null, FormulaValue.New("1"))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), FormulaValue.New("Text"), null)); @@ -52,7 +52,7 @@ public void WaitFunctionNumberThrowsOnInvalidArgumentsTest() LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Text", FormulaType.Number); var waitFunction = new WaitFunctionNumber(Timeout, MockLogger.Object); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); Assert.Throws(() => waitFunction.Execute(null, FormulaValue.New("Text"), FormulaValue.New(1d))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), null, FormulaValue.New(1d))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), FormulaValue.New("Text"), null)); @@ -65,7 +65,7 @@ public void WaitFunctionBooleanThrowsOnInvalidArgumentsTest() LoggingTestHelper.SetupMock(MockLogger); var recordType = RecordType.Empty().Add("Text", FormulaType.Boolean); var waitFunction = new WaitFunctionBoolean(Timeout, MockLogger.Object); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); Assert.Throws(() => waitFunction.Execute(null, FormulaValue.New("Text"), FormulaValue.New(false))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), null, FormulaValue.New(false))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), FormulaValue.New("Text"), null)); @@ -79,7 +79,7 @@ public void WaitFunctionDateThrowsOnInvalidArgumentsTest() var value = new DateTime(1970, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("Text", FormulaType.Date); var waitFunction = new WaitFunctionDate(Timeout, MockLogger.Object); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); Assert.Throws(() => waitFunction.Execute(null, FormulaValue.New("Text"), FormulaValue.NewDateOnly(value.Date))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), null, FormulaValue.NewDateOnly(value.Date))); Assert.Throws(() => waitFunction.Execute(new SomeOtherRecordValue(recordType), FormulaValue.New("Text"), null)); @@ -92,7 +92,7 @@ public void WaitFunctionStringSucceedsTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = "1"; var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor, @@ -102,14 +102,14 @@ public void WaitFunctionStringSucceedsTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionString(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), FormulaValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -118,7 +118,7 @@ public void WaitFunctionImproperValueForStringTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = true; var recordType = RecordType.Empty().Add("Value", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Toggle1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Toggle1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString(), @@ -128,7 +128,7 @@ public void WaitFunctionImproperValueForStringTest() ControlName = "Toggle1", PropertyName = "Value" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); @@ -144,7 +144,7 @@ public void WaitFunctionNumberSucceedsTest() var valueToWaitFor = 1d; var recordType = RecordType.Empty().Add("Text", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString("G"), @@ -154,14 +154,14 @@ public void WaitFunctionNumberSucceedsTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionNumber(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), FormulaValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -170,7 +170,7 @@ public void WaitFunctionImproperValueForNumberTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = true; var recordType = RecordType.Empty().Add("Value", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Toggle1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Toggle1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString(), @@ -180,7 +180,7 @@ public void WaitFunctionImproperValueForNumberTest() ControlName = "Toggle1", PropertyName = "Value" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); @@ -193,7 +193,7 @@ public void WaitFunctionBooleanSucceedsTest() { var valueToWaitFor = false; var recordType = RecordType.Empty().Add("Text", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString(), @@ -203,7 +203,7 @@ public void WaitFunctionBooleanSucceedsTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); LoggingTestHelper.SetupMock(MockLogger); @@ -211,7 +211,7 @@ public void WaitFunctionBooleanSucceedsTest() var waitFunction = new WaitFunctionBoolean(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), BooleanValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -221,7 +221,7 @@ public void WaitFunctionImproperValueForBooleanTest() var valueToWaitFor = 1; var recordType = RecordType.Empty().Add("Text", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString(), @@ -231,7 +231,7 @@ public void WaitFunctionImproperValueForBooleanTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); @@ -245,7 +245,7 @@ public void WaitFunctionDate_DateValueSucceedsTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("SelectedDate", FormulaType.Date); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = value.ToString(), @@ -255,14 +255,14 @@ public void WaitFunctionDate_DateValueSucceedsTest() ControlName = "DatePicker1", PropertyName = "SelectedDate" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionDate(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("SelectedDate"), FormulaValue.NewDateOnly(value.Date)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -271,7 +271,7 @@ public void WaitFunctionDate_DateTimeValueSucceedsTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("DefaultDate", FormulaType.DateTime); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = value.ToString(), @@ -281,14 +281,14 @@ public void WaitFunctionDate_DateTimeValueSucceedsTest() ControlName = "DatePicker1", PropertyName = "DefaultDate" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionDate(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("DefaultDate"), FormulaValue.NewDateOnly(value)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -299,7 +299,7 @@ public void WaitFunctionImproperValueForDateTest() var valueToWaitFor = 1; var recordType = RecordType.Empty().Add("Text", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = valueToWaitFor.ToString(), @@ -309,7 +309,7 @@ public void WaitFunctionImproperValueForDateTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); @@ -323,7 +323,7 @@ public void WaitFunctionDateTime_DateTimeValueSucceedsTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("DefaultDate", FormulaType.DateTime); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = value.ToString(), @@ -333,14 +333,14 @@ public void WaitFunctionDateTime_DateTimeValueSucceedsTest() ControlName = "DatePicker1", PropertyName = "DefaultDate" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionDateTime(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("DefaultDate"), FormulaValue.New(value)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -349,7 +349,7 @@ public void WaitFunctionDateTime_DateValueSucceedsTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("SelectedDate", FormulaType.Date); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = value.ToString(), @@ -359,14 +359,14 @@ public void WaitFunctionDateTime_DateValueSucceedsTest() ControlName = "DatePicker1", PropertyName = "SelectedDate" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); MockTestState.Setup(x => x.GetTimeout()).Returns(Timeout); var waitFunction = new WaitFunctionDateTime(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("SelectedDate"), FormulaValue.New(value.Date)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(2)); } [Fact] @@ -375,7 +375,7 @@ public void WaitFunctionStringWaitsForValueToUpdateTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = "1"; var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", @@ -389,7 +389,7 @@ public void WaitFunctionStringWaitsForValueToUpdateTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(finalJsPropertyValueModel)); @@ -398,7 +398,7 @@ public void WaitFunctionStringWaitsForValueToUpdateTest() var waitFunction = new WaitFunctionString(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), FormulaValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); } [Fact] @@ -407,7 +407,7 @@ public void WaitFunctionNumberWaitsForValueToUpdateTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = 1d; var recordType = RecordType.Empty().Add("Text", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", @@ -421,7 +421,7 @@ public void WaitFunctionNumberWaitsForValueToUpdateTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(finalJsPropertyValueModel)); @@ -430,7 +430,7 @@ public void WaitFunctionNumberWaitsForValueToUpdateTest() var waitFunction = new WaitFunctionNumber(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), FormulaValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); } [Fact] @@ -439,7 +439,7 @@ public void WaitFunctionBooleanWaitsForValueToUpdateTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = false; var recordType = RecordType.Empty().Add("Text", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "true", @@ -453,7 +453,7 @@ public void WaitFunctionBooleanWaitsForValueToUpdateTest() ControlName = "Label1", PropertyName = "Text" }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(finalJsPropertyValueModel)); @@ -462,7 +462,7 @@ public void WaitFunctionBooleanWaitsForValueToUpdateTest() var waitFunction = new WaitFunctionBoolean(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("Text"), BooleanValue.New(valueToWaitFor)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); } [Fact] @@ -471,7 +471,7 @@ public void WaitFunctionDateWaitsForValueToUpdateTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("SelectedDate", FormulaType.Date); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", @@ -485,7 +485,7 @@ public void WaitFunctionDateWaitsForValueToUpdateTest() ControlName = "DatePicker1", PropertyName = "SelectedDate" }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(finalJsPropertyValueModel)); @@ -494,7 +494,7 @@ public void WaitFunctionDateWaitsForValueToUpdateTest() var waitFunction = new WaitFunctionDate(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("SelectedDate"), FormulaValue.NewDateOnly(value.Date)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); } [Fact] @@ -503,7 +503,7 @@ public void WaitFunctionDateTimeWaitsForValueToUpdateTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("DefaultDate", FormulaType.DateTime); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", @@ -517,7 +517,7 @@ public void WaitFunctionDateTimeWaitsForValueToUpdateTest() ControlName = "DatePicker1", PropertyName = "DefaultDate" }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(finalJsPropertyValueModel)); @@ -526,7 +526,7 @@ public void WaitFunctionDateTimeWaitsForValueToUpdateTest() var waitFunction = new WaitFunctionDateTime(Timeout, MockLogger.Object); waitFunction.Execute(recordValue, FormulaValue.New("DefaultDate"), FormulaValue.New(value)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == expectedItemPath.ControlName && itemPath.PropertyName == expectedItemPath.PropertyName)), Times.Exactly(3)); } [Fact] @@ -535,12 +535,12 @@ public void WaitFunctionStringTimeoutTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = "1"; var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); @@ -556,13 +556,13 @@ public void WaitFunctionNumberTimeoutTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = 1d; var recordType = RecordType.Empty().Add("Text", FormulaType.Number); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); @@ -579,14 +579,14 @@ public void WaitFunctionBooleanTimeoutTest() LoggingTestHelper.SetupMock(MockLogger); var valueToWaitFor = false; var recordType = RecordType.Empty().Add("Text", FormulaType.Boolean); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "true", }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); @@ -602,13 +602,13 @@ public void WaitFunctionDateTimeoutTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("SelectedDate", FormulaType.Date); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); @@ -624,13 +624,13 @@ public void WaitFunctionDateTimeTimeoutTest() LoggingTestHelper.SetupMock(MockLogger); var value = new DateTime(2030, 1, 1, 0, 0, 0); var recordType = RecordType.Empty().Add("DefaultDate", FormulaType.DateTime); - var recordValue = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "DatePicker1"); + var recordValue = new ControlRecordValue(recordType, MockTestWebProvider.Object, "DatePicker1"); var jsPropertyValueModel = new JSPropertyValueModel() { PropertyValue = "0", }; - MockPowerAppFunctions.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) + MockTestWebProvider.SetupSequence(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)) .Returns(JsonConvert.SerializeObject(jsPropertyValueModel)); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs index 8b8147d61..0b9d81e54 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs @@ -4,16 +4,20 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Reflection.Emit; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Logging; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Moq; using Newtonsoft.Json; @@ -25,19 +29,19 @@ public class PowerFxEngineTests { private Mock MockTestInfraFunctions; private Mock MockTestState; - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockFileSystem; private Mock MockSingleTestInstanceState; - private Mock MockLogger; + private Mock MockLogger; public PowerFxEngineTests() { MockTestInfraFunctions = new Mock(MockBehavior.Strict); MockTestState = new Mock(MockBehavior.Strict); - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); MockFileSystem = new Mock(MockBehavior.Strict); MockSingleTestInstanceState = new Mock(MockBehavior.Strict); - MockLogger = new Mock(MockBehavior.Strict); + MockLogger = new Mock(MockBehavior.Strict); MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestState.Setup(x => x.GetTimeout()).Returns(30000); LoggingTestHelper.SetupMock(MockLogger); @@ -46,14 +50,17 @@ public PowerFxEngineTests() [Fact] public void SetupDoesNotThrow() { - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); } [Fact] public void ExecuteThrowsOnNoSetupTest() { - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); Assert.Throws(() => powerFxEngine.Execute("", It.IsAny())); LoggingTestHelper.VerifyLogging(MockLogger, "Engine is null, make sure to call Setup first", LogLevel.Error, Times.Once()); } @@ -61,7 +68,7 @@ public void ExecuteThrowsOnNoSetupTest() [Fact] public void UpdatePowerFxModelAsyncThrowsOnNoSetupTest() { - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); Assert.ThrowsAsync(() => powerFxEngine.UpdatePowerFxModelAsync()); LoggingTestHelper.VerifyLogging(MockLogger, "Engine is null, make sure to call Setup first", LogLevel.Error, Times.Once()); } @@ -70,15 +77,16 @@ public void UpdatePowerFxModelAsyncThrowsOnNoSetupTest() public async void UpdatePowerFxModelAsyncThrowsOnCantGetAppStatusTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var button1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(false)); + var button1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(false)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await Assert.ThrowsAsync(() => powerFxEngine.UpdatePowerFxModelAsync()); LoggingTestHelper.VerifyLogging(MockLogger, "Something went wrong when Test Engine tried to get App status.", LogLevel.Error, Times.Once()); @@ -87,52 +95,65 @@ public async void UpdatePowerFxModelAsyncThrowsOnCantGetAppStatusTest() [Fact] public async void RunRequirementsCheckAsyncTest() { - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.CompletedTask); - MockPowerAppFunctions.Setup(x => x.TestEngineReady()).Returns(Task.FromResult(true)); + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.TestEngineReady()).Returns(Task.FromResult(true)); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.RunRequirementsCheckAsync(); - MockPowerAppFunctions.Verify(x => x.CheckAndHandleIfLegacyPlayerAsync(), Times.Once()); - MockPowerAppFunctions.Verify(x => x.TestEngineReady(), Times.Once()); + MockTestWebProvider.Verify(x => x.CheckProviderAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.TestEngineReady(), Times.Once()); } [Fact] public async void RunRequirementsCheckAsyncThrowsOnCheckAndHandleIfLegacyPlayerTest() { - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Throws(new Exception()); - MockPowerAppFunctions.Setup(x => x.TestEngineReady()).Returns(Task.FromResult(true)); + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Throws(new Exception()); + MockTestWebProvider.Setup(x => x.TestEngineReady()).Returns(Task.FromResult(true)); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await Assert.ThrowsAsync(() => powerFxEngine.RunRequirementsCheckAsync()); - MockPowerAppFunctions.Verify(x => x.CheckAndHandleIfLegacyPlayerAsync(), Times.Once()); - MockPowerAppFunctions.Verify(x => x.TestEngineReady(), Times.Never()); + MockTestWebProvider.Verify(x => x.CheckProviderAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.TestEngineReady(), Times.Never()); } [Fact] public async void RunRequirementsCheckAsyncThrowsOnTestEngineReadyTest() { - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.CompletedTask); - MockPowerAppFunctions.Setup(x => x.TestEngineReady()).Throws(new Exception()); + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.TestEngineReady()).Throws(new Exception()); + + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await Assert.ThrowsAsync(() => powerFxEngine.RunRequirementsCheckAsync()); - MockPowerAppFunctions.Verify(x => x.CheckAndHandleIfLegacyPlayerAsync(), Times.Once()); - MockPowerAppFunctions.Verify(x => x.TestEngineReady(), Times.Once()); + MockTestWebProvider.Verify(x => x.CheckProviderAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.TestEngineReady(), Times.Once()); } [Fact] public void ExecuteOneFunctionTest() { - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestState.Setup(x => x.GetTestSettings()).Returns(null); powerFxEngine.Setup(); var result = powerFxEngine.Execute("1+1", new CultureInfo("en-US")); Assert.Equal(2, ((NumberValue)result).Value); @@ -146,8 +167,12 @@ public void ExecuteOneFunctionTest() [Fact] public void ExecuteMultipleFunctionsTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + var powerFxExpression = "1+1; //some comment \n 2+2;\n Concatenate(\"hello\", \"world\");"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestState.Setup(x => x.GetTestSettings()).Returns(null); powerFxEngine.Setup(); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.Equal("helloworld", ((StringValue)result).Value); @@ -161,17 +186,20 @@ public void ExecuteMultipleFunctionsTest() [Fact] public void ExecuteMultipleFunctionsWithDifferentLocaleTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + // en-US locale var culture = new CultureInfo("en-US"); var enUSpowerFxExpression = "1+1;2+2;"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); var enUSResult = powerFxEngine.Execute(enUSpowerFxExpression, culture); // fr locale culture = new CultureInfo("fr"); var frpowerFxExpression = "1+1;;2+2;;"; - powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); var frResult = powerFxEngine.Execute(frpowerFxExpression, culture); @@ -186,8 +214,8 @@ public void ExecuteMultipleFunctionsWithDifferentLocaleTest() public async Task ExecuteWithVariablesTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var label1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); - var label2 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label2"); + var label1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); + var label2 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label2"); var powerFxExpression = "Concatenate(Text(Label1.Text), Text(Label2.Text))"; var label1Text = "Hello"; var label2Text = "World"; @@ -210,42 +238,46 @@ public async Task ExecuteWithVariablesTest() PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == "Label1"))) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == "Label1"))) .Returns(JsonConvert.SerializeObject(label1JsProperty)); - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == "Label2"))) + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == "Label2"))) .Returns(JsonConvert.SerializeObject(label2JsProperty)); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 }, { "Label2", label2 } })); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 }, { "Label2", label2 } })); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.Equal($"{label1Text}{label2Text}", ((StringValue)result).Value); LoggingTestHelper.VerifyLogging(MockLogger, $"Attempting:\n\n{{\n{powerFxExpression}}}", LogLevel.Trace, Times.Once()); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label1ItemPath.ControlName && itemPath.PropertyName == label1ItemPath.PropertyName)), Times.Once()); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label2ItemPath.ControlName && itemPath.PropertyName == label2ItemPath.PropertyName)), Times.Once()); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label1ItemPath.ControlName && itemPath.PropertyName == label1ItemPath.PropertyName)), Times.Once()); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label2ItemPath.ControlName && itemPath.PropertyName == label2ItemPath.PropertyName)), Times.Once()); result = powerFxEngine.Execute($"={powerFxExpression}", It.IsAny()); Assert.Equal($"{label1Text}{label2Text}", ((StringValue)result).Value); LoggingTestHelper.VerifyLogging(MockLogger, $"Attempting:\n\n{{\n{powerFxExpression}}}", LogLevel.Trace, Times.Exactly(2)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label1ItemPath.ControlName && itemPath.PropertyName == label1ItemPath.PropertyName)), Times.Exactly(2)); - MockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label2ItemPath.ControlName && itemPath.PropertyName == label2ItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label1ItemPath.ControlName && itemPath.PropertyName == label1ItemPath.PropertyName)), Times.Exactly(2)); + MockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((itemPath) => itemPath.ControlName == label2ItemPath.ControlName && itemPath.PropertyName == label2ItemPath.PropertyName)), Times.Exactly(2)); } [Fact] public void ExecuteFailsWhenPowerFXThrowsTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + var powerFxExpression = "someNonExistentPowerFxFunction(1, 2, 3)"; - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary())); - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary())); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); Assert.ThrowsAsync(async () => await powerFxEngine.ExecuteWithRetryAsync(powerFxExpression, It.IsAny())); } @@ -253,8 +285,11 @@ public void ExecuteFailsWhenPowerFXThrowsTest() [Fact] public void ExecuteFailsWhenUsingNonExistentVariableTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + var powerFxExpression = "Concatenate(Label1.Text, Label2.Text)"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); Assert.ThrowsAsync(async () => await powerFxEngine.ExecuteWithRetryAsync(powerFxExpression, It.IsAny())); } @@ -262,8 +297,11 @@ public void ExecuteFailsWhenUsingNonExistentVariableTest() [Fact] public void ExecuteAssertFunctionTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + var powerFxExpression = "Assert(1+1=2, \"Adding 1 + 1\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.IsType(result); @@ -275,11 +313,14 @@ public void ExecuteAssertFunctionTest() [Fact] public async Task ExecuteScreenshotFunctionTest() { + MockTestState.Setup(x => x.GetTestSettings()).Returns(new TestSettings()); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns("C:\\testResults"); MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny())).Returns(true); MockTestInfraFunctions.Setup(x => x.ScreenshotAsync(It.IsAny())).Returns(Task.CompletedTask); var powerFxExpression = "Screenshot(\"1.jpg\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.IsType(result); @@ -292,45 +333,47 @@ public async Task ExecuteScreenshotFunctionTest() public async Task ExecuteSelectFunctionTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var button1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + var button1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "Select(Button1)"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); await powerFxEngine.ExecuteWithRetryAsync(powerFxExpression, It.IsAny()); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Exactly(3)); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Exactly(3)); } [Fact] public async Task ExecuteSelectFunctionFailingTest() { - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(false)); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var button1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + var button1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); var powerFxExpression = "Select(Button1)"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); Assert.ThrowsAny(() => powerFxEngine.Execute(powerFxExpression, It.IsAny())); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } [Fact] @@ -338,74 +381,77 @@ public async Task ExecuteSelectFunctionThrowsOnDifferentRecordTypeTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); var otherRecordType = RecordType.Empty().Add("Foo", FormulaType.String); - var button1 = new ControlRecordValue(otherRecordType, MockPowerAppFunctions.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + var button1 = new ControlRecordValue(otherRecordType, MockTestWebProvider.Object, "Button1"); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "Select(Button1)"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); Assert.ThrowsAny(() => powerFxEngine.Execute(powerFxExpression, It.IsAny())); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } [Fact] public async Task ExecuteSetPropertyFunctionTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var button1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Button1"); + var button1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.SetPropertyAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "SetProperty(Button1.Text, \"10\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); await powerFxEngine.ExecuteWithRetryAsync(powerFxExpression, It.IsAny()); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } [Fact] public async Task ExecuteSetPropertyFunctionThrowsOnDifferentRecordTypeTest() { var wrongRecordType = RecordType.Empty().Add("Foo", FormulaType.String); - var button1 = new ControlRecordValue(wrongRecordType, MockPowerAppFunctions.Object, "Button1"); + var button1 = new ControlRecordValue(wrongRecordType, MockTestWebProvider.Object, "Button1"); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Button1", button1 } })); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "SetProperty(Button1.Text, \"10\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); Assert.ThrowsAny(() => powerFxEngine.Execute(powerFxExpression, It.IsAny())); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } [Fact] public async Task ExecuteWaitFunctionTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); - var label1 = new ControlRecordValue(recordType, MockPowerAppFunctions.Object, "Label1"); + var label1 = new ControlRecordValue(recordType, MockTestWebProvider.Object, "Label1"); var label1Text = "1"; var label1JsProperty = new JSPropertyValueModel() { @@ -417,23 +463,24 @@ public async Task ExecuteWaitFunctionTest() PropertyName = "Text" }; - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((input) => itemPath.ControlName == input.ControlName && itemPath.PropertyName == input.PropertyName))) + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((input) => itemPath.ControlName == input.ControlName && itemPath.PropertyName == input.PropertyName))) .Returns(JsonConvert.SerializeObject(label1JsProperty)); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 } })); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 } })); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "Wait(Label1, \"Text\", \"1\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); await powerFxEngine.ExecuteWithRetryAsync(powerFxExpression, It.IsAny()); var result = powerFxEngine.Execute(powerFxExpression, It.IsAny()); Assert.IsType(result); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } [Fact] @@ -441,33 +488,33 @@ public async Task ExecuteWaitFunctionThrowsOnDifferentRecordTypeTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String); var otherRecordType = RecordType.Empty().Add("Foo", FormulaType.String); - var label1 = new ControlRecordValue(otherRecordType, MockPowerAppFunctions.Object, "Label1"); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 } })); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); + var label1 = new ControlRecordValue(otherRecordType, MockTestWebProvider.Object, "Label1"); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { { "Label1", label1 } })); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); var testSettings = new TestSettings() { Timeout = 3000 }; MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); var powerFxExpression = "Wait(Label1, \"Text\", \"1\")"; - var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); powerFxEngine.Setup(); await powerFxEngine.UpdatePowerFxModelAsync(); Assert.ThrowsAny(() => powerFxEngine.Execute(powerFxExpression, It.IsAny())); - MockPowerAppFunctions.Verify(x => x.LoadPowerAppsObjectModelAsync(), Times.Once()); + MockTestWebProvider.Verify(x => x.LoadObjectModelAsync(), Times.Once()); } - [Fact] public async Task TestStepByStep() { // Arrange var powerFxEngine = GetPowerFxEngine(); int updateCounter = 0; var otherRecordType = RecordType.Empty().Add("Foo", FormulaType.String); - var label1 = new ControlRecordValue(otherRecordType, MockPowerAppFunctions.Object, "Label1"); - var label2 = new ControlRecordValue(otherRecordType, MockPowerAppFunctions.Object, "Label2"); - var label3 = new ControlRecordValue(otherRecordType, MockPowerAppFunctions.Object, "Label3"); - MockPowerAppFunctions.Setup(x => x.LoadPowerAppsObjectModelAsync()).Returns(() => + var label1 = new ControlRecordValue(otherRecordType, MockTestWebProvider.Object, "Label1"); + var label2 = new ControlRecordValue(otherRecordType, MockTestWebProvider.Object, "Label2"); + var label3 = new ControlRecordValue(otherRecordType, MockTestWebProvider.Object, "Label3"); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(() => { if (updateCounter == 0) { @@ -484,9 +531,9 @@ public async Task TestStepByStep() return Task.FromResult(new Dictionary() { { "Label3", label3 } }); } }); - MockPowerAppFunctions.Setup(x => x.CheckAndHandleIfLegacyPlayerAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.CheckIfAppIsIdleAsync()).Returns(Task.FromResult(true)); - MockPowerAppFunctions.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.SelectControlAsync(It.IsAny())).Returns(Task.FromResult(true)); var oldUICulture = CultureInfo.CurrentUICulture; var frenchCulture = new CultureInfo("fr"); @@ -522,7 +569,47 @@ public async Task TestStepByStep() private PowerFxEngine GetPowerFxEngine() { - return new PowerFxEngine(MockTestInfraFunctions.Object, MockPowerAppFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + return new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + } + + [Fact] + public async Task ExecuteFooFromModuleFunction() + { + var testSettings = new TestSettings() { ExtensionModules = new TestSettingExtensions { Enable = true } }; + + var mockModule = new Mock(); + var modules = new List() { mockModule.Object }; + + mockModule.Setup(x => x.RegisterPowerFxFunction(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((PowerFxConfig powerFxConfig, ITestInfraFunctions functions, ITestWebProvider apps, ISingleTestInstanceState instanceState, ITestState state, IFileSystem fileSystem) => + { + powerFxConfig.AddFunction(new FooFunction()); + }); + + MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(modules); + + MockTestWebProvider.Setup(x => x.CheckProviderAsync()).Returns(Task.CompletedTask); + MockTestWebProvider.Setup(x => x.CheckIsIdleAsync()).Returns(Task.FromResult(true)); + MockTestWebProvider.Setup(x => x.LoadObjectModelAsync()).Returns(Task.FromResult(new Dictionary() { })); + + var powerFxExpression = "Foo()"; + var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + powerFxEngine.Setup(); + await powerFxEngine.UpdatePowerFxModelAsync(); + powerFxEngine.Execute(powerFxExpression, CultureInfo.CurrentCulture); + } + } + + public class FooFunction : ReflectionFunction + { + public FooFunction() : base("Foo", FormulaType.Blank) + { + } + + public BlankValue Execute() + { + return BlankValue.NewBlank(); } } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlRecordValueTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs similarity index 75% rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlRecordValueTests.cs rename to src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs index edc6205fc..f05db4df7 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlRecordValueTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs @@ -3,8 +3,8 @@ using System; using System.Linq; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx.Types; using Moq; using Newtonsoft.Json; @@ -18,23 +18,23 @@ public class ControlRecordValueTests public void SimpleControlRecordValueTest() { var recordType = RecordType.Empty().Add("Text", FormulaType.String).Add("X", FormulaType.Number).Add("SelectedDate", FormulaType.Date).Add("DefaultDate", FormulaType.DateTime); - var mockPowerAppFunctions = new Mock(MockBehavior.Strict); + var mockTestWebProvider = new Mock(MockBehavior.Strict); var controlName = "Label1"; var propertyValue = Guid.NewGuid().ToString(); var numberPropertyValue = 11; var datePropertyValue = new DateTime(2030, 1, 1, 0, 0, 0).Date; var dateTimePropertyValue = new DateTime(2030, 1, 1, 0, 0, 0); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text"))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text"))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = propertyValue })); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "X"))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "X"))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = numberPropertyValue.ToString() })); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "SelectedDate"))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "SelectedDate"))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = datePropertyValue.ToString() })); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "DefaultDate"))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "DefaultDate"))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = dateTimePropertyValue.ToString() })); - var controlRecordValue = new ControlRecordValue(recordType, mockPowerAppFunctions.Object, controlName); + var controlRecordValue = new ControlRecordValue(recordType, mockTestWebProvider.Object, controlName); Assert.Equal(controlName, controlRecordValue.Name); Assert.Equal(recordType, controlRecordValue.Type); @@ -49,10 +49,10 @@ public void SimpleControlRecordValueTest() Assert.Equal(datePropertyValue.ToString(), (controlRecordValue.GetField("SelectedDate") as DateValue).GetConvertedValue(null).ToString()); Assert.Equal(dateTimePropertyValue.ToString(), (controlRecordValue.GetField("DefaultDate") as DateTimeValue).GetConvertedValue(null).ToString()); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == controlName)), Times.Once()); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "X" && x.ControlName == controlName)), Times.Once()); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "SelectedDate" && x.ControlName == controlName)), Times.Once()); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "DefaultDate" && x.ControlName == controlName)), Times.Once()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == controlName)), Times.Once()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "X" && x.ControlName == controlName)), Times.Once()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "SelectedDate" && x.ControlName == controlName)), Times.Once()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "DefaultDate" && x.ControlName == controlName)), Times.Once()); } [Fact] @@ -65,14 +65,14 @@ public void GalleryControlRecordValueTest() var galleryRecordType = RecordType.Empty().Add(allItemsName, galleryAllItemsTableType); var galleryName = "Gallery1"; var labelText = Guid.NewGuid().ToString(); - var mockPowerAppFunctions = new Mock(MockBehavior.Strict); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) + var mockTestWebProvider = new Mock(MockBehavior.Strict); + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.IsAny())) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = labelText })); var itemCount = 4; - mockPowerAppFunctions.Setup(x => x.GetItemCount(It.IsAny())).Returns(itemCount); + mockTestWebProvider.Setup(x => x.GetItemCount(It.IsAny())).Returns(itemCount); - var galleryRecordValue = new ControlRecordValue(galleryRecordType, mockPowerAppFunctions.Object, galleryName); + var galleryRecordValue = new ControlRecordValue(galleryRecordType, mockTestWebProvider.Object, galleryName); Assert.Equal(galleryName, galleryRecordValue.Name); Assert.Equal(galleryRecordType, galleryRecordValue.Type); @@ -119,7 +119,7 @@ public void GalleryControlRecordValueTest() // Index(Gallery1.AllItems, i).Label1.Text Assert.Equal(labelText, (labelRecordValue.GetField("Text") as StringValue).Value); } - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == labelName)), Times.Exactly(itemCount)); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == labelName)), Times.Exactly(itemCount)); } [Fact] @@ -129,13 +129,13 @@ public void ComponentsControlRecordValueTest() var labelName = "Label1"; var componentRecordType = RecordType.Empty().Add(labelName, labelRecordType); var componentName = "Component1"; - var mockPowerAppFunctions = new Mock(MockBehavior.Strict); + var mockTestWebProvider = new Mock(MockBehavior.Strict); var propertyValue = Guid.NewGuid().ToString(); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text"))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text"))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = propertyValue })); - var controlRecordValue = new ControlRecordValue(componentRecordType, mockPowerAppFunctions.Object, componentName); + var controlRecordValue = new ControlRecordValue(componentRecordType, mockTestWebProvider.Object, componentName); Assert.Equal(componentName, controlRecordValue.Name); Assert.Equal(componentRecordType, controlRecordValue.Type); @@ -163,7 +163,7 @@ public void ComponentsControlRecordValueTest() // Component1.Label1.Text Assert.Equal(propertyValue, (labelRecordValue.GetField("Text") as StringValue).Value); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == labelName)), Times.Once()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is((x) => x.PropertyName == "Text" && x.ControlName == labelName)), Times.Once()); } } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableSourceTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableSourceTests.cs similarity index 76% rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableSourceTests.cs rename to src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableSourceTests.cs index 7662300ce..6d081ecdd 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableSourceTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableSourceTests.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. using System; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx.Types; using Moq; using Xunit; @@ -15,7 +15,7 @@ public class ControlTableSourceTests [Fact] public void TableSourceTest() { - var mockPowerAppFunctions = new Mock(MockBehavior.Strict); + var mockTestWebProvider = new Mock(MockBehavior.Strict); var itemPath = new ItemPath() { ControlName = "Gallery1", @@ -23,9 +23,9 @@ public void TableSourceTest() }; var itemCount = 3; - mockPowerAppFunctions.Setup(x => x.GetItemCount(It.IsAny())).Returns(itemCount); + mockTestWebProvider.Setup(x => x.GetItemCount(It.IsAny())).Returns(itemCount); var recordType = RecordType.Empty().Add("Label1", RecordType.Empty().Add("Text", FormulaType.String)); - var controlTableSource = new ControlTableSource(mockPowerAppFunctions.Object, itemPath, recordType); + var controlTableSource = new ControlTableSource(mockTestWebProvider.Object, itemPath, recordType); Assert.Equal(itemCount, controlTableSource.Count); for (var i = 0; i < itemCount; i++) diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableValueTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableValueTests.cs similarity index 75% rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableValueTests.cs rename to src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableValueTests.cs index 8ac325776..c436446d4 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/ControlTableValueTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlTableValueTests.cs @@ -3,8 +3,8 @@ using System; using System.Linq; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx.Types; using Moq; using Newtonsoft.Json; @@ -24,14 +24,14 @@ public void TableTest() var control1Type = RecordType.Empty().Add(control1PropName, FormulaType.String); var control2Type = RecordType.Empty().Add(control2PropName, FormulaType.String); var tableType = TableType.Empty().Add(new NamedFormulaType(control1Name, control1Type)).Add(new NamedFormulaType(control2Name, control2Type)); - var mockPowerAppFunctions = new Mock(MockBehavior.Strict); + var mockTestWebProvider = new Mock(MockBehavior.Strict); var tableCount = 5; var control1PropertyValue = Guid.NewGuid().ToString(); var control2PropertyValue = Guid.NewGuid().ToString(); - mockPowerAppFunctions.Setup(x => x.GetItemCount(It.IsAny())).Returns(tableCount); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control1PropName))) + mockTestWebProvider.Setup(x => x.GetItemCount(It.IsAny())).Returns(tableCount); + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control1PropName))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = control1PropertyValue })); - mockPowerAppFunctions.Setup(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control2PropName))) + mockTestWebProvider.Setup(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control2PropName))) .Returns(JsonConvert.SerializeObject(new JSPropertyValueModel() { PropertyValue = control2PropertyValue })); var itemPath = new ItemPath() @@ -41,8 +41,8 @@ public void TableTest() }; var recordType = tableType.ToRecord(); - var tableSource = new ControlTableSource(mockPowerAppFunctions.Object, itemPath, recordType); - var tableValue = new ControlTableValue(recordType, tableSource, mockPowerAppFunctions.Object); + var tableSource = new ControlTableSource(mockTestWebProvider.Object, itemPath, recordType); + var tableValue = new ControlTableValue(recordType, tableSource, mockTestWebProvider.Object); Assert.Equal(recordType, tableValue.RecordType); Assert.Equal(tableCount, tableValue.Rows.Count()); @@ -78,9 +78,9 @@ public void TableTest() Assert.Equal(control2PropertyValue, (control2PropValue as StringValue).Value); } - mockPowerAppFunctions.Verify(x => x.GetItemCount(It.IsAny()), Times.AtLeastOnce()); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control1PropName)), Times.Exactly(tableCount)); - mockPowerAppFunctions.Verify(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control2PropName)), Times.Exactly(tableCount)); + mockTestWebProvider.Verify(x => x.GetItemCount(It.IsAny()), Times.AtLeastOnce()); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control1PropName)), Times.Exactly(tableCount)); + mockTestWebProvider.Verify(x => x.GetPropertyValueFromControl(It.Is(x => x.PropertyName == control2PropName)), Times.Exactly(tableCount)); } } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/TypeMappingTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/TypeMappingTests.cs similarity index 97% rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/TypeMappingTests.cs rename to src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/TypeMappingTests.cs index 0f433c358..9da9f3ef7 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerFXModel/TypeMappingTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/TypeMappingTests.cs @@ -3,7 +3,7 @@ using System; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx.Types; using Xunit; @@ -107,7 +107,7 @@ public void GetTableTypeTest() // Empty table Assert.True(typeMapping.TryGetType("*[]", out formulaType)); - Assert.Equal(RecordType.Empty().ToTable(),formulaType); + Assert.Equal(RecordType.Empty().ToTable(), formulaType); } [Fact] @@ -147,7 +147,7 @@ public void GetRecordTypeTest() // Empty table Assert.True(typeMapping.TryGetType("![]", out formulaType)); - Assert.Equal(RecordType.Empty(),formulaType); + Assert.Equal(RecordType.Empty(), formulaType); } [Fact] diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestReporterTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestReporterTests.cs index 13bb2c988..9b04a12b2 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestReporterTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestReporterTests.cs @@ -163,7 +163,7 @@ public void CreateTestTest() var testRunName = "testRunName"; var testUser = "testUser"; var testName = "testName"; - var testSuiteName = "testSuite"; + var testSuiteName = "testSuite"; var testReporter = new TestReporter(MockFileSystem.Object); var testRunId = testReporter.CreateTestRun(testRunName, testUser); var testLocation = $"{TestReporter.ResultsPrefix}{testRunId}"; @@ -420,5 +420,5 @@ public void GenerateTestReportTest() }; MockFileSystem.Verify(x => x.WriteTextToFile(expectedTrxPath, It.Is(y => validateTestResults(y))), Times.Once()); } - } + } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs index c4f919e19..8b53a0155 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs @@ -9,8 +9,9 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; @@ -31,13 +32,15 @@ public class SingleTestRunnerTests private Mock MockTestInfraFunctions; private Mock MockUserManager; private Mock MockLoggerFactory; - private Mock MockTestState; - private Mock MockUrlMapper; + private Mock MockTestState; + private Mock MockSingleTestInstanceState; private Mock MockFileSystem; private Mock MockLogger; private Mock MockTestLogger; - private Mock MockPowerAppFunctions; + private Mock MockTestWebProvider; private Mock MockTestEngineEventHandler; + private Mock MockEnvironmentVariable; + private Mock MockBrowserContext; public SingleTestRunnerTests() { @@ -46,13 +49,15 @@ public SingleTestRunnerTests() MockTestInfraFunctions = new Mock(MockBehavior.Strict); MockUserManager = new Mock(MockBehavior.Strict); MockLoggerFactory = new Mock(MockBehavior.Strict); - MockTestState = new Mock(MockBehavior.Strict); - MockUrlMapper = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); MockFileSystem = new Mock(MockBehavior.Strict); MockLogger = new Mock(MockBehavior.Strict); MockTestLogger = new Mock(MockBehavior.Strict); - MockPowerAppFunctions = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); MockTestEngineEventHandler = new Mock(MockBehavior.Strict); + MockEnvironmentVariable = new Mock(MockBehavior.Strict); + MockBrowserContext = new Mock(MockBehavior.Strict); } private void SetupMocks(string testRunId, string testSuiteId, string testId, string appUrl, TestSuiteDefinition testSuiteDefinition, bool powerFxTestSuccess, string[]? additionalFiles, string testSuitelocale) @@ -65,7 +70,7 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str MockTestReporter.Setup(x => x.StartTest(It.IsAny(), It.IsAny())); MockTestReporter.Setup(x => x.EndTest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())); MockTestReporter.Setup(x => x.FailTest(It.IsAny(), It.IsAny())); - + MockTestReporter.SetupSet(x => x.TestResultsDirectory = "TestRunDirectory"); MockTestReporter.SetupGet(x => x.TestResultsDirectory).Returns("TestRunDirectory"); MockTestReporter.SetupSet(x => x.TestRunAppURL = "https://fake-app-url.com"); @@ -73,14 +78,14 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); - MockTestState.Setup(x => x.SetLogger(It.IsAny())); - MockTestState.Setup(x => x.SetTestSuiteDefinition(It.IsAny())); - MockTestState.Setup(x => x.SetTestRunId(It.IsAny())); - MockTestState.Setup(x => x.SetTestId(It.IsAny())); - MockTestState.Setup(x => x.SetTestResultsDirectory(It.IsAny())); - MockTestState.Setup(x => x.SetBrowserConfig(It.IsAny())); - MockTestState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); - MockTestState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + MockSingleTestInstanceState.Setup(x => x.SetLogger(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.SetTestSuiteDefinition(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.SetTestRunId(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.SetTestId(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.SetTestResultsDirectory(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.SetBrowserConfig(It.IsAny())); + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())); MockFileSystem.Setup(x => x.GetFiles(It.IsAny())).Returns(additionalFiles); @@ -92,6 +97,7 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str MockPowerFxEngine.Setup(x => x.RunRequirementsCheckAsync()).Returns(Task.CompletedTask); MockPowerFxEngine.Setup(x => x.UpdatePowerFxModelAsync()).Returns(Task.CompletedTask); MockPowerFxEngine.Setup(x => x.Execute(It.IsAny(), It.IsAny())).Returns(FormulaValue.NewBlank()); + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); MockTestEngineEventHandler.Setup(x => x.SetAndInitializeCounters(It.IsAny())); MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); @@ -108,17 +114,24 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str { MockPowerFxEngine.Setup(x => x.ExecuteWithRetryAsync(It.IsAny(), It.IsAny())).Throws(new Exception("something bad happened")); } - MockPowerFxEngine.Setup(x => x.GetPowerAppFunctions()).Returns(MockPowerAppFunctions.Object); + MockPowerFxEngine.Setup(x => x.GetWebProvider()).Returns(MockTestWebProvider.Object); - MockTestInfraFunctions.Setup(x => x.SetupAsync()).Returns(Task.CompletedTask); + MockTestInfraFunctions.Setup(x => x.SetupAsync(It.IsAny())).Returns(Task.CompletedTask); MockTestInfraFunctions.Setup(x => x.SetupNetworkRequestMockAsync()).Returns(Task.CompletedTask); MockTestInfraFunctions.Setup(x => x.GoToUrlAsync(It.IsAny())).Returns(Task.CompletedTask); MockTestInfraFunctions.Setup(x => x.EndTestRunAsync()).Returns(Task.CompletedTask); MockTestInfraFunctions.Setup(x => x.DisposeAsync()).Returns(Task.CompletedTask); + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(MockBrowserContext.Object); - MockUserManager.Setup(x => x.LoginAsUserAsync(appUrl)).Returns(Task.CompletedTask); + MockUserManager.Setup(x => x.LoginAsUserAsync(appUrl, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()) + ).Returns(Task.CompletedTask); - MockUrlMapper.Setup(x => x.GenerateTestUrl("", "")).Returns(appUrl); + MockTestWebProvider.Setup(x => x.GenerateTestUrl("", "")).Returns(appUrl); + MockTestWebProvider.SetupSet(x => x.TestInfraFunctions = MockTestInfraFunctions.Object); MockTestLogger.Setup(x => x.WriteToLogsFile(It.IsAny(), It.IsAny())); MockTestLogger.Setup(x => x.WriteExceptionToDebugLogsFile(It.IsAny(), It.IsAny())); @@ -129,10 +142,10 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str private void VerifyTestStateSetup(string testSuiteId, string testRunId, TestSuiteDefinition testSuiteDefinition, string testResultDirectory, BrowserConfiguration browserConfig, int setDirectoryTimes = 1) { MockLoggerFactory.Verify(x => x.CreateLogger(testSuiteId), Times.Once()); - MockTestState.Verify(x => x.SetTestSuiteDefinition(testSuiteDefinition), Times.Once()); - MockTestState.Verify(x => x.SetTestRunId(testRunId), Times.Once()); - MockTestState.Verify(x => x.SetBrowserConfig(browserConfig)); - MockTestState.Verify(x => x.SetTestResultsDirectory(testResultDirectory), Times.Exactly(setDirectoryTimes)); + MockSingleTestInstanceState.Verify(x => x.SetTestSuiteDefinition(testSuiteDefinition), Times.Once()); + MockSingleTestInstanceState.Verify(x => x.SetTestRunId(testRunId), Times.Once()); + MockSingleTestInstanceState.Verify(x => x.SetBrowserConfig(browserConfig)); + MockSingleTestInstanceState.Verify(x => x.SetTestResultsDirectory(testResultDirectory), Times.Exactly(setDirectoryTimes)); MockFileSystem.Verify(x => x.CreateDirectory(testResultDirectory), Times.Once()); } @@ -141,18 +154,22 @@ private void VerifySuccessfulTestExecution(string testResultDirectory, TestSuite { MockPowerFxEngine.Verify(x => x.Setup(), Times.Once()); MockPowerFxEngine.Verify(x => x.UpdatePowerFxModelAsync(), Times.Once()); - MockTestInfraFunctions.Verify(x => x.SetupAsync(), Times.Once()); - MockUserManager.Verify(x => x.LoginAsUserAsync(appUrl), Times.Once()); + MockTestInfraFunctions.Verify(x => x.SetupAsync(It.IsAny()), Times.Once()); + MockUserManager.Verify(x => x.LoginAsUserAsync(appUrl, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); MockTestInfraFunctions.Verify(x => x.SetupNetworkRequestMockAsync(), Times.Once()); - MockUrlMapper.Verify(x => x.GenerateTestUrl("", ""), Times.Once()); + MockTestWebProvider.Verify(x => x.GenerateTestUrl("", ""), Times.Once()); MockTestInfraFunctions.Verify(x => x.GoToUrlAsync(appUrl), Times.Once()); - MockTestState.Verify(x => x.GetTestSuiteDefinition(), Times.Exactly(2)); + MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Exactly(2)); MockTestReporter.Verify(x => x.CreateTest(testRunId, testSuiteId, testSuiteDefinition.TestCases[0].TestCaseName), Times.Once()); MockTestReporter.Verify(x => x.StartTest(testRunId, testId), Times.Once()); - MockTestState.Verify(x => x.SetTestId(testId), Times.Once()); + MockSingleTestInstanceState.Verify(x => x.SetTestId(testId), Times.Once()); MockLoggerFactory.Verify(x => x.CreateLogger(testSuiteId), Times.Once()); - MockTestState.Verify(x => x.SetLogger(It.IsAny()), Times.Exactly(1)); - MockTestState.Verify(x => x.SetTestResultsDirectory(testResultDirectory), Times.Once()); + MockSingleTestInstanceState.Verify(x => x.SetLogger(It.IsAny()), Times.Exactly(1)); + MockSingleTestInstanceState.Verify(x => x.SetTestResultsDirectory(testResultDirectory), Times.Once()); MockFileSystem.Verify(x => x.CreateDirectory(testResultDirectory), Times.Once()); MockTestLogger.Verify(x => x.WriteToLogsFile(testResultDirectory, testId), Times.Once()); MockFileSystem.Verify(x => x.GetFiles(testResultDirectory), Times.Once()); @@ -187,17 +204,19 @@ public async Task SingleTestRunnerSuccessWithTestDataOneTest(string[]? additiona MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, true, additionalFiles, testData.testSuiteLocale); var locale = string.IsNullOrEmpty(testData.testSuiteLocale) ? CultureInfo.CurrentCulture : new CultureInfo(testData.testSuiteLocale); - + await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); VerifyTestStateSetup(testData.testSuiteId, testData.testRunId, testData.testSuiteDefinition, testData.testResultDirectory, testData.browserConfig, 2); @@ -216,17 +235,19 @@ public async Task SingleTestRunnerSuccessWithTestDataTwoTest(string[]? additiona MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataTwo(); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, true, additionalFiles, testData.testSuiteLocale); var locale = string.IsNullOrEmpty(testData.testSuiteLocale) ? CultureInfo.CurrentCulture : new CultureInfo(testData.testSuiteLocale); - + await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); VerifyTestStateSetup(testData.testSuiteId, testData.testRunId, testData.testSuiteDefinition, testData.testResultDirectory, testData.browserConfig); @@ -242,17 +263,20 @@ public async Task SingleTestRunnerCanOnlyBeRunOnce() MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, true, testData.additionalFiles, testData.testSuiteLocale); var locale = string.IsNullOrEmpty(testData.testSuiteLocale) ? CultureInfo.CurrentCulture : new CultureInfo(testData.testSuiteLocale); - + await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); await Assert.ThrowsAsync(async () => { await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); }); } @@ -265,17 +289,19 @@ public async Task SingleTestRunnerPowerFxTestFail() MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, false, testData.additionalFiles, testData.testSuiteLocale); var locale = string.IsNullOrEmpty(testData.testSuiteLocale) ? CultureInfo.CurrentCulture : new CultureInfo(testData.testSuiteLocale); - + await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); VerifyTestStateSetup(testData.testSuiteId, testData.testRunId, testData.testSuiteDefinition, testData.testResultDirectory, testData.browserConfig, 2); @@ -289,10 +315,12 @@ public async Task SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper(Action< MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); @@ -304,13 +332,13 @@ public async Task SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper(Action< debugObj.TryAdd("environmentId", "someEnvironmentId"); debugObj.TryAdd("sessionId", "someSessionId"); - MockPowerAppFunctions.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)debugObj)); + MockTestWebProvider.Setup(x => x.GetDebugInfo()).Returns(Task.FromResult((object)debugObj)); var exceptionToThrow = new InvalidOperationException("Test exception"); additionalMockSetup(exceptionToThrow); var locale = string.IsNullOrEmpty(testData.testSuiteLocale) ? CultureInfo.CurrentCulture : new CultureInfo(testData.testSuiteLocale); - + await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); VerifyTestStateSetup(testData.testSuiteId, testData.testRunId, testData.testSuiteDefinition, testData.testResultDirectory, testData.browserConfig); @@ -321,6 +349,7 @@ public async Task SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper(Action< [Fact] public async Task CreateDirectoryThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())).Throws(exceptionToThrow); @@ -330,6 +359,7 @@ await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptio [Fact] public async Task PowerFxSetupThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { MockPowerFxEngine.Setup(x => x.Setup()).Throws(exceptionToThrow); @@ -339,7 +369,9 @@ await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptio [Fact] public async Task PowerFxUpdatePowerFxModelAsyncThrowsTest() { - await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); + await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => + { MockPowerFxEngine.Setup(x => x.UpdatePowerFxModelAsync()).Throws(exceptionToThrow); }); } @@ -347,24 +379,29 @@ await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptio [Fact] public async Task TestInfraSetupThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { - MockTestInfraFunctions.Setup(x => x.SetupAsync()).Throws(exceptionToThrow); + MockTestInfraFunctions.Setup(x => x.SetupAsync(It.IsAny())).Throws(exceptionToThrow); }); } [Fact] public async Task LoginAsUserThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { - MockUserManager.Setup(x => x.LoginAsUserAsync(It.IsAny())).Throws(exceptionToThrow); + MockUserManager.Setup(x => + x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + ).Throws(exceptionToThrow); }); } [Fact] public async Task SetupNetworkRequestMockThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { MockTestInfraFunctions.Setup(x => x.SetupNetworkRequestMockAsync()).Throws(exceptionToThrow); @@ -374,15 +411,17 @@ await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptio [Fact] public async Task GenerateAppUrlThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { - MockUrlMapper.Setup(x => x.GenerateTestUrl("", "")).Throws(exceptionToThrow); + MockTestWebProvider.Setup(x => x.GenerateTestUrl("", "")).Throws(exceptionToThrow); }); } [Fact] public async Task GoToUrlThrowsTest() { + MockPowerFxEngine.Setup(x => x.PowerAppIntegrationEnabled).Returns(true); await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { MockTestInfraFunctions.Setup(x => x.GoToUrlAsync(It.IsAny())).Throws(exceptionToThrow); @@ -397,10 +436,12 @@ public async Task PowerFxExecuteThrowsTest() MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); @@ -428,10 +469,12 @@ public async Task UserInputExceptionHandlingTest() MockTestInfraFunctions.Object, MockUserManager.Object, MockTestState.Object, - MockUrlMapper.Object, + MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLoggerFactory.Object, - MockTestEngineEventHandler.Object); + MockTestEngineEventHandler.Object, + MockEnvironmentVariable.Object, + MockTestWebProvider.Object); var testData = new TestDataOne(); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, true, testData.additionalFiles, testData.testSuiteLocale); @@ -439,15 +482,15 @@ public async Task UserInputExceptionHandlingTest() // Specific setup for this test var exceptionToThrow = new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); - MockUserManager.Setup(x => x.LoginAsUserAsync(It.IsAny())).Throws(exceptionToThrow); - MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); + MockUserManager.Setup(x => x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(exceptionToThrow); + MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); // Act await singleTestRunner.RunTestAsync(testData.testRunId, testData.testRunDirectory, testData.testSuiteDefinition, testData.browserConfig, "", "", locale); // Assert MockTestEngineEventHandler.Verify(x => x.EncounteredException(exceptionToThrow), Times.Once()); - } + } // Sample Test Data for test with OnTestCaseStart, OnTestCaseComplete and OnTestSuiteComplete class TestDataOne { diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs index 37632c2a3..0bf1f6105 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs @@ -278,6 +278,8 @@ private void SetupMocks(string outputDirectory, TestSettings testSettings, TestS MockState.Setup(x => x.GetOutputDirectory()).Returns(outputDirectory); MockState.Setup(x => x.GetTestSettings()).Returns(testSettings); MockState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockState.Setup(x => x.SetTestConfigFile(It.IsAny())); + MockState.Setup(x => x.LoadExtensionModules(It.IsAny())); MockTestReporter.Setup(x => x.CreateTestRun(It.IsAny(), It.IsAny())).Returns(testRunId); MockTestReporter.Setup(x => x.StartTestRun(It.IsAny())); @@ -286,7 +288,7 @@ private void SetupMocks(string outputDirectory, TestSettings testSettings, TestS MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())); - MockSingleTestRunner.Setup(x => x.RunTestAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + MockSingleTestRunner.Setup(x => x.RunTestAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); } @@ -358,7 +360,7 @@ public async Task TestEngineReturnsPathOnUserInputErrors() MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); LoggingTestHelper.SetupMock(MockLogger); MockState.Setup(x => x.SetOutputDirectory(It.IsAny())); - MockState.Setup(x => x.GetOutputDirectory()).Returns("MockOutputDirectory"); + MockState.Setup(x => x.GetOutputDirectory()).Returns("MockOutputDirectory"); MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())); MockTestLoggerProvider.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs index 750127341..55834de09 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs @@ -7,9 +7,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Microsoft.PowerApps.TestEngine.Users; using Moq; using Xunit; @@ -32,6 +34,8 @@ public class PlaywrightTestInfraFunctionTests private Mock MockElementHandle; private Mock MockLogger; private Mock MockLoggerFactory; + private Mock MockUserManager; + private Mock MockTestWebProvider; public PlaywrightTestInfraFunctionTests() { @@ -50,6 +54,8 @@ public PlaywrightTestInfraFunctionTests() MockLogger = new Mock(MockBehavior.Strict); MockLoggerFactory = new Mock(MockBehavior.Strict); MockElementHandle = new Mock(MockBehavior.Strict); + MockUserManager = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(MockBehavior.Strict); } [Theory] @@ -90,10 +96,11 @@ public async Task SetupAsyncTest(string browser, string device, int? screenWidth MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns(testResultsDirectory); MockBrowser.Setup(x => x.NewContextAsync(It.IsAny())).Returns(Task.FromResult(MockBrowserContext.Object)); LoggingTestHelper.SetupMock(MockLogger); + MockUserManager.SetupGet(x => x.UseStaticContext).Returns(false); var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPlaywrightObject.Object); - await playwrightTestInfraFunctions.SetupAsync(); + await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object); MockSingleTestInstanceState.Verify(x => x.GetBrowserConfig(), Times.Once()); MockPlaywrightObject.Verify(x => x[browserConfig.Browser], Times.Once()); @@ -145,6 +152,68 @@ public async Task SetupAsyncTest(string browser, string device, int? screenWidth MockBrowser.Verify(x => x.NewContextAsync(It.Is(y => verifyBrowserContextOptions(y))), Times.Once()); } + [Theory] + [InlineData("en-US")] + [InlineData("fr-FR")] + [InlineData("de-DE")] + public async Task SetupAsyncExtensionLocaleTest(string testLocale) + { + var browserConfig = new BrowserConfiguration() + { + Browser = "Firefox" + }; + + var testSettings = new TestSettings() + { + ExtensionModules = new TestSettingExtensions() { Enable = true } + }; + + var devicesDictionary = new Dictionary() + { + { "Pixel 2", new BrowserNewContextOptions() { UserAgent = "Pixel 2 User Agent "} }, + { "iPhone 8", new BrowserNewContextOptions() { UserAgent = "iPhone 8 User Agent "} } + }; + + var mockTestEngineModule = new Mock(); + mockTestEngineModule.Setup(x => x.ExtendBrowserContextOptions(It.IsAny(), It.IsAny())) + .Callback((BrowserNewContextOptions options, TestSettings settings) => options.Locale = testLocale); + + MockSingleTestInstanceState.Setup(x => x.GetBrowserConfig()).Returns(browserConfig); + MockPlaywrightObject.SetupGet(x => x[It.IsAny()]).Returns(MockBrowserType.Object); + MockPlaywrightObject.SetupGet(x => x.Devices).Returns(devicesDictionary); + MockBrowserType.Setup(x => x.LaunchAsync(It.IsAny())).Returns(Task.FromResult(MockBrowser.Object)); + MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List() { mockTestEngineModule.Object }); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + MockBrowser.Setup(x => x.NewContextAsync(It.IsAny())).Returns(Task.FromResult(MockBrowserContext.Object)); + LoggingTestHelper.SetupMock(MockLogger); + MockUserManager.SetupGet(x => x.UseStaticContext).Returns(false); + + var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, + MockFileSystem.Object, MockPlaywrightObject.Object); + await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object); + + MockSingleTestInstanceState.Verify(x => x.GetBrowserConfig(), Times.Once()); + MockPlaywrightObject.Verify(x => x[browserConfig.Browser], Times.Once()); + MockBrowserType.Verify(x => x.LaunchAsync(It.Is(y => y.Headless == true && y.Timeout == testSettings.Timeout)), Times.Once()); + MockTestState.Verify(x => x.GetTestSettings(), Times.Once()); + + if (browserConfig.Device != null) + { + MockPlaywrightObject.Verify(x => x.Devices, Times.Once()); + } + + var verifyBrowserContextOptions = (BrowserNewContextOptions options) => + { + if (options.Locale != testLocale) + { + return false; + } + return true; + }; + MockBrowser.Verify(x => x.NewContextAsync(It.Is(y => verifyBrowserContextOptions(y))), Times.Once()); + } + [Theory] [InlineData("")] [InlineData(null)] @@ -168,7 +237,7 @@ public async Task SetupAsyncThrowsOnNullOrEmptyBrowserTest(string browser) var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, null); - await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync()); + await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object)); } [Theory] @@ -197,7 +266,7 @@ public async Task SetupAsyncThrowsOnInvalidBrowserTest(string browser) MockFileSystem.Object, null); // Act and Assert - var ex = await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync()); + var ex = await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object)); Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionInvalidTestSettings.ToString(), ex.Message); LoggingTestHelper.VerifyLogging(MockLogger, PlaywrightTestInfraFunctions.BrowserNotSupportedErrorMessage, LogLevel.Error, Times.Once()); } @@ -218,7 +287,7 @@ public async Task SetupAsyncThrowsOnNullTestSettingsTest() var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPlaywrightObject.Object); - await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync()); + await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object)); } [Fact] @@ -231,7 +300,7 @@ public async Task SetupAsyncThrowsOnNullBrowserConfigTest() var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPlaywrightObject.Object); - await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync()); + await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupAsync(MockUserManager.Object)); } [Fact] @@ -254,7 +323,8 @@ public async Task SetupNetworkRequestMockAsyncTest() var mock = new NetworkRequestMock() { RequestURL = "https://make.powerapps.com", - ResponseDataFile = "response.json" + ResponseDataFile = "response.json", + IsExtension = false }; var testSuiteDefinition = new TestSuiteDefinition() @@ -438,6 +508,48 @@ public async Task SetupNetworkRequestMockAsyncThrowOnEmptyFilePathTest() Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionInvalidFilePath.ToString(), ex.Message); } + [Fact] + public async Task SetupNetworkRequestModule() + { + var mock = new NetworkRequestMock() + { + RequestURL = "https://make.powerapps.com", + IsExtension = false + }; + + var testSuiteDefinition = new TestSuiteDefinition() + { + TestSuiteName = "Test1", + TestSuiteDescription = "First test", + AppLogicalName = "logicalAppName1", + Persona = "User1", + NetworkRequestMocks = new List { mock }, + TestCases = new List() + { + new TestCase + { + TestCaseName = "Test Case Name", + TestCaseDescription = "Test Case Description", + TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" + } + } + }; + + MockTestState.Setup(x => x.GetTestEngineModules()).Returns(new List()); + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockBrowserContext.Setup(x => x.NewPageAsync()).Returns(Task.FromResult(MockPage.Object)); + MockFileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(false); + MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny())).Returns(false); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, + MockFileSystem.Object, browserContext: MockBrowserContext.Object); + var ex = await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.SetupNetworkRequestMockAsync()); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionInvalidFilePath.ToString(), ex.Message); + } + [Fact] public async Task GoToUrlTest() { @@ -605,9 +717,10 @@ public async Task RunJavascriptSuccessfulTest() LoggingTestHelper.SetupMock(MockLogger); MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockPage.Setup(x => x.EvaluateAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(expectedResponse)); + MockTestWebProvider.SetupGet(x => x.CheckTestEngineObject).Returns("Sample"); var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, - MockFileSystem.Object, page: MockPage.Object); + MockFileSystem.Object, page: MockPage.Object, testWebProvider: MockTestWebProvider.Object); var result = await playwrightTestInfraFunctions.RunJavascriptAsync(jsExpression); Assert.Equal(expectedResponse, result); @@ -652,102 +765,5 @@ public async Task RouteNetworkRequestTest() await playwrightTestInfraFunctions.RouteNetworkRequest(MockRoute.Object, mock); MockRoute.Verify(x => x.ContinueAsync(It.IsAny()), Times.Once); } - - [Fact] - public async Task HandleUserPasswordScreen() - { - string testSelector = "input:has-text('Password')"; - string testTextEntry = "*****"; - string desiredUrl = "https://make.powerapps.com"; - - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var mockLocator = new Mock(MockBehavior.Strict); - MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); - mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); - - MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); - MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); - // Assume ask already logged in - MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.IsAny())).Returns(Task.FromResult(MockElementHandle.Object)); - // Simulate Click to stay signed in - MockPage.Setup(x => x.ClickAsync("[id=\"idBtn_Back\"]", null)).Returns(Task.CompletedTask); - // Wait until login is complete and redirect to desired page - MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Returns(Task.CompletedTask); - - var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, - MockFileSystem.Object, browserContext: MockBrowserContext.Object, page: MockPage.Object); - - await playwrightTestInfraFunctions.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl); - - MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); - MockPage.Verify(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.Is(v => v.Timeout >= 8000))); - } - - [Fact] - public async Task HandleUserPasswordScreenErrorEntry() - { - string testSelector = "input:has-text('Password')"; - string testTextEntry = "*****"; - string desiredUrl = "https://make.powerapps.com"; - - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var mockLocator = new Mock(MockBehavior.Strict); - MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); - mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); - - MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); - MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); - // Not ask to sign in as selector not found - MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.IsAny())).Throws(new TimeoutException()); - // Simulate error response for password error - MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.IsAny())).Returns(Task.FromResult(MockElementHandle.Object)); - // Throw exception as not make it to desired url - MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); - - var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, - MockFileSystem.Object, browserContext: MockBrowserContext.Object, page: MockPage.Object); - - // scenario where password error or missing - var ex = await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl)); - - MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); - MockPage.Verify(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.Is(v => v.Timeout >= 2000))); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); - } - - [Fact] - public async Task HandleUserPasswordScreenUnknownError() - { - string testSelector = "input:has-text('Password')"; - string testTextEntry = "*****"; - string desiredUrl = "https://make.powerapps.com"; - - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - var mockLocator = new Mock(MockBehavior.Strict); - MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); - mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); - - MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); - MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); - // Not ask to sign in as selector not found - MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", null)).Throws(new TimeoutException()); - // Also not able to find password error, must be some other error - MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.IsAny())).Throws(new TimeoutException()); - // Throw exception as not make it to desired url - MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); - - var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, - MockFileSystem.Object, browserContext: MockBrowserContext.Object, page: MockPage.Object); - - await Assert.ThrowsAsync(async () => await playwrightTestInfraFunctions.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl)); - - MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); - } - } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Users/UserManagerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Users/UserManagerTests.cs deleted file mode 100644 index d893ba409..000000000 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Users/UserManagerTests.cs +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.System; -using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.PowerApps.TestEngine.Tests.Helpers; -using Microsoft.PowerApps.TestEngine.Users; -using Moq; -using Xunit; - -namespace Microsoft.PowerApps.TestEngine.Tests.Users -{ - public class UserManagerTests - { - private Mock MockTestInfraFunctions; - private Mock MockTestState; - private Mock MockSingleTestInstanceState; - private Mock MockEnvironmentVariable; - private TestSuiteDefinition TestSuiteDefinition; - private Mock MockLogger; - - public UserManagerTests() - { - MockTestInfraFunctions = new Mock(MockBehavior.Strict); - MockTestState = new Mock(MockBehavior.Strict); - MockSingleTestInstanceState = new Mock(MockBehavior.Strict); - MockEnvironmentVariable = new Mock(MockBehavior.Strict); - TestSuiteDefinition = new TestSuiteDefinition() - { - TestSuiteName = "Test1", - TestSuiteDescription = "First test", - AppLogicalName = "logicalAppName1", - Persona = "User1", - TestCases = new List() - { - new TestCase - { - TestCaseName = "Test Case Name", - TestCaseDescription = "Test Case Description", - TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" - } - } - }; - MockLogger = new Mock(MockBehavior.Strict); - } - - [Fact] - public async Task LoginAsUserSuccessTest() - { - var userConfiguration = new UserConfiguration() - { - PersonaName = "User1", - EmailKey = "user1Email", - PasswordKey = "user1Password" - }; - - var email = "someone@example.com"; - var password = "myPassword1234"; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); - MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); - MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); - MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.PasswordKey)).Returns(password); - MockTestInfraFunctions.Setup(x => x.GoToUrlAsync(It.IsAny())).Returns(Task.CompletedTask); - MockTestInfraFunctions.Setup(x => x.HandleUserEmailScreen(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); - MockTestInfraFunctions.Setup(x => x.HandleUserPasswordScreen(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); - MockTestInfraFunctions.Setup(x => x.ClickAsync(It.IsAny())).Returns(Task.CompletedTask); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - await userManager.LoginAsUserAsync("*"); - - MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); - MockTestState.Verify(x => x.GetUserConfiguration(userConfiguration.PersonaName), Times.Once()); - MockEnvironmentVariable.Verify(x => x.GetVariable(userConfiguration.EmailKey), Times.Once()); - MockEnvironmentVariable.Verify(x => x.GetVariable(userConfiguration.PasswordKey), Times.Once()); - MockTestInfraFunctions.Verify(x => x.HandleUserEmailScreen("input[type=\"email\"]", email), Times.Once()); - MockTestInfraFunctions.Verify(x => x.ClickAsync("input[type=\"submit\"]"), Times.Once()); - MockTestInfraFunctions.Verify(x => x.HandleUserPasswordScreen("input[type=\"password\"]", password, "*"), Times.Once()); - } - - [Fact] - public async Task LoginUserAsyncThrowsOnNullTestDefinitionTest() - { - TestSuiteDefinition testSuiteDefinition = null; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*")); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public async Task LoginUserAsyncThrowsOnInvalidPersonaTest(string persona) - { - var testSuiteDefinition = new TestSuiteDefinition() - { - TestSuiteName = "Test1", - TestSuiteDescription = "First test", - AppLogicalName = "logicalAppName1", - Persona = persona, - TestCases = new List() - { - new TestCase - { - TestCaseName = "Test Case Name", - TestCaseDescription = "Test Case Description", - TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" - } - } - }; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*")); - } - - [Fact] - public async Task LoginUserAsyncThrowsOnNullUserConfigTest() - { - UserConfiguration userConfiguration = null; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); - MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*")); - } - - [Theory] - [InlineData(null, "myPassword1234")] - [InlineData("", "myPassword1234")] - [InlineData("user1Email", null)] - [InlineData("user1Email", "")] - public async Task LoginUserAsyncThrowsOnInvalidUserConfigTest(string emailKey, string passwordKey) - { - UserConfiguration userConfiguration = new UserConfiguration() - { - PersonaName = "User1", - EmailKey = emailKey, - PasswordKey = passwordKey - }; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); - MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*")); - } - - [Theory] - [InlineData(null, "user1Password")] - [InlineData("", "user1Password")] - [InlineData("someone@example.com", null)] - [InlineData("someone@example.com", "")] - public async Task LoginUserAsyncThrowsOnInvalidEnviromentVariablesTest(string email, string password) - { - UserConfiguration userConfiguration = new UserConfiguration() - { - PersonaName = "User1", - EmailKey = "user1Email", - PasswordKey = "user1Password" - }; - - MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); - MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); - MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); - MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.PasswordKey)).Returns(password); - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - - var userManager = new UserManager(MockTestInfraFunctions.Object, MockTestState.Object, MockSingleTestInstanceState.Object, MockEnvironmentVariable.Object); - - var ex = await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*")); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); - if (String.IsNullOrEmpty(email)) - { - LoggingTestHelper.VerifyLogging(MockLogger, "User email cannot be null. Please check if the environment variable is set properly.", LogLevel.Error, Times.Once()); - } - if (String.IsNullOrEmpty(password)) - { - LoggingTestHelper.VerifyLogging(MockLogger, "Password cannot be null. Please check if the environment variable is set properly.", LogLevel.Error, Times.Once()); - } - } - } -} diff --git a/src/Microsoft.PowerApps.TestEngine/Config/BrowserConfiguration.cs b/src/Microsoft.PowerApps.TestEngine/Config/BrowserConfiguration.cs index cc3d2a6eb..03b0d0bf0 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/BrowserConfiguration.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/BrowserConfiguration.cs @@ -39,5 +39,10 @@ public class BrowserConfiguration /// If not specified, the browser will be used /// public string ConfigName { get; set; } + + /// + /// Gets or sets the channel to be used for browser + /// + public string Channel { get; set; } = ""; } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs b/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs index 71c8938ec..7de117100 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Users; namespace Microsoft.PowerApps.TestEngine.Config { @@ -71,6 +74,19 @@ public interface ITestState /// Output directory public void SetOutputDirectory(string outputDirectory); + + /// + /// Sets the test config file + /// + /// The test config file + public void SetTestConfigFile(FileInfo testConfigFile); + + /// + /// Gets the test config file + /// + /// Test config file + public FileInfo GetTestConfigFile(); + /// /// Gets the directory that all tests outputs should be placed in. /// @@ -95,5 +111,38 @@ public interface ITestState /// /// The timeout value public int GetTimeout(); + + /// + /// Loads any matching Test Engine Modules + /// + public void LoadExtensionModules(ILogger logger); + + /// + /// Sets path to locate option extension modules + /// + /// The path to set + public void SetModulePath(string path); + + /// + /// Add optional test engine modules + /// + /// + public void AddModules(IEnumerable modules); + + /// + /// Get the list of registered Test engine extension models + /// + /// + public List GetTestEngineModules(); + + /// + /// Get the list of registered Test engine user managers + /// + public List GetTestEngineUserManager(); + + /// + /// Get the list of registered Test engine web test providers + /// + public List GetTestEngineWebProviders(); } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/NetworkRequestMock.cs b/src/Microsoft.PowerApps.TestEngine/Config/NetworkRequestMock.cs index 2aac95301..d8401d811 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/NetworkRequestMock.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/NetworkRequestMock.cs @@ -28,5 +28,17 @@ public class NetworkRequestMock /// Gets or sets the request's payload. /// public string RequestBodyFile { get; set; } + + /// + /// Indicate if this request should be used for registed Network Module extensions + /// + /// + public bool IsExtension { get; set; } = false; + + /// + /// Extended parameters to be used by extension + /// + /// + public Dictionary ExtensionProperties { get; set; } = new Dictionary(); } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensionSource.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensionSource.cs new file mode 100644 index 000000000..e793bd8a3 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensionSource.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerApps.TestEngine.Config +{ + public class TestSettingExtensionSource + { + public TestSettingExtensionSource() + { + EnableFileSystem = true; + InstallSource.Add(Path.GetDirectoryName(this.GetType().Assembly.Location)); + } + + public bool EnableNuGet { get; set; } = false; + + public bool EnableFileSystem { get; set; } = false; + + public List InstallSource { get; set; } = new List(); + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs new file mode 100644 index 000000000..a7ce4572b --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.PowerApps.TestEngine.Config +{ + /// + /// Test Settings for modules + /// + public class TestSettingExtensions + { + /// + /// Determine if extension modules should be enabled + /// + public bool Enable { get; set; } = false; + + public TestSettingExtensionSource Source { get; set; } = new TestSettingExtensionSource() { }; + + /// + /// Determine if extension modules should be checks for Namespace rules + /// + public bool CheckAssemblies { get; set; } = true; + + /// + /// List of allowed Test Engine Modules that can be referenced. + /// + public List AllowModule { get; set; } = new List(); + + /// + /// List of allowed Test Engine Modules cannot be loaded unless there is an explict allow + /// + public List DenyModule { get; set; } = new List(); + + /// + /// List of allowed .Net Namespaces that can be referenced in a Test Engine Module + /// + public List AllowNamespaces { get; set; } = new List(); + + /// + /// List of allowed .Net Namespaces that deney load unless explict allow is defined + /// + public List DenyNamespaces { get; set; } = new List(); + + /// + /// Additional optional parameters for extension modules + /// + public Dictionary Parameters { get; set; } = new Dictionary(); + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestSettings.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestSettings.cs index 761d207ec..e6d708462 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/TestSettings.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestSettings.cs @@ -50,5 +50,10 @@ public class TestSettings /// Timeout in milliseconds. Default is 30000 (30s) /// public int Timeout { get; set; } = 30000; + + /// + /// Define settings for Test Engine Extensions + /// + public TestSettingExtensions ExtensionModules { get; set; } = new TestSettingExtensions(); } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs index 135d74f7c..505676ce2 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs @@ -1,9 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.ComponentModel.Composition; +using System.ComponentModel.Composition.Hosting; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.Users; namespace Microsoft.PowerApps.TestEngine.Config { @@ -13,6 +18,7 @@ namespace Microsoft.PowerApps.TestEngine.Config public class TestState : ITestState { private readonly ITestConfigParser _testConfigParser; + private TestPlanDefinition TestPlanDefinition { get; set; } private List TestCases { get; set; } = new List(); private string EnvironmentId { get; set; } @@ -22,6 +28,16 @@ public class TestState : ITestState private string OutputDirectory { get; set; } + private FileInfo TestConfigFile { get; set; } + + private string ModulePath { get; set; } + + private List Modules { get; set; } = new List(); + + private List UserManagers { get; set; } = new List(); + + private List WebProviders { get; set; } = new List(); + private bool IsValid { get; set; } = false; public TestState(ITestConfigParser testConfigParser) @@ -229,6 +245,19 @@ public string GetOutputDirectory() return OutputDirectory; } + public void SetTestConfigFile(FileInfo testConfig) + { + if (testConfig == null) + { + throw new ArgumentNullException(nameof(testConfig)); + } + TestConfigFile = testConfig; + } + public FileInfo GetTestConfigFile() + { + return TestConfigFile; + } + public UserConfiguration GetUserConfiguration(string persona) { if (!IsValid) @@ -254,5 +283,71 @@ public int GetTimeout() { return GetTestSettings().Timeout; } + + public void SetModulePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + ModulePath = path; + } + + /// + /// Load Managed Extensibility Framework (MEF) Test Engine modules + /// + public void LoadExtensionModules(ILogger logger) + { + var loader = new TestEngineModuleMEFLoader(logger); + var settings = this.GetTestSettings(); + var catalogModules = loader.LoadModules(settings.ExtensionModules); + + using var catalog = new AggregateCatalog(catalogModules); + using var container = new CompositionContainer(catalog); + + var mefComponents = new MefComponents(); + container.ComposeParts(mefComponents); + var components = mefComponents.MefModules.Select(v => v.Value).ToArray(); + this.AddModules(components); + + var userManagers = mefComponents.UserModules.Select(v => v.Value).OrderByDescending(v => v.Priority).ToArray(); + this.AddUserModules(userManagers); + + var webProviders = mefComponents.WebProviderModules.Select(v => v.Value).ToArray(); + this.AddWebProviderModules(webProviders); + } + + public void AddModules(IEnumerable modules) + { + Modules.Clear(); + Modules.AddRange(modules); + } + + public void AddUserModules(IEnumerable modules) + { + UserManagers.Clear(); + UserManagers.AddRange(modules); + } + + public void AddWebProviderModules(IEnumerable modules) + { + WebProviders.Clear(); + WebProviders.AddRange(modules); + } + + public List GetTestEngineModules() + { + return Modules; + } + + public List GetTestEngineUserManager() + { + return UserManagers; + } + + public List GetTestEngineWebProviders() + { + return WebProviders; + } } } diff --git a/src/Microsoft.PowerApps.TestEngine/Helpers/ExceptionHandlingHelper.cs b/src/Microsoft.PowerApps.TestEngine/Helpers/ExceptionHandlingHelper.cs index a8b8d99af..cd570535c 100644 --- a/src/Microsoft.PowerApps.TestEngine/Helpers/ExceptionHandlingHelper.cs +++ b/src/Microsoft.PowerApps.TestEngine/Helpers/ExceptionHandlingHelper.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; namespace Microsoft.PowerApps.TestEngine.Helpers { diff --git a/src/Microsoft.PowerApps.TestEngine/Helpers/LoggingHelper.cs b/src/Microsoft.PowerApps.TestEngine/Helpers/LoggingHelper.cs index c663b2b87..8bc49eef7 100644 --- a/src/Microsoft.PowerApps.TestEngine/Helpers/LoggingHelper.cs +++ b/src/Microsoft.PowerApps.TestEngine/Helpers/LoggingHelper.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; @@ -14,15 +14,15 @@ namespace Microsoft.PowerApps.TestEngine.Helpers { public class LoggingHelper { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly ISingleTestInstanceState _singleTestInstanceState; private readonly ITestEngineEvents _eventHandler; private ILogger Logger { get { return _singleTestInstanceState.GetLogger(); } } - public LoggingHelper(IPowerAppFunctions powerAppFunctions, + public LoggingHelper(ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestEngineEvents eventHandler) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; _singleTestInstanceState = singleTestInstanceState; _eventHandler = eventHandler; } @@ -31,7 +31,7 @@ public async void DebugInfo() { try { - ExpandoObject debugInfo = (ExpandoObject)await _powerAppFunctions.GetDebugInfo(); + ExpandoObject debugInfo = (ExpandoObject)await _testWebProvider.GetDebugInfo(); if (debugInfo != null && debugInfo.ToString() != "undefined") { Logger.LogInformation($"------------------------------\n Debug Info \n------------------------------"); diff --git a/src/Microsoft.PowerApps.TestEngine/ISingleTestRunner.cs b/src/Microsoft.PowerApps.TestEngine/ISingleTestRunner.cs index 6342db890..878fb3ae7 100644 --- a/src/Microsoft.PowerApps.TestEngine/ISingleTestRunner.cs +++ b/src/Microsoft.PowerApps.TestEngine/ISingleTestRunner.cs @@ -3,6 +3,7 @@ using System.Globalization; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; namespace Microsoft.PowerApps.TestEngine { diff --git a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj index 67d9954ed..242f2ff20 100644 --- a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj +++ b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj @@ -30,17 +30,13 @@ - + + + + - - - - PreserveNewest - true - - all diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/ITestEngineModule.cs b/src/Microsoft.PowerApps.TestEngine/Modules/ITestEngineModule.cs new file mode 100644 index 000000000..db38ba891 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Modules/ITestEngineModule.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; + +namespace Microsoft.PowerApps.TestEngine.Modules +{ + public interface ITestEngineModule + { + void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings); + void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem); + Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock); + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs b/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs new file mode 100644 index 000000000..39458fe4a --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel.Composition; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Users; + +namespace Microsoft.PowerApps.TestEngine.Modules +{ + /// + /// Container for loading up MEF components supported by the CLI. + /// + public class MefComponents + { +#pragma warning disable 0649 // Field 'MefModules' is never assigned to... Justification: Value set by MEF + [ImportMany] + public IEnumerable> MefModules; + + [ImportMany] + public IEnumerable> UserModules; + + [ImportMany] + public IEnumerable> WebProviderModules; +#pragma warning restore 0649 + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs new file mode 100644 index 000000000..75a225cf3 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace Microsoft.PowerApps.TestEngine.Modules +{ + /// + /// Check that references, types and called methods are allowed or denied + /// The assembly to be checked is not loaded into the AppDomain is is loaded with definition only for checks + /// + public class TestEngineExtensionChecker + { + ILogger _logger; + + public Func GetExtentionContents = (file) => File.ReadAllBytes(file); + + public TestEngineExtensionChecker() + { + + } + + public TestEngineExtensionChecker(ILogger logger) + { + _logger = logger; + } + + public ILogger Logger + { + get + { + return _logger; + } + set + { + _logger = value; + } + } + + public Func CheckCertificates = () => VerifyCertificates(); + + /// + /// Verify that the provided file is signed by a trusted X509 root certificate authentication provider and the certificate is still valid + /// + /// The test settings that should be evaluated + /// The .Net Assembly file to validate + /// True if the assembly can be verified, False if not + public virtual bool Verify(TestSettingExtensions settings, string file) + { + if (!CheckCertificates()) + { + return true; + } + + var cert = X509Certificate.CreateFromSignedFile(file); + var cert2 = new X509Certificate2(cert.GetRawCertData()); + + + X509Chain chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + + var valid = true; + chain.Build(cert2); + + var sources = GetTrustedSources(settings); + + var allowUntrustedRoot = false; + if (settings.Parameters.ContainsKey("AllowUntrustedRoot")) + { + allowUntrustedRoot = bool.Parse(settings.Parameters["AllowUntrustedRoot"]); + } + + foreach (var elem in chain.ChainElements) + { + foreach (var status in elem.ChainElementStatus) + { + if (status.Status == X509ChainStatusFlags.UntrustedRoot && allowUntrustedRoot) + { + continue; + } + valid = false; + } + } + + // Check if the chain of certificates is valid + if (!valid) + { + return false; + } + + // Check for valid trust sources + foreach (var elem in chain.ChainElements) + { + foreach (var source in sources) + { + if (!string.IsNullOrEmpty(source.Name) && elem.Certificate.IssuerName.Name.IndexOf($"CN={source.Name}") == -1) + { + continue; + } + if (!string.IsNullOrEmpty(source.Organization) && elem.Certificate.IssuerName.Name.IndexOf($"O={source.Organization}") == -1) + { + continue; + } + if (!string.IsNullOrEmpty(source.Location) && elem.Certificate.IssuerName.Name.IndexOf($"L={source.Location}") == -1) + { + continue; + } + if (!string.IsNullOrEmpty(source.State) && elem.Certificate.IssuerName.Name.IndexOf($"S={source.State}") == -1) + { + continue; + } + if (!string.IsNullOrEmpty(source.Country) && elem.Certificate.IssuerName.Name.IndexOf($"C={source.Country}") == -1) + { + continue; + } + if (!string.IsNullOrEmpty(source.Thumbprint) && elem.Certificate.Thumbprint != source.Thumbprint) + { + continue; + } + // Found a trusted source + return true; + } + } + return false; + } + + private static bool VerifyCertificates() + { +#if RELEASE + return true; +#endif + return false; + } + + private List GetTrustedSources(TestSettingExtensions settings) + { + var sources = new List(); + + sources.Add(new TestEngineTrustSource() + { + Name = "Microsoft Root Certificate Authority", + Organization = "Microsoft Corporation", + Location = "Redmond", + State = "Washington", + Country = "US", + Thumbprint = "8F43288AD272F3103B6FB1428485EA3014C0BCFE" + }); + + if (settings.Parameters.ContainsKey("TrustedSource")) + { + var parts = settings.Parameters["TrustedSource"].Split(','); + var name = string.Empty; + var organization = string.Empty; + var location = string.Empty; + var state = string.Empty; + var country = string.Empty; + var thumbprint = string.Empty; + + foreach (var part in parts) + { + var nameValue = part.Trim().Split('='); + switch (nameValue[0]) + { + case "CN": + name = nameValue[1]; + break; + case "O": + organization = nameValue[1]; + break; + case "L": + location = nameValue[1]; + break; + case "S": + state = nameValue[1]; + break; + case "C": + country = nameValue[1]; + break; + case "T": + thumbprint = nameValue[1]; + break; + } + } + if (!string.IsNullOrEmpty(name)) + { + sources.Add(new TestEngineTrustSource() + { + Name = name, + Organization = organization, + Location = location, + State = state, + Country = country, + Thumbprint = thumbprint + }); + } + } + + return sources; + } + + + /// + /// Validate that the provided file is allowed or should be denied based on the test settings + /// + /// The test settings that should be evaluated + /// The .Net Assembly file to validate + /// True if the assembly meets the test setting requirements, False if not + public virtual bool Validate(TestSettingExtensions settings, string file) + { + var contents = GetExtentionContents(file); + + var allowList = new List(settings.AllowNamespaces); + // Add minimum namespaces for a MEF plugin used by TestEngine + allowList.Add("System.Threading.Tasks"); + allowList.Add("Microsoft.PowerFx"); + allowList.Add("System.ComponentModel.Composition"); + allowList.Add("Microsoft.Extensions.Logging"); + allowList.Add("Microsoft.PowerApps.TestEngine."); + allowList.Add("Microsoft.Playwright"); + + var denyList = new List(settings.DenyNamespaces); + + var found = LoadTypes(contents); + + var valid = true; + + foreach (var item in found) + { + // Allow if what was found is shorter and starts with allow value or what was found is a subset of a more specific allow rule + var allowLongest = allowList.Where(a => item.StartsWith(a) || (item.Length < a.Length && a.StartsWith(item))).OrderByDescending(a => a.Length).FirstOrDefault(); + var denyLongest = denyList.Where(d => item.StartsWith(d)).OrderByDescending(d => d.Length).FirstOrDefault(); + var allow = !String.IsNullOrEmpty(allowLongest); + var deny = !String.IsNullOrEmpty(denyLongest); + + if (allow && deny && denyLongest?.Length > allowLongest?.Length || !allow && deny) + { + _logger.LogInformation("Deny usage of " + item); + _logger.LogInformation("Allow rule " + allowLongest); + _logger.LogInformation("Deny rule " + denyLongest); + valid = false; + } + } + + return valid; + } + + /// + /// Load all the types from the assembly using Intermediate Langiage (IL) mode only + /// + /// The byte representation of the assembly + /// The Dependancies, Types and Method calls found in the assembly + private List LoadTypes(byte[] assembly) + { + List found = new List(); + + using (var stream = new MemoryStream(assembly)) + { + stream.Position = 0; + ModuleDefinition module = ModuleDefinition.ReadModule(stream); + + // Add each assembly reference + foreach (var reference in module.AssemblyReferences) + { + if (!found.Contains(reference.Name)) + { + found.Add(reference.Name); + } + } + + foreach (TypeDefinition type in module.GetAllTypes()) + { + AddType(type, found); + + // Load each constructor parameter and types in the body + foreach (var constructor in type.GetConstructors()) + { + if (constructor.HasParameters) + { + LoadParametersTypes(constructor.Parameters, found); + } + if (constructor.HasBody) + { + LoadMethodBodyTypes(constructor.Body, found); + } + } + + // Load any fields + foreach (var field in type.Fields) + { + if (found.Contains(field.FieldType.FullName) && !field.FieldType.IsValueType) + { + found.Add(field.FieldType.FullName); + } + } + + // ... properties with get/set body if they exist + foreach (var property in type.Properties) + { + if (found.Contains(property.PropertyType.FullName) && !property.PropertyType.IsValueType) + { + found.Add(property.PropertyType.FullName); + } + if (property.GetMethod != null) + { + if (property.GetMethod.HasBody) + { + LoadMethodBodyTypes(property.GetMethod.Body, found); + } + } + if (property.SetMethod != null) + { + if (property.SetMethod.HasBody) + { + LoadMethodBodyTypes(property.SetMethod.Body, found); + } + } + } + + // and method parameters and types in the method body + foreach (var method in type.Methods) + { + if (method.HasParameters) + { + LoadParametersTypes(method.Parameters, found); + } + + if (method.HasBody) + { + LoadMethodBodyTypes(method.Body, found); + } + } + } + + return found; + } + } + + private void LoadParametersTypes(Mono.Collections.Generic.Collection paramInfo, List found) + { + foreach (var parameter in paramInfo) + { + AddType(parameter.ParameterType.GetElementType(), found); + } + } + + private void AddType(TypeReference type, List found) + { + if (!found.Contains(type.FullName) && !type.IsPrimitive) + { + found.Add(type.FullName); + } + } + + /// + /// Add method body instructions to the found list + /// + /// The body instructions to be searched + /// The list of matching code found + private void LoadMethodBodyTypes(MethodBody body, List found) + { + foreach (var variable in body.Variables) + { + AddType(variable.VariableType.GetElementType(), found); + } + foreach (var instruction in body.Instructions) + { + switch (instruction.OpCode.FlowControl) + { + case FlowControl.Call: + var methodInfo = (IMethodSignature)instruction.Operand; + AddType(methodInfo.ReturnType, found); + var name = methodInfo.ToString(); + if (name.IndexOf(" ") > 0) + { + // Remove the return type from the call definition + name = name.Substring(name.IndexOf(" ") + 1); + var start = name.IndexOf("("); + var args = name.Substring(start + 1, name.Length - start - 2).Split(','); + if (args.Length >= 1 && !string.IsNullOrEmpty(args[0])) + { + name = name.Substring(0, start) + GetArgs(args, instruction); + } + } + if (!found.Contains(name)) + { + found.Add(name); + } + break; + } + } + } + + /// + /// Convert call arguments into values + /// + /// The arguments to be converted + /// The call instruction that the arguments relate to + /// The call text with primative values or argument types + private string GetArgs(string[] args, Instruction instruction) + { + StringBuilder result = new StringBuilder("("); + + for (var i = 0; i < args.Length; i++) + { + var argValue = GetCallArgument(i, args.Length, instruction); + switch (args[i]) + { + case "System.String": + if (argValue.OpCode.Code == Code.Ldstr) + { + result.Append("\""); + result.Append(argValue.Operand.ToString()); + result.Append("\""); + } + else + { + result.Append(args[i]); + } + break; + default: + result.Append(args[i]); + break; + } + if (i != args.Length - 1) + { + result.Append(","); + } + } + + result.Append(")"); + return result.ToString(); + } + + /// + /// Get an argument for a method. They should be the nth intruction loaded before the method call + /// + /// The argument instruction to load + /// The total number of arguments + /// The call instruction + /// + private Instruction GetCallArgument(int index, int argCount, Instruction instruction) + { + Instruction current = instruction; + while (index < argCount) + { + current = current.Previous; + index++; + } + return current; + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs new file mode 100644 index 000000000..98bd6d80a --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel.Composition.Hosting; +using System.ComponentModel.Composition.Primitives; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using NuGet.Configuration; + +namespace Microsoft.PowerApps.TestEngine.Modules +{ + /// + /// Load matching Test Engine Managed Extensibility Framework modules + /// + public class TestEngineModuleMEFLoader + { + public Func DirectoryExists { get; set; } = (string location) => Directory.Exists(location); + + public Func DirectoryGetFiles { get; set; } = (string location, string searchPattern) => Directory.GetFiles(location, searchPattern); + + public Func LoadAssembly { get; set; } = (string file) => new AssemblyCatalog(file); + + public TestEngineExtensionChecker Checker { get; set; } + + ILogger _logger; + + public TestEngineModuleMEFLoader(ILogger logger) + { + _logger = logger; + Checker = new TestEngineExtensionChecker(logger); + } + + /// + /// Load matching modules using test settings from provided file system location + /// + /// The settings to use determine if extensions are enaabled and allow / deny settings + /// The file system location to read the modules from + /// Catalog of located modules + public AggregateCatalog LoadModules(TestSettingExtensions settings) + { + List match = new List() { }; + + if (settings.Enable) + { + _logger.LogInformation("Extensions enabled"); + + // Load MEF exports from this assembly + match.Add(new AssemblyCatalog(typeof(TestEngine).Assembly)); + + foreach (var sourcelocation in settings.Source.InstallSource) + { + string location = sourcelocation; + if (settings.Source.EnableNuGet) + { + var nuGetSettings = Settings.LoadDefaultSettings(null); + location = Path.Combine(SettingsUtility.GetGlobalPackagesFolder(nuGetSettings), location, "lib", "netstandard2.0"); + } + + if (!DirectoryExists(location)) + { + _logger.LogDebug("Skipping " + location); + continue; + } + + // Check if want all modules in the location + if (settings.DenyModule.Count == 0 && settings.AllowModule.Count() == 1 && settings.AllowModule[0].Equals("*") || settings.AllowModule.Count() == 0) + { + _logger.LogInformation("Load all modules from " + location); + + var files = DirectoryGetFiles(location, "testengine.module.*.dll"); + foreach (var file in files) + { + if (!string.IsNullOrEmpty(file)) + { + _logger.LogInformation(Path.GetFileName(file)); + if (settings.CheckAssemblies) + { + if (!Checker.Validate(settings, file)) + { + continue; + } + } + match.Add(LoadAssembly(file)); + } + } + } + + var possibleUserManager = DirectoryGetFiles(location, "testengine.user.*.dll"); + foreach (var possibleModule in possibleUserManager) + { + if ( Checker.Verify(settings, possibleModule)) + { + match.Add(LoadAssembly(possibleModule)); + } + } + + var possibleWebProviderModule = DirectoryGetFiles(location, "testengine.provider.*.dll"); + foreach (var possibleModule in possibleWebProviderModule) + { + if (Checker.Verify(settings, possibleModule)) + { + match.Add(LoadAssembly(possibleModule)); + } + } + + // Check if need to deny a module or a specific list of modules are allowed + if (settings.DenyModule.Count > 0 || (settings.AllowModule.Count() > 1)) + { + _logger.LogInformation("Load modules from " + location); + var possibleModules = DirectoryGetFiles(location, "testengine.module.*.dll"); + foreach (var possibleModule in possibleModules) + { + if (!string.IsNullOrEmpty(possibleModule)) + { + // Convert from testegine.module.name.dll format to name for search comparision + var moduleName = Path.GetFileNameWithoutExtension(possibleModule).Replace("testengine.module.", "").ToLower(); + var allow = settings.AllowModule.Any(a => Regex.IsMatch(moduleName, WildCardToRegular(a.ToLower()))); + var deny = settings.DenyModule.Any(d => Regex.IsMatch(moduleName, WildCardToRegular(d.ToLower()))); + var allowLongest = settings.AllowModule.Max(a => Regex.IsMatch(moduleName, WildCardToRegular(a.ToLower())) ? a : ""); + var denyLongest = settings.DenyModule.Max(d => Regex.IsMatch(moduleName, WildCardToRegular(d.ToLower())) ? d : ""); + + // Two cases: + // 1. Found deny but also found allow. Assume that the allow has higher proirity if a longer match + // allow | deny | add + // * | name | No + // name | * | Yes + // n* | name | No + // 2. No deny match found, allow is found + if (deny && allow && allowLongest.Length > denyLongest.Length || allow && !deny) + { + if (settings.CheckAssemblies) + { + if (!Checker.Validate(settings, possibleModule)) + { + continue; + } + } + _logger.LogInformation(Path.GetFileName(possibleModule)); + if (Checker.Verify(settings, possibleModule)) + { + match.Add(LoadAssembly(possibleModule)); + } + } + } + } + } + } + } + else + { + _logger.LogInformation("Extensions not enabled"); + } + + AggregateCatalog results = new AggregateCatalog(match); + return results; + } + + private static String WildCardToRegular(String value) + { + return "^" + Regex.Escape(value).Replace("\\*", ".*") + "$"; + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineTrustSource.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineTrustSource.cs new file mode 100644 index 000000000..113a15577 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineTrustSource.cs @@ -0,0 +1,46 @@ +namespace Microsoft.PowerApps.TestEngine.Modules +{ + /// + /// A wrapper object to store information about a tested certificate source + /// + /// + /// + public class TestEngineTrustSource + { + /// + /// The name of the certificate + /// + /// + public string Name { get; set; } + + /// + /// The organization who has issued the certificate + /// + /// + public string Organization { get; set; } + + /// + /// The location of the organization + /// + /// + public string Location { get; set; } + + /// + /// The state that the organization is within + /// + /// + public string State { get; set; } + + /// + /// The country code + /// + /// + public string Country { get; set; } + + /// + /// The thumbprint of the certificate + /// + /// + public string Thumbprint { get; set; } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/IUrlMapper.cs b/src/Microsoft.PowerApps.TestEngine/PowerApps/IUrlMapper.cs deleted file mode 100644 index c54c329f7..000000000 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/IUrlMapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerApps.TestEngine.PowerApps -{ - /// - /// Map urls - /// - public interface IUrlMapper - { - /// - /// Generates url for the test - /// - /// Test url - public string GenerateTestUrl(string domain, string queryParams); - } -} diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppsUrlMapper.cs b/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppsUrlMapper.cs deleted file mode 100644 index a20047da1..000000000 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppsUrlMapper.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.Config; - -namespace Microsoft.PowerApps.TestEngine.PowerApps -{ - /// - /// Map urls - /// - public class PowerAppsUrlMapper : IUrlMapper - { - private readonly ITestState _testState; - private readonly ISingleTestInstanceState _singleTestInstanceState; - - public PowerAppsUrlMapper(ITestState testState, ISingleTestInstanceState singleTestInstanceState) - { - _testState = testState; - _singleTestInstanceState = singleTestInstanceState; - } - - public string GenerateTestUrl(string domain, string additionalQueryParams) - { - var environment = _testState.GetEnvironment(); - if (string.IsNullOrEmpty(environment)) - { - _singleTestInstanceState.GetLogger().LogError("Environment cannot be empty."); - throw new InvalidOperationException(); - } - - var testSuiteDefinition = _singleTestInstanceState.GetTestSuiteDefinition(); - if (testSuiteDefinition == null) - { - _singleTestInstanceState.GetLogger().LogError("Test definition must be specified."); - throw new InvalidOperationException(); - } - - var appLogicalName = testSuiteDefinition.AppLogicalName; - var appId = testSuiteDefinition.AppId; - - if (string.IsNullOrEmpty(appLogicalName) && string.IsNullOrEmpty(appId)) - { - _singleTestInstanceState.GetLogger().LogError("At least one of the App Logical Name or App Id must be defined."); - throw new InvalidOperationException(); - } - - var tenantId = _testState.GetTenant(); - if (string.IsNullOrEmpty(tenantId)) - { - _singleTestInstanceState.GetLogger().LogError("Tenant cannot be empty."); - throw new InvalidOperationException(); - } - - var queryParametersForTestUrl = GetQueryParametersForTestUrl(tenantId, additionalQueryParams); - - return !string.IsNullOrEmpty(appLogicalName) ? - $"https://{domain}/play/e/{environment}/an/{appLogicalName}{queryParametersForTestUrl}" : - $"https://{domain}/play/e/{environment}/a/{appId}{queryParametersForTestUrl}"; - } - - private static string GetQueryParametersForTestUrl(string tenantId, string additionalQueryParams) - { - return $"?tenantId={tenantId}&source=testengine{additionalQueryParams}"; - } - } -} diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectOneParamFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectOneParamFunction.cs index 39bb80073..ebdb61932 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectOneParamFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectOneParamFunction.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; @@ -15,13 +15,13 @@ namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions /// public class SelectOneParamFunction : ReflectionFunction { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly Func _updateModelFunction; protected readonly ILogger _logger; - public SelectOneParamFunction(IPowerAppFunctions powerAppFunctions, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty()) + public SelectOneParamFunction(ITestWebProvider testWebProvider, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty()) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; _updateModelFunction = updateModelFunction; _logger = logger; } @@ -46,7 +46,7 @@ private async Task SelectAsync(RecordValue obj) } var powerAppControlModel = (ControlRecordValue)obj; - var result = await _powerAppFunctions.SelectControlAsync(powerAppControlModel.GetItemPath()); + var result = await _testWebProvider.SelectControlAsync(powerAppControlModel.GetItemPath()); if (!result) { diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectThreeParamsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectThreeParamsFunction.cs index fa2e07556..9352a51d0 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectThreeParamsFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectThreeParamsFunction.cs @@ -3,8 +3,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; @@ -16,13 +16,13 @@ namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions /// public class SelectThreeParamsFunction : ReflectionFunction { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly Func _updateModelFunction; protected readonly ILogger _logger; - public SelectThreeParamsFunction(IPowerAppFunctions powerAppFunctions, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty(), FormulaType.Number, RecordType.Empty()) + public SelectThreeParamsFunction(ITestWebProvider TestWebProvider, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty(), FormulaType.Number, RecordType.Empty()) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = TestWebProvider; _updateModelFunction = updateModelFunction; _logger = logger; } @@ -60,8 +60,8 @@ private async Task SelectAsync(RecordValue obj, NumberValue rowOrColumn, RecordV }; var recordType = RecordType.Empty().Add(childControlName, RecordType.Empty()); - var powerAppControlModel = new ControlRecordValue(recordType, _powerAppFunctions, childControlName, parentItemPath); - var result = await _powerAppFunctions.SelectControlAsync(powerAppControlModel.GetItemPath()); + var powerAppControlModel = new ControlRecordValue(recordType, _testWebProvider, childControlName, parentItemPath); + var result = await _testWebProvider.SelectControlAsync(powerAppControlModel.GetItemPath()); if (!result) { diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectTwoParamsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectTwoParamsFunction.cs index 74e5f9a6b..0c3c829e6 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectTwoParamsFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/Select/SelectTwoParamsFunction.cs @@ -3,8 +3,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; @@ -16,13 +16,13 @@ namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions /// public class SelectTwoParamsFunction : ReflectionFunction { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _TestWebProvider; private readonly Func _updateModelFunction; private readonly ILogger _logger; - public SelectTwoParamsFunction(IPowerAppFunctions powerAppFunctions, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty(), FormulaType.Number) + public SelectTwoParamsFunction(ITestWebProvider TestWebProvider, Func updateModelFunction, ILogger logger) : base("Select", FormulaType.Blank, RecordType.Empty(), FormulaType.Number) { - _powerAppFunctions = powerAppFunctions; + _TestWebProvider = TestWebProvider; _updateModelFunction = updateModelFunction; _logger = logger; } @@ -52,9 +52,9 @@ private async Task SelectAsync(RecordValue obj, NumberValue rowOrColumn) }; var recordType = RecordType.Empty().Add(controlName, RecordType.Empty()); - var powerAppControlModel = new ControlRecordValue(recordType, _powerAppFunctions, controlName); + var powerAppControlModel = new ControlRecordValue(recordType, _TestWebProvider, controlName); - var result = await _powerAppFunctions.SelectControlAsync(itemPath); + var result = await _TestWebProvider.SelectControlAsync(itemPath); if (!result) { diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetPropertyFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetPropertyFunction.cs index 6526e120e..b01c0400e 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetPropertyFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetPropertyFunction.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx; @@ -18,12 +18,12 @@ namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions /// public class SetPropertyFunction : ReflectionFunction { - protected readonly IPowerAppFunctions _powerAppFunctions; + protected readonly ITestWebProvider _testWebProvider; protected readonly ILogger _logger; - public SetPropertyFunction(IPowerAppFunctions powerAppFunctions, ILogger logger) : base("SetProperty", FormulaType.Blank, RecordType.Empty(), FormulaType.String, FormulaType.Boolean) + public SetPropertyFunction(ITestWebProvider testWebProvider, ILogger logger) : base("SetProperty", FormulaType.Blank, RecordType.Empty(), FormulaType.String, FormulaType.Boolean) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; _logger = logger; } @@ -41,7 +41,7 @@ protected async Task SetProperty(RecordValue obj, StringValue propName, FormulaV NullCheckHelper.NullCheck(obj, propName, value, _logger); var controlModel = (ControlRecordValue)obj; - var result = await _powerAppFunctions.SetPropertyAsync(controlModel.GetItemPath(propName.Value), value); + var result = await _testWebProvider.SetPropertyAsync(controlModel.GetItemPath(propName.Value), value); if (!result) { diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/WaitFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/WaitFunction.cs index b0af1331e..4642f5691 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/WaitFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/WaitFunction.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/IPowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/IPowerFxEngine.cs index 14fd81773..ce8e46ab7 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/IPowerFxEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/IPowerFxEngine.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Text.RegularExpressions; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerFx.Types; namespace Microsoft.PowerApps.TestEngine.PowerFx @@ -47,8 +47,13 @@ public interface IPowerFxEngine public Task RunRequirementsCheckAsync(); /// - /// get PowerAppFunctions + /// Get Web Provider instance /// - public IPowerAppFunctions GetPowerAppFunctions(); + public ITestWebProvider GetWebProvider(); + + /// + /// Disables checking Power Apps state checks + /// + public bool PowerAppIntegrationEnabled { get; set; } } } diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs index ee55b6751..e5adf2258 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs @@ -5,12 +5,14 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; +using NuGet.Configuration; +using System.Text.RegularExpressions; namespace Microsoft.PowerApps.TestEngine.PowerFx { @@ -19,41 +21,71 @@ namespace Microsoft.PowerApps.TestEngine.PowerFx /// public class PowerFxEngine : IPowerFxEngine { - private readonly ITestInfraFunctions _testInfraFunctions; - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestInfraFunctions TestInfraFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly IFileSystem _fileSystem; - private readonly ISingleTestInstanceState _singleTestInstanceState; - private readonly ITestState _testState; + private readonly ISingleTestInstanceState SingleTestInstanceState; + private readonly ITestState TestState; private int _retryLimit = 2; private RecalcEngine Engine { get; set; } - private ILogger Logger { get { return _singleTestInstanceState.GetLogger(); } } + private ILogger Logger { get { return SingleTestInstanceState.GetLogger(); } } public PowerFxEngine(ITestInfraFunctions testInfraFunctions, - IPowerAppFunctions powerAppFunctions, + ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) { - _testInfraFunctions = testInfraFunctions; - _powerAppFunctions = powerAppFunctions; - _singleTestInstanceState = singleTestInstanceState; - _testState = testState; + TestInfraFunctions = testInfraFunctions; + _testWebProvider = testWebProvider; + SingleTestInstanceState = singleTestInstanceState; + TestState = testState; _fileSystem = fileSystem; } public void Setup() { - var powerFxConfig = new PowerFxConfig(); + var powerFxConfig = new PowerFxConfig(Features.PowerFxV1); - powerFxConfig.AddFunction(new SelectOneParamFunction(_powerAppFunctions, async () => await UpdatePowerFxModelAsync(), Logger)); - powerFxConfig.AddFunction(new SelectTwoParamsFunction(_powerAppFunctions, async () => await UpdatePowerFxModelAsync(), Logger)); - powerFxConfig.AddFunction(new SelectThreeParamsFunction(_powerAppFunctions, async () => await UpdatePowerFxModelAsync(), Logger)); - powerFxConfig.AddFunction(new ScreenshotFunction(_testInfraFunctions, _singleTestInstanceState, _fileSystem, Logger)); + var vals = new SymbolValues(); + var symbols = (SymbolTable)vals.SymbolTable; + symbols.EnableMutationFunctions(); + powerFxConfig.SymbolTable = symbols; + + //TODO: Remove + powerFxConfig.EnableSetFunction(); + //TODO: End + + powerFxConfig.AddFunction(new SelectOneParamFunction(_testWebProvider, async () => await UpdatePowerFxModelAsync(), Logger)); + powerFxConfig.AddFunction(new SelectTwoParamsFunction(_testWebProvider, async () => await UpdatePowerFxModelAsync(), Logger)); + powerFxConfig.AddFunction(new SelectThreeParamsFunction(_testWebProvider, async () => await UpdatePowerFxModelAsync(), Logger)); + powerFxConfig.AddFunction(new ScreenshotFunction(TestInfraFunctions, SingleTestInstanceState, _fileSystem, Logger)); powerFxConfig.AddFunction(new AssertWithoutMessageFunction(Logger)); powerFxConfig.AddFunction(new AssertFunction(Logger)); - powerFxConfig.AddFunction(new SetPropertyFunction(_powerAppFunctions, Logger)); - WaitRegisterExtensions.RegisterAll(powerFxConfig, _testState.GetTimeout(), Logger); + powerFxConfig.AddFunction(new SetPropertyFunction(_testWebProvider, Logger)); + + var settings = TestState.GetTestSettings(); + if (settings != null && settings.ExtensionModules != null && settings.ExtensionModules.Enable) + { + if (TestState.GetTestEngineModules().Count == 0) + { + Logger.LogError("Extension enabled not none loaded"); + } + foreach (var module in TestState.GetTestEngineModules()) + { + module.RegisterPowerFxFunction(powerFxConfig, TestInfraFunctions, _testWebProvider, SingleTestInstanceState, TestState, _fileSystem); + } + } + else + { + if (TestState.GetTestEngineModules().Count > 0) + { + Logger.LogInformation("Extension loaded but not enabled"); + } + } + + WaitRegisterExtensions.RegisterAll(powerFxConfig, TestState.GetTimeout(), Logger); Engine = new RecalcEngine(powerFxConfig); } @@ -102,6 +134,7 @@ public FormulaValue Execute(string testSteps, CultureInfo culture) } var goStepByStep = false; + // Check if the syntax is correct var checkResult = Engine.Check(testSteps, null, GetPowerFxParserOptions(culture)); if (!checkResult.IsSuccess) @@ -125,6 +158,7 @@ public FormulaValue Execute(string testSteps, CultureInfo culture) } else { + var values = new SymbolValues(); Logger.LogTrace($"Attempting:\n\n{{\n{testSteps}}}"); return Engine.Eval(testSteps, null, new ParserOptions() { AllowsSideEffects = true, Culture = culture, NumberIsFloat = true }); } @@ -138,9 +172,14 @@ public async Task UpdatePowerFxModelAsync() throw new InvalidOperationException(); } - await PollingHelper.PollAsync(false, (x) => !x, () => _powerAppFunctions.CheckIfAppIsIdleAsync(), _testState.GetTestSettings().Timeout, _singleTestInstanceState.GetLogger(), "Something went wrong when Test Engine tried to get App status."); + if (!PowerAppIntegrationEnabled) + { + return; + } - var controlRecordValues = await _powerAppFunctions.LoadPowerAppsObjectModelAsync(); + await PollingHelper.PollAsync(false, (x) => !x, () => _testWebProvider.CheckIsIdleAsync(), TestState.GetTestSettings().Timeout, SingleTestInstanceState.GetLogger(), "Something went wrong when Test Engine tried to get App status."); + + var controlRecordValues = await _testWebProvider.LoadObjectModelAsync(); foreach (var control in controlRecordValues) { Engine.UpdateVariable(control.Key, control.Value); @@ -154,15 +193,17 @@ private static ParserOptions GetPowerFxParserOptions(CultureInfo culture) return new ParserOptions() { AllowsSideEffects = true, Culture = culture, NumberIsFloat = true }; } - public IPowerAppFunctions GetPowerAppFunctions() + public ITestWebProvider GetWebProvider() { - return _powerAppFunctions; + return _testWebProvider; } public async Task RunRequirementsCheckAsync() { - await _powerAppFunctions.CheckAndHandleIfLegacyPlayerAsync(); - await _powerAppFunctions.TestEngineReady(); + await _testWebProvider.CheckProviderAsync(); + await _testWebProvider.TestEngineReady(); } + + public bool PowerAppIntegrationEnabled { get; set; } = true; } } diff --git a/src/Microsoft.PowerApps.TestEngine/Providers/ITestProviderState.cs b/src/Microsoft.PowerApps.TestEngine/Providers/ITestProviderState.cs new file mode 100644 index 000000000..49f22b8d2 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Providers/ITestProviderState.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerApps.TestEngine.Providers +{ + public interface ITestProviderState + { + public object GetState(); + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/IPowerAppFunctions.cs b/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs similarity index 66% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/IPowerAppFunctions.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs index e3c6a5a35..84b9fa635 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/IPowerAppFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs @@ -1,17 +1,46 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; +using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx.Types; -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// - /// Functions for interacting with the Power App + /// Functions for interacting with the a web based resource to test /// - public interface IPowerAppFunctions + public interface ITestWebProvider { + #nullable enable + public ITestInfraFunctions? TestInfraFunctions { get; set; } + + public ISingleTestInstanceState? SingleTestInstanceState { get; set; } + + public ITestState? TestState { get; set; } + + public ITestProviderState? ProviderState { get; set; } + #nullable disable + + /// + /// The name of the provider + /// + public string Name { get; } + + /// + /// Verify if provider is usable + /// + public Task CheckProviderAsync(); + + public string CheckTestEngineObject { get; } + + /// + /// Generates url for the test + /// + /// Test url + public string GenerateTestUrl(string domain, string queryParams); + /// /// Gets the value of a property from a control. /// @@ -39,7 +68,7 @@ public interface IPowerAppFunctions /// Loads the object model for Power Apps /// /// Power Apps object model - public Task> LoadPowerAppsObjectModelAsync(); + public Task> LoadObjectModelAsync(); /// /// Gets the number of items in an array @@ -48,16 +77,11 @@ public interface IPowerAppFunctions /// Number of items in the array public int GetItemCount(ItemPath itemPath); - /// - /// Verify if using legacy player - /// - public Task CheckAndHandleIfLegacyPlayerAsync(); - /// /// Check if app status returns 'idle' or 'busy' /// /// True if app status is idle - public Task CheckIfAppIsIdleAsync(); + public Task CheckIsIdleAsync(); /// /// Get Debug Info diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/ItemPath.cs b/src/Microsoft.PowerApps.TestEngine/Providers/ItemPath.cs similarity index 95% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/ItemPath.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/ItemPath.cs index 7557b2ba4..f7f10b789 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/ItemPath.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/ItemPath.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// /// Describes the path to access an item diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSControlModel.cs b/src/Microsoft.PowerApps.TestEngine/Providers/JSControlModel.cs similarity index 91% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/JSControlModel.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/JSControlModel.cs index 23a7d5eea..e979fdcbf 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSControlModel.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/JSControlModel.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// /// Object model for a control returned from javascript diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSObjectModel.cs b/src/Microsoft.PowerApps.TestEngine/Providers/JSObjectModel.cs similarity index 87% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/JSObjectModel.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/JSObjectModel.cs index 652818155..1176ebbbd 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSObjectModel.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/JSObjectModel.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// /// Object model returned from javascript diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyModel.cs b/src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyModel.cs similarity index 81% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyModel.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyModel.cs index 2dba0375b..064053236 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyModel.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyModel.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { public class JSPropertyModel { diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyValueModel.cs b/src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyValueModel.cs similarity index 88% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyValueModel.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyValueModel.cs index bf586de25..7cd971393 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/JSPropertyValueModel.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/JSPropertyValueModel.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// /// Object model for a property value returned from JavaScript diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlRecordValue.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs similarity index 90% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlRecordValue.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs index 28120a81e..dc7477475 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlRecordValue.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs @@ -12,14 +12,14 @@ using Microsoft.PowerFx.Types; using Newtonsoft.Json; -namespace Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel +namespace Microsoft.PowerApps.TestEngine.Providers.PowerFxModel { /// /// This is a Power FX RecordValue created to represent a control or a control property /// public class ControlRecordValue : RecordValue { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly string _name; private readonly ItemPath _parentItemPath; @@ -27,13 +27,13 @@ public class ControlRecordValue : RecordValue /// Creates a ControlRecordValue /// /// Record type for the control record value - /// Power App functions so that the property values can be fetched + /// Power App functions so that the property values can be fetched /// Our logger object /// Name of the control /// Path to the parent control - public ControlRecordValue(RecordType type, IPowerAppFunctions powerAppFunctions, string name = null, ItemPath parentItemPath = null) : base(type) + public ControlRecordValue(RecordType type, ITestWebProvider testWebProvider, string name = null, ItemPath parentItemPath = null) : base(type) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; _parentItemPath = parentItemPath; _name = name; } @@ -79,8 +79,8 @@ protected override bool TryGetField(FormulaType fieldType, string fieldName, out var tableType = fieldType as TableType; var recordType = tableType.ToRecord(); // Create indexable table source - var tableSource = new ControlTableSource(_powerAppFunctions, GetItemPath(fieldName), recordType); - var table = new ControlTableValue(recordType, tableSource, _powerAppFunctions); + var tableSource = new ControlTableSource(_testWebProvider, GetItemPath(fieldName), recordType); + var table = new ControlTableValue(recordType, tableSource, _testWebProvider); result = table; return true; } @@ -90,13 +90,13 @@ protected override bool TryGetField(FormulaType fieldType, string fieldName, out if (string.IsNullOrEmpty(_name)) { // We reach here if we are referencing a child item in a Gallery. Eg. Index(Gallery1.AllItems).Label1 (fieldName = Label1) - result = new ControlRecordValue(recordType, _powerAppFunctions, fieldName, _parentItemPath); + result = new ControlRecordValue(recordType, _testWebProvider, fieldName, _parentItemPath); return true; } else { // We reach here if we are referencing a child item in a component. Eg. Component1.Label1 (fieldName = Label1) - result = new ControlRecordValue(recordType, _powerAppFunctions, fieldName, GetItemPath()); + result = new ControlRecordValue(recordType, _testWebProvider, fieldName, GetItemPath()); return true; } } @@ -105,7 +105,7 @@ protected override bool TryGetField(FormulaType fieldType, string fieldName, out // We reach here if we are referencing a terminating property of a control, Eg. Label1.Text (fieldName = Text) var itemPath = GetItemPath(fieldName); - var propertyValueJson = _powerAppFunctions.GetPropertyValueFromControl(itemPath); + var propertyValueJson = _testWebProvider.GetPropertyValueFromControl(itemPath); var jsPropertyValueModel = JsonConvert.DeserializeObject(propertyValueJson); if (jsPropertyValueModel != null) diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableRowSchema.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableRowSchema.cs similarity index 91% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableRowSchema.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableRowSchema.cs index 480dc296a..ee3f9e7ea 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableRowSchema.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableRowSchema.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerFx.Types; -namespace Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel +namespace Microsoft.PowerApps.TestEngine.Providers.PowerFxModel { /// /// Schema for a row in a table representing a control or it's property diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableSource.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableSource.cs similarity index 84% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableSource.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableSource.cs index 200f4ee44..2bd6c03ff 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableSource.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableSource.cs @@ -10,20 +10,20 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerFx.Types; -namespace Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel +namespace Microsoft.PowerApps.TestEngine.Providers.PowerFxModel { /// /// Source of a table representing a control or property /// public class ControlTableSource : IReadOnlyList { - private readonly IPowerAppFunctions _powerAppFunctions; + private readonly ITestWebProvider _testWebProvider; private readonly ItemPath _itemPath; public RecordType RecordType { get; set; } - public ControlTableSource(IPowerAppFunctions powerAppFunctions, ItemPath itemPath, RecordType recordType) + public ControlTableSource(ITestWebProvider testWebProvider, ItemPath itemPath, RecordType recordType) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; _itemPath = itemPath; RecordType = recordType; } @@ -44,7 +44,7 @@ public int Count get { // Always have to go fetch the count as it could dynamically change - return _powerAppFunctions.GetItemCount(_itemPath); + return _testWebProvider.GetItemCount(_itemPath); } } diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableValue.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableValue.cs similarity index 69% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableValue.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableValue.cs index 3b8850a87..b8eaa419e 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/ControlTableValue.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlTableValue.cs @@ -9,22 +9,22 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerFx.Types; -namespace Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel +namespace Microsoft.PowerApps.TestEngine.Providers.PowerFxModel { /// /// Class representing a Power FX TableValue for a Control or property /// public class ControlTableValue : CollectionTableValue { - private readonly IPowerAppFunctions _powerAppFunctions; - public ControlTableValue(RecordType recordType, IEnumerable source, IPowerAppFunctions powerAppFunctions) : base(recordType, source) + private readonly ITestWebProvider _testWebProvider; + public ControlTableValue(RecordType recordType, IEnumerable source, ITestWebProvider testWebProvider) : base(recordType, source) { - _powerAppFunctions = powerAppFunctions; + _testWebProvider = testWebProvider; } protected override DValue Marshal(ControlTableRowSchema item) { - var recordValue = new ControlRecordValue(item.RecordType, _powerAppFunctions, parentItemPath: item.ItemPath); + var recordValue = new ControlRecordValue(item.RecordType, _testWebProvider, parentItemPath: item.ItemPath); return DValue.Of(recordValue); } } diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/TypeMapping.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/TypeMapping.cs similarity index 98% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/TypeMapping.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/TypeMapping.cs index 4dc68fb04..474d27601 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerFxModel/TypeMapping.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/TypeMapping.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.PowerFx.Types; -namespace Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel +namespace Microsoft.PowerApps.TestEngine.Providers.PowerFxModel { /// /// Maps Power Apps internal types to Power FX types diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/RecordValueObject.cs b/src/Microsoft.PowerApps.TestEngine/Providers/RecordValueObject.cs similarity index 86% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/RecordValueObject.cs rename to src/Microsoft.PowerApps.TestEngine/Providers/RecordValueObject.cs index 37df85134..28cf15f73 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/RecordValueObject.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/RecordValueObject.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { public class RecordValueObject { diff --git a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs index 7913ae5a2..198c239fc 100644 --- a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs +++ b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; @@ -22,13 +22,16 @@ public class SingleTestRunner : ISingleTestRunner { private readonly ITestReporter _testReporter; private readonly IPowerFxEngine _powerFxEngine; - private readonly ITestInfraFunctions _testInfraFunctions; + private readonly ITestInfraFunctions TestInfraFunctions; private readonly IUserManager _userManager; - private readonly ISingleTestInstanceState _testState; - private readonly IUrlMapper _urlMapper; + private readonly ITestState _state; + private readonly ISingleTestInstanceState TestState; private readonly IFileSystem _fileSystem; private readonly ILoggerFactory _loggerFactory; private readonly ITestEngineEvents _eventHandler; + private readonly IEnvironmentVariable _environmentVariable; + private ITestWebProvider _testWebProvider; + private ILogger Logger { get; set; } private bool TestSuccess { get; set; } = true; @@ -39,21 +42,25 @@ public SingleTestRunner(ITestReporter testReporter, IPowerFxEngine powerFxEngine, ITestInfraFunctions testInfraFunctions, IUserManager userManager, + ITestState state, ISingleTestInstanceState testState, - IUrlMapper urlMapper, IFileSystem fileSystem, ILoggerFactory loggerFactory, - ITestEngineEvents eventHandler) + ITestEngineEvents eventHandler, + IEnvironmentVariable environmentVariable, + ITestWebProvider testWebProvider) { _testReporter = testReporter; _powerFxEngine = powerFxEngine; - _testInfraFunctions = testInfraFunctions; + TestInfraFunctions = testInfraFunctions; _userManager = userManager; - _testState = testState; - _urlMapper = urlMapper; + _state = state; + TestState = testState; _fileSystem = fileSystem; _loggerFactory = loggerFactory; _eventHandler = eventHandler; + _environmentVariable = environmentVariable; + _testWebProvider = testWebProvider; } public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSuiteDefinition testSuiteDefinition, BrowserConfiguration browserConfig, string domain, string queryParams, CultureInfo locale) @@ -79,16 +86,16 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu var desiredUrl = ""; Logger = _loggerFactory.CreateLogger(testSuiteId); - _testState.SetLogger(Logger); + TestState.SetLogger(Logger); - _testState.SetTestSuiteDefinition(testSuiteDefinition); - _testState.SetTestRunId(testRunId); - _testState.SetBrowserConfig(browserConfig); + TestState.SetTestSuiteDefinition(testSuiteDefinition); + TestState.SetTestRunId(testRunId); + TestState.SetBrowserConfig(browserConfig); var testResultDirectory = Path.Combine(testRunDirectory, $"{_fileSystem.RemoveInvalidFileNameChars(testSuiteName)}_{browserConfigName}_{testSuiteId.Substring(0, 6)}"); - _testState.SetTestResultsDirectory(testResultDirectory); + TestState.SetTestResultsDirectory(testResultDirectory); - casesTotal = _testState.GetTestSuiteDefinition().TestCases.Count(); + casesTotal = TestState.GetTestSuiteDefinition().TestCases.Count(); // Number of total cases are recorded and also initialize the passed cases to 0 for this test run _eventHandler.SetAndInitializeCounters(casesTotal); @@ -106,22 +113,26 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu Logger.LogInformation($"Browser configuration: {JsonConvert.SerializeObject(browserConfig)}"); // Set up test infra - await _testInfraFunctions.SetupAsync(); + await TestInfraFunctions.SetupAsync(_userManager); Logger.LogInformation("Test infrastructure setup finished"); - desiredUrl = _urlMapper.GenerateTestUrl(domain, queryParams); + _testWebProvider.TestState = _state; + _testWebProvider.SingleTestInstanceState = TestState; + _testWebProvider.TestInfraFunctions = TestInfraFunctions; + + desiredUrl = _testWebProvider.GenerateTestUrl(domain, queryParams); Logger.LogInformation($"Desired URL: {desiredUrl}"); _eventHandler.SuiteBegin(testSuiteName, testRunDirectory, browserConfigName, desiredUrl); // Navigate to test url - await _testInfraFunctions.GoToUrlAsync(desiredUrl); + await TestInfraFunctions.GoToUrlAsync(desiredUrl); Logger.LogInformation("Successfully navigated to target URL"); _testReporter.TestRunAppURL = desiredUrl; // Log in user - await _userManager.LoginAsUserAsync(desiredUrl); + await _userManager.LoginAsUserAsync(desiredUrl, TestInfraFunctions.GetContext(), _state, TestState, _environmentVariable); // Set up Power Fx _powerFxEngine.Setup(); @@ -129,25 +140,25 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu await _powerFxEngine.UpdatePowerFxModelAsync(); // Set up network request mocking if any - await _testInfraFunctions.SetupNetworkRequestMockAsync(); + await TestInfraFunctions.SetupNetworkRequestMockAsync(); allTestsSkipped = false; // Run test case one by one - foreach (var testCase in _testState.GetTestSuiteDefinition().TestCases) + foreach (var testCase in TestState.GetTestSuiteDefinition().TestCases) { _eventHandler.TestCaseBegin(testCase.TestCaseName); TestSuccess = true; var testId = _testReporter.CreateTest(testRunId, testSuiteId, $"{testCase.TestCaseName}"); _testReporter.StartTest(testRunId, testId); - _testState.SetTestId(testId); + TestState.SetTestId(testId); using (var scope = Logger.BeginScope(testId)) { var testCaseResultDirectory = Path.Combine(testResultDirectory, $"{testCase.TestCaseName}_{testId.Substring(0, 6)}"); - _testState.SetTestResultsDirectory(testCaseResultDirectory); + TestState.SetTestResultsDirectory(testCaseResultDirectory); _fileSystem.CreateDirectory(testCaseResultDirectory); string caseException = null; @@ -219,7 +230,7 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu if (!string.IsNullOrEmpty(testSuiteDefinition.OnTestSuiteComplete)) { Logger.LogInformation($"Running OnTestSuiteComplete for test suite: {testSuiteName}"); - _testState.SetTestResultsDirectory(testResultDirectory); + TestState.SetTestResultsDirectory(testResultDirectory); _powerFxEngine.Execute(testSuiteDefinition.OnTestSuiteComplete, locale); } } @@ -236,17 +247,20 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu finally { // Trying to log the debug info including session details - // Consider avoiding calling DebugInfo in cases where the PowerAppsTestEngine object is not needed + + // Consider avoiding calling DebugInfo in cases where the provider object is not needed // Like exceptions thrown during initialization failures or user input errors - LoggingHelper loggingHelper = new LoggingHelper(_powerFxEngine.GetPowerAppFunctions(), _testState, _eventHandler); + var provider = _powerFxEngine.GetWebProvider(); + provider.TestInfraFunctions = TestInfraFunctions; + LoggingHelper loggingHelper = new LoggingHelper(provider, TestState, _eventHandler); loggingHelper.DebugInfo(); - await _testInfraFunctions.EndTestRunAsync(); + await TestInfraFunctions.EndTestRunAsync(); if (allTestsSkipped) { // Run test case one by one, mark it as failed - foreach (var testCase in _testState.GetTestSuiteDefinition().TestCases) + foreach (var testCase in TestState.GetTestSuiteDefinition().TestCases) { var testId = _testReporter.CreateTest(testRunId, testSuiteId, $"{testCase.TestCaseName}"); _testReporter.FailTest(testRunId, testId); @@ -272,7 +286,7 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu var testLogger = TestLoggerProvider.TestLoggers[testSuiteId]; testLogger.WriteExceptionToDebugLogsFile(testResultDirectory, suiteException); } - await _testInfraFunctions.DisposeAsync(); + await TestInfraFunctions.DisposeAsync(); } } } diff --git a/src/Microsoft.PowerApps.TestEngine/TestEngine.cs b/src/Microsoft.PowerApps.TestEngine/TestEngine.cs index 32949a811..313be2aa7 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestEngine.cs @@ -7,8 +7,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; namespace Microsoft.PowerApps.TestEngine { @@ -101,21 +104,24 @@ public async Task RunTestAsync(FileInfo testConfigFile, string environme Logger.LogDebug($"Using query: {queryParams}"); } + _state.ParseAndSetTestState(testConfigFile.FullName, Logger); + _state.SetEnvironment(environmentId); + _state.SetTenant(tenantId.ToString()); + _state.LoadExtensionModules(Logger); + + _state.SetDomain(domain); + Logger.LogDebug($"Using domain: {domain}"); + // Create the output directory as early as possible so that any exceptions can be logged. _state.SetOutputDirectory(outputDirectory.FullName); Logger.LogDebug($"Using output directory: {outputDirectory.FullName}"); + _state.SetTestConfigFile(testConfigFile); + testRunDirectory = Path.Combine(_state.GetOutputDirectory(), testRunId.Substring(0, 6)); _fileSystem.CreateDirectory(testRunDirectory); Logger.LogInformation($"Test results will be stored in: {testRunDirectory}"); - _state.ParseAndSetTestState(testConfigFile.FullName, Logger); - _state.SetEnvironment(environmentId); - _state.SetTenant(tenantId.ToString()); - - _state.SetDomain(domain); - Logger.LogDebug($"Using domain: {domain}"); - await RunTestByBrowserAsync(testRunId, testRunDirectory, domain, queryParams); _testReporter.EndTestRun(testRunId); return _testReporter.GenerateTestReport(testRunId, testRunDirectory); diff --git a/src/Microsoft.PowerApps.TestEngine/TestEngineEventHandler.cs b/src/Microsoft.PowerApps.TestEngine/TestEngineEventHandler.cs index 94800b20d..d95cdabce 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestEngineEventHandler.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestEngineEventHandler.cs @@ -17,7 +17,7 @@ public class TestEngineEventHandler : ITestEngineEvents // NOTE: Any changes to these messages need to be handled in the consuming tool's console event handler, like in pac cli tool. // These console messages need to be considered for localization. - public static string UserAppExceptionMessage = " [Critical Error] Could not access PowerApps. For more details, check the logs."; + public static string UserAppExceptionMessage = " [Critical Error] Could not access Provider. For more details, check the logs."; public static string UserInputExceptionInvalidFilePathMessage = " Invalid file path. For more details, check the logs."; public static string UserInputExceptionInvalidOutputPathMessage = " [Critical Error]: The output directory provided is invalid."; public static string UserInputExceptionInvalidTestSettingsMessage = " Invalid test settings specified in testconfig. For more details, check the logs."; diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs index 38d167ebf..daa0d48a6 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Users; namespace Microsoft.PowerApps.TestEngine.TestInfra { @@ -11,11 +13,17 @@ namespace Microsoft.PowerApps.TestEngine.TestInfra /// public interface ITestInfraFunctions { + /// + /// Return the current browser context + /// + /// The current browser context + public IBrowserContext GetContext(); + /// /// Setup the test infrastructure /// /// Task - public Task SetupAsync(); + public Task SetupAsync(IUserManager userManager); /// /// Setup the network request mocking @@ -79,21 +87,5 @@ public interface ITestInfraFunctions /// Javascript expression to run /// Return value of javascript public Task RunJavascriptAsync(string jsExpression); - - /// - /// Fills in user email - /// - /// Selector to find element - /// Value to fill in - /// Task - public Task HandleUserEmailScreen(string selector, string value); - - /// - /// Fills in user password - /// - /// Selector to find element - /// Value to fill in - /// Task - public Task HandleUserPasswordScreen(string selector, string value, string desiredUrl); } } diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs index be9d1b299..ed6095fb6 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Runtime; using Microsoft.Extensions.Logging; using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.Users; namespace Microsoft.PowerApps.TestEngine.TestInfra { @@ -18,34 +20,44 @@ public class PlaywrightTestInfraFunctions : ITestInfraFunctions private readonly ITestState _testState; private readonly ISingleTestInstanceState _singleTestInstanceState; private readonly IFileSystem _fileSystem; + private readonly ITestWebProvider _testWebProvider; public static string BrowserNotSupportedErrorMessage = "Browser not supported by Playwright, for more details check https://playwright.dev/dotnet/docs/browsers"; private IPlaywright PlaywrightObject { get; set; } private IBrowser Browser { get; set; } private IBrowserContext BrowserContext { get; set; } private IPage Page { get; set; } + - public PlaywrightTestInfraFunctions(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem) + public PlaywrightTestInfraFunctions(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, ITestWebProvider testWebProvider) { _testState = testState; _singleTestInstanceState = singleTestInstanceState; _fileSystem = fileSystem; + _testWebProvider = testWebProvider; } // Constructor to aid with unit testing public PlaywrightTestInfraFunctions(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, - IPlaywright playwrightObject = null, IBrowserContext browserContext = null, IPage page = null) : this(testState, singleTestInstanceState, fileSystem) + IPlaywright playwrightObject = null, IBrowserContext browserContext = null, IPage page = null, ITestWebProvider testWebProvider = null) : this(testState, singleTestInstanceState, fileSystem, testWebProvider) { PlaywrightObject = playwrightObject; Page = page; BrowserContext = browserContext; } - public async Task SetupAsync() + public IBrowserContext GetContext() + { + return BrowserContext; + } + + public async Task SetupAsync(IUserManager userManager) { var browserConfig = _singleTestInstanceState.GetBrowserConfig(); + var staticContext = new BrowserTypeLaunchPersistentContextOptions(); + if (browserConfig == null) { _singleTestInstanceState.GetLogger().LogError("Browser config cannot be null"); @@ -77,6 +89,9 @@ public async Task SetupAsync() Timeout = testSettings.Timeout }; + staticContext.Headless = launchOptions.Headless; + staticContext.Timeout = launchOptions.Timeout; + var browser = PlaywrightObject[browserConfig.Browser]; if (browser == null) { @@ -84,8 +99,17 @@ public async Task SetupAsync() throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionInvalidTestSettings.ToString()); } - Browser = await browser.LaunchAsync(launchOptions); - _singleTestInstanceState.GetLogger().LogInformation("Browser setup finished"); + if (!userManager.UseStaticContext) + { + // Check if a channel has been specified + if (testSettings.BrowserConfigurations.Any(c => !string.IsNullOrEmpty(c.Channel))) + { + launchOptions.Channel = testSettings.BrowserConfigurations.First(c => !string.IsNullOrEmpty(c.Channel)).Channel; + } + + Browser = await browser.LaunchAsync(launchOptions); + _singleTestInstanceState.GetLogger().LogInformation("Browser setup finished"); + } var contextOptions = new BrowserNewContextOptions(); @@ -97,6 +121,7 @@ public async Task SetupAsync() if (testSettings.RecordVideo) { contextOptions.RecordVideoDir = _singleTestInstanceState.GetTestResultsDirectory(); + staticContext.RecordVideoDir = contextOptions.RecordVideoDir; } if (browserConfig.ScreenWidth != null && browserConfig.ScreenHeight != null) @@ -106,9 +131,44 @@ public async Task SetupAsync() Width = browserConfig.ScreenWidth.Value, Height = browserConfig.ScreenHeight.Value }; + staticContext.RecordVideoSize = new RecordVideoSize() + { + Width = browserConfig.ScreenWidth.Value, + Height = browserConfig.ScreenHeight.Value, + }; + } + + if (testSettings.ExtensionModules != null && testSettings.ExtensionModules.Enable) + { + foreach (var module in _testState.GetTestEngineModules()) + { + module.ExtendBrowserContextOptions(contextOptions, testSettings); + } + } + + if (userManager.UseStaticContext) + { + _fileSystem.CreateDirectory(userManager.Location); + var location = userManager.Location; + if (!Path.IsPathRooted(location)) + { + location = Path.Combine(Directory.GetCurrentDirectory(), location); + } + _singleTestInstanceState.GetLogger().LogInformation($"Using static context in '{location}' using {userManager.Name}"); + + // Check if a channel has been specified + if (testSettings.BrowserConfigurations.Any(c => !string.IsNullOrEmpty(c.Channel))) + { + staticContext.Channel = testSettings.BrowserConfigurations.First(c => !string.IsNullOrEmpty(c.Channel)).Channel; + } + + BrowserContext = await browser.LaunchPersistentContextAsync(location, staticContext); + } + else + { + BrowserContext = await Browser.NewContextAsync(contextOptions); } - BrowserContext = await Browser.NewContextAsync(contextOptions); _singleTestInstanceState.GetLogger().LogInformation("Browser context created"); } @@ -129,20 +189,35 @@ public async Task SetupNetworkRequestMockAsync() foreach (var mock in mocks) { - - if (string.IsNullOrEmpty(mock.RequestURL)) + if (mock.IsExtension) { - _singleTestInstanceState.GetLogger().LogError("RequestURL cannot be null"); - throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString()); + foreach (var module in _testState.GetTestEngineModules()) + { + await module.RegisterNetworkRoute(_testState, _singleTestInstanceState, _fileSystem, Page, mock); + } } - - if (!_fileSystem.IsValidFilePath(mock.ResponseDataFile) || !_fileSystem.FileExists(mock.ResponseDataFile)) + else { - _singleTestInstanceState.GetLogger().LogError("ResponseDataFile is invalid or missing"); - throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionInvalidFilePath.ToString()); - } + if (string.IsNullOrEmpty(mock.RequestURL)) + { + _singleTestInstanceState.GetLogger().LogError("RequestURL cannot be null"); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString()); + } + + if (string.IsNullOrEmpty(mock.RequestURL)) + { + _singleTestInstanceState.GetLogger().LogError("RequestURL cannot be null"); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString()); + } - await Page.RouteAsync(mock.RequestURL, async route => await RouteNetworkRequest(route, mock)); + if (!_fileSystem.IsValidFilePath(mock.ResponseDataFile) || !_fileSystem.FileExists(mock.ResponseDataFile)) + { + _singleTestInstanceState.GetLogger().LogError("ResponseDataFile is invalid or missing"); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionInvalidFilePath.ToString()); + } + + await Page.RouteAsync(mock.RequestURL, async route => await RouteNetworkRequest(route, mock)); + } } } @@ -195,10 +270,13 @@ public async Task GoToUrlAsync(string url) throw new InvalidOperationException(); } - if (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp) + if ((uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp)) { - _singleTestInstanceState.GetLogger().LogError("Url must be http/https"); - throw new InvalidOperationException(); + if ( url != "about:blank") + { + _singleTestInstanceState.GetLogger().LogError("Url must be http/https"); + throw new InvalidOperationException(); + } } if (Page == null) @@ -290,102 +368,12 @@ public async Task RunJavascriptAsync(string jsExpression) { ValidatePage(); - if (!jsExpression.Equals(PowerAppFunctions.CheckPowerAppsTestEngineObject)) + if (!jsExpression.Equals(_testWebProvider.CheckTestEngineObject)) { _singleTestInstanceState.GetLogger().LogDebug("Run Javascript: " + jsExpression); } return await Page.EvaluateAsync(jsExpression); } - - // Justification: Limited ability to run unit tests for - // Playwright actions on the sign-in page - [ExcludeFromCodeCoverage] - public async Task HandleUserEmailScreen(string selector, string value) - { - ValidatePage(); - await Page.Locator(selector).WaitForAsync(); - await Page.TypeAsync(selector, value, new PageTypeOptions { Delay = 50 }); - await Page.Keyboard.PressAsync("Tab", new KeyboardPressOptions { Delay = 20 }); - } - - public async Task HandleUserPasswordScreen(string selector, string value, string desiredUrl) - { - var logger = _singleTestInstanceState.GetLogger(); - - ValidatePage(); - - try - { - // Find the password box - await Page.Locator(selector).WaitForAsync(); - - // Fill in the password - await Page.FillAsync(selector, value); - - // Submit password form - await this.ClickAsync("input[type=\"submit\"]"); - - PageWaitForSelectorOptions selectorOptions = new PageWaitForSelectorOptions(); - selectorOptions.Timeout = 8000; - - // For instances where there is a 'Stay signed in?' dialogue box - try - { - logger.LogDebug("Checking if asked to stay signed in."); - - // Check if we received a 'Stay signed in?' box? - await Page.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", selectorOptions); - logger.LogDebug("Was asked to 'stay signed in'."); - - // Click to stay signed in - await Page.ClickAsync("[id=\"idBtn_Back\"]"); - } - // If there is no 'Stay signed in?' box, an exception will throw; just catch and continue - catch (Exception ssiException) - { - logger.LogDebug("Exception encountered: " + ssiException.ToString()); - - // Keep record if passwordError was encountered - bool hasPasswordError = false; - - try - { - selectorOptions.Timeout = 2000; - - // Check if we received a password error - await Page.WaitForSelectorAsync("[id=\"passwordError\"]", selectorOptions); - hasPasswordError = true; - } - catch (Exception peException) - { - logger.LogDebug("Exception encountered: " + peException.ToString()); - } - - // If encountered password error, exit program - if (hasPasswordError) - { - logger.LogError("Incorrect password entered. Make sure you are using the correct credentials."); - throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); - } - // If not, continue - else - { - logger.LogDebug("Did not encounter an invalid password error."); - } - - logger.LogDebug("Was not asked to 'stay signed in'."); - } - - await Page.WaitForURLAsync(desiredUrl); - } - catch (TimeoutException) - { - logger.LogError("Timed out during login attempt. In order to determine why, it may be beneficial to view the output recording. Make sure that your login credentials are correct."); - throw new TimeoutException(); - } - - logger.LogDebug("Logged in successfully."); - } } } diff --git a/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs b/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs index 51230a4be..9e7e0432d 100644 --- a/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs +++ b/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; namespace Microsoft.PowerApps.TestEngine.Users { @@ -10,10 +13,42 @@ namespace Microsoft.PowerApps.TestEngine.Users /// public interface IUserManager { + /// + /// The name of the user manager as multiple Manager instances may exist + /// + public string Name { get; } + + /// + /// The relative priority order of the user manager of multiple macthes are found. Higher values will be prioritized first + /// + public int Priority { get; } + + /// + /// Determines if the user manager should use static context to provide cached user state as part of a test session + /// + /// True if static context, False if not + public bool UseStaticContext { get; } + + /// + /// The location to use for this user session + /// + /// Path or resource where the user session should be located + public string Location { get; set; } + /// /// Log in as user for currently running test /// + /// The location to open after a successful login + /// The current open browser context state + /// The current overall tset state being executed + /// The instance of the running test + /// Provides access to environment to enable successfull login /// Task - public Task LoginAsUserAsync(string desiredUrl); + public Task LoginAsUserAsync( + string desiredUrl, + IBrowserContext context, + ITestState testState, + ISingleTestInstanceState singleTestInstanceState, + IEnvironmentVariable environmentVariable); } } diff --git a/src/Microsoft.PowerApps.TestEngine/Users/UserManager.cs b/src/Microsoft.PowerApps.TestEngine/Users/UserManager.cs deleted file mode 100644 index a738d36fe..000000000 --- a/src/Microsoft.PowerApps.TestEngine/Users/UserManager.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using Microsoft.Extensions.Logging; -using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; -using Microsoft.PowerApps.TestEngine.System; -using Microsoft.PowerApps.TestEngine.TestInfra; - -namespace Microsoft.PowerApps.TestEngine.Users -{ - /// - /// Handles anything related to the user - /// - public class UserManager : IUserManager - { - private readonly ITestInfraFunctions _testInfraFunctions; - private readonly ITestState _testState; - private readonly ISingleTestInstanceState _singleTestInstanceState; - private readonly IEnvironmentVariable _environmentVariable; - - private const string EmailSelector = "input[type=\"email\"]"; - private const string PasswordSelector = "input[type=\"password\"]"; - private const string SubmitButtonSelector = "input[type=\"submit\"]"; - private const string KeepMeSignedInNoSelector = "[id=\"idBtn_Back\"]"; - - - public UserManager(ITestInfraFunctions testInfraFunctions, ITestState testState, - ISingleTestInstanceState singleTestInstanceState, IEnvironmentVariable environmentVariable) - { - _testInfraFunctions = testInfraFunctions; - _testState = testState; - _singleTestInstanceState = singleTestInstanceState; - _environmentVariable = environmentVariable; - } - - public async Task LoginAsUserAsync(string desiredUrl) - { - var testSuiteDefinition = _singleTestInstanceState.GetTestSuiteDefinition(); - var logger = _singleTestInstanceState.GetLogger(); - - if (testSuiteDefinition == null) - { - logger.LogError("Test definition cannot be null"); - throw new InvalidOperationException(); - } - - if (string.IsNullOrEmpty(testSuiteDefinition.Persona)) - { - logger.LogError("Persona cannot be empty"); - throw new InvalidOperationException(); - } - - var userConfig = _testState.GetUserConfiguration(testSuiteDefinition.Persona); - - if (userConfig == null) - { - logger.LogError("Cannot find user config for persona"); - throw new InvalidOperationException(); - } - - if (string.IsNullOrEmpty(userConfig.EmailKey)) - { - logger.LogError("Email key for persona cannot be empty"); - throw new InvalidOperationException(); - } - - if (string.IsNullOrEmpty(userConfig.PasswordKey)) - { - logger.LogError("Password key for persona cannot be empty"); - throw new InvalidOperationException(); - } - - var user = _environmentVariable.GetVariable(userConfig.EmailKey); - var password = _environmentVariable.GetVariable(userConfig.PasswordKey); - - bool missingUserOrPassword = false; - - if (string.IsNullOrEmpty(user)) - { - logger.LogError(("User email cannot be null. Please check if the environment variable is set properly.")); - missingUserOrPassword = true; - } - - if (string.IsNullOrEmpty(password)) - { - logger.LogError("Password cannot be null. Please check if the environment variable is set properly."); - missingUserOrPassword = true; - } - - if (missingUserOrPassword) - { - throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); - } - - await _testInfraFunctions.HandleUserEmailScreen(EmailSelector, user); - - await _testInfraFunctions.ClickAsync(SubmitButtonSelector); - - // Wait for the sliding animation to finish - await Task.Delay(1000); - - await _testInfraFunctions.HandleUserPasswordScreen(PasswordSelector, password, desiredUrl); - } - } -} diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index e9bd31401..16efe04c3 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -14,6 +14,38 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.sample", "testengine.module.sample\testengine.module.sample.csproj", "{89C38E35-85CA-454C-B80A-F5BD5BFE9FE3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.provider.canvas", "testengine.provider.canvas\testengine.provider.canvas.csproj", "{6AFE2228-38CA-4D73-9008-BF67F4442706}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.provider.canvas.tests", "testengine.provider.canvas.tests\testengine.provider.canvas.tests.csproj", "{1F097AC5-D840-4005-A8AF-527B51734138}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{63A04DC1-C37E-43E6-8FEA-A480483E11F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{0B61ADD8-5EED-4A2C-99AA-B597EC3EE223}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{D53FFBF2-F4D0-4139-9FD3-47C8216E4448}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.environment", "testengine.user.environment\testengine.user.environment.csproj", "{24E6C3CF-8CEC-4091-A217-378C74D30339}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.environment.tests", "testengine.user.environment.tests\testengine.user.environment.tests.csproj", "{B91EFA35-C28B-497E-BFDC-8497933393A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.browser", "testengine.user.browser\testengine.user.browser.csproj", "{2778A59F-773D-414E-A1FF-9C5B5F90E28F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.browser.tests", "testengine.user.browser.tests\testengine.user.browser.tests.csproj", "{8AEAF6BD-38E3-4649-9221-6A67AD1E96EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{D34E437A-6149-46EC-B7DA-FF449E55CEEA}" + ProjectSection(SolutionItems) = preProject + ..\Extensions.md = ..\Extensions.md + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause", "testengine.module.pause\testengine.module.pause.csproj", "{B3A02421-223D-4E80-A8CE-977B425A6EB2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause.tests", "testengine.module.pause.tests\testengine.module.pause.tests.csproj", "{3D9F90F2-0937-486D-AA0B-BFE425354F4A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,10 +64,60 @@ Global {05883A68-F545-495C-9D5D-D0A4CBC6879E}.Debug|Any CPU.Build.0 = Debug|Any CPU {05883A68-F545-495C-9D5D-D0A4CBC6879E}.Release|Any CPU.ActiveCfg = Release|Any CPU {05883A68-F545-495C-9D5D-D0A4CBC6879E}.Release|Any CPU.Build.0 = Release|Any CPU + {89C38E35-85CA-454C-B80A-F5BD5BFE9FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89C38E35-85CA-454C-B80A-F5BD5BFE9FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89C38E35-85CA-454C-B80A-F5BD5BFE9FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89C38E35-85CA-454C-B80A-F5BD5BFE9FE3}.Release|Any CPU.Build.0 = Release|Any CPU + {6AFE2228-38CA-4D73-9008-BF67F4442706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AFE2228-38CA-4D73-9008-BF67F4442706}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AFE2228-38CA-4D73-9008-BF67F4442706}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AFE2228-38CA-4D73-9008-BF67F4442706}.Release|Any CPU.Build.0 = Release|Any CPU + {1F097AC5-D840-4005-A8AF-527B51734138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F097AC5-D840-4005-A8AF-527B51734138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F097AC5-D840-4005-A8AF-527B51734138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F097AC5-D840-4005-A8AF-527B51734138}.Release|Any CPU.Build.0 = Release|Any CPU + {24E6C3CF-8CEC-4091-A217-378C74D30339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24E6C3CF-8CEC-4091-A217-378C74D30339}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24E6C3CF-8CEC-4091-A217-378C74D30339}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24E6C3CF-8CEC-4091-A217-378C74D30339}.Release|Any CPU.Build.0 = Release|Any CPU + {B91EFA35-C28B-497E-BFDC-8497933393A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B91EFA35-C28B-497E-BFDC-8497933393A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B91EFA35-C28B-497E-BFDC-8497933393A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B91EFA35-C28B-497E-BFDC-8497933393A0}.Release|Any CPU.Build.0 = Release|Any CPU + {2778A59F-773D-414E-A1FF-9C5B5F90E28F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2778A59F-773D-414E-A1FF-9C5B5F90E28F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2778A59F-773D-414E-A1FF-9C5B5F90E28F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2778A59F-773D-414E-A1FF-9C5B5F90E28F}.Release|Any CPU.Build.0 = Release|Any CPU + {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC}.Release|Any CPU.Build.0 = Release|Any CPU + {B3A02421-223D-4E80-A8CE-977B425A6EB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3A02421-223D-4E80-A8CE-977B425A6EB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3A02421-223D-4E80-A8CE-977B425A6EB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3A02421-223D-4E80-A8CE-977B425A6EB2}.Release|Any CPU.Build.0 = Release|Any CPU + {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {89C38E35-85CA-454C-B80A-F5BD5BFE9FE3} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {6AFE2228-38CA-4D73-9008-BF67F4442706} = {D53FFBF2-F4D0-4139-9FD3-47C8216E4448} + {1F097AC5-D840-4005-A8AF-527B51734138} = {D53FFBF2-F4D0-4139-9FD3-47C8216E4448} + {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} = {63A04DC1-C37E-43E6-8FEA-A480483E11F8} + {D53FFBF2-F4D0-4139-9FD3-47C8216E4448} = {63A04DC1-C37E-43E6-8FEA-A480483E11F8} + {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} = {63A04DC1-C37E-43E6-8FEA-A480483E11F8} + {24E6C3CF-8CEC-4091-A217-378C74D30339} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {B91EFA35-C28B-497E-BFDC-8497933393A0} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {2778A59F-773D-414E-A1FF-9C5B5F90E28F} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {B3A02421-223D-4E80-A8CE-977B425A6EB2} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {3D9F90F2-0937-486D-AA0B-BFE425354F4A} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7E7B2C01-DDE2-4C5A-96C3-AF474B074331} EndGlobalSection diff --git a/src/PowerAppsTestEngine/InputOptions.cs b/src/PowerAppsTestEngine/InputOptions.cs index 99dfa149a..39509fe90 100644 --- a/src/PowerAppsTestEngine/InputOptions.cs +++ b/src/PowerAppsTestEngine/InputOptions.cs @@ -12,5 +12,8 @@ public class InputOptions public string? LogLevel { get; set; } public string? QueryParams { get; set; } public string? Domain { get; set; } + public string? Modules { get; set; } + public string? UserAuth { get; set; } + public string? Provider { get; set; } } } diff --git a/src/PowerAppsTestEngine/PowerAppsTestEngine.csproj b/src/PowerAppsTestEngine/PowerAppsTestEngine.csproj index 6877d58d6..a354e351e 100644 --- a/src/PowerAppsTestEngine/PowerAppsTestEngine.csproj +++ b/src/PowerAppsTestEngine/PowerAppsTestEngine.csproj @@ -13,6 +13,7 @@ + diff --git a/src/PowerAppsTestEngine/Program.cs b/src/PowerAppsTestEngine/Program.cs index a01b00444..b0007c511 100644 --- a/src/PowerAppsTestEngine/Program.cs +++ b/src/PowerAppsTestEngine/Program.cs @@ -1,12 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.ComponentModel.Composition; +using System.ComponentModel.Composition.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; @@ -22,7 +25,10 @@ { "-o", "OutputDirectory" }, { "-l", "LogLevel" }, { "-q", "QueryParams" }, - { "-d", "Domain" } + { "-d", "Domain" }, + { "-m", "Modules" }, + { "-u", "UserAuth" }, + { "-p", "Provider" } }; var inputOptions = new ConfigurationBuilder() @@ -115,32 +121,63 @@ Enum.TryParse(inputOptions.LogLevel, true, out logLevel); } - try + var userAuth ="browser"; // Default to brower authentication + if (!string.IsNullOrEmpty(inputOptions.UserAuth)) { + userAuth = inputOptions.UserAuth; + } - var serviceProvider = new ServiceCollection() - .AddLogging(loggingBuilder => - { - loggingBuilder + var provider = "canvas"; + if (!string.IsNullOrEmpty(inputOptions.Provider)) + { + provider = inputOptions.Provider; + } + + try + { + using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder .ClearProviders() .AddFilter(l => l >= logLevel) - .AddProvider(new TestLoggerProvider(new FileSystem())); - }) + .AddProvider(new TestLoggerProvider(new FileSystem()))); + + var logger = loggerFactory.CreateLogger(); + + var serviceProvider = new ServiceCollection() + .AddSingleton(loggerFactory) .AddSingleton() - .AddScoped() .AddSingleton() .AddScoped() - .AddScoped() + .AddScoped(sp => + { + var testState = sp.GetRequiredService(); + var userManagers = testState.GetTestEngineUserManager(); + if (userManagers.Count == 0) + { + testState.LoadExtensionModules(logger); + userManagers = testState.GetTestEngineUserManager(); + } + return userManagers.Where(x => x.Name.Equals(userAuth)).First(); + }) + .AddTransient(sp => { + var testState = sp.GetRequiredService(); + var testWebProviders = testState.GetTestEngineWebProviders(); + if (testWebProviders.Count == 0) + { + testState.LoadExtensionModules(logger); + testWebProviders = testState.GetTestEngineWebProviders(); + } + return testWebProviders.Where(x => x.Name.Equals(provider)).First(); + }) .AddSingleton() - .AddScoped() - .AddScoped() .AddSingleton() .AddScoped() .AddScoped() .AddScoped((sp) => sp.GetRequiredService().GetLogger()) .AddSingleton() + .AddScoped() .AddSingleton() .AddSingleton() + .BuildServiceProvider(); TestEngine testEngine = serviceProvider.GetRequiredService(); @@ -176,6 +213,16 @@ domain = inputOptions.Domain; } + string modulePath = Path.GetDirectoryName(typeof(Program).Assembly.Location); + List modules = new List(); + if (!string.IsNullOrEmpty(inputOptions.Modules) && Directory.Exists(inputOptions.Modules)) + { + modulePath = inputOptions.Modules; + } + + ITestState state = serviceProvider.GetService(); + state.SetModulePath(modulePath); + //setting defaults for optional parameters outside RunTestAsync var testResult = await testEngine.RunTestAsync(testPlanFile, environmentId, tenantId, outputDirectory, domain, queryParams); if (testResult != "InvalidOutputDirectory") @@ -187,5 +234,8 @@ catch (Exception ex) { Console.WriteLine("[Critical Error]: " + ex.Message); + Console.WriteLine(ex); } } + + diff --git a/src/PowerAppsTestEngine/Properties/launchSettings.json b/src/PowerAppsTestEngine/Properties/launchSettings.json new file mode 100644 index 000000000..1cf8eaef6 --- /dev/null +++ b/src/PowerAppsTestEngine/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "PowerAppsTestEngine": { + "commandName": "Project", + "commandLineArgs": "-i ..\\..\\..\\samples\\pause\\testPlan.fx.yaml -u browser" + } + } +} \ No newline at end of file diff --git a/src/testengine.module.pause.tests/PauseFunctionTests.cs b/src/testengine.module.pause.tests/PauseFunctionTests.cs new file mode 100644 index 000000000..cd310bceb --- /dev/null +++ b/src/testengine.module.pause.tests/PauseFunctionTests.cs @@ -0,0 +1,102 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; + +namespace testengine.module.browserlocale.tests +{ + public class PauseFunctionTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + + public PauseFunctionTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + } + + [Fact] + public void PauseExecute() + { + // Arrange + + var module = new PauseFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockLogger.Object); + var settings = new TestSettings() { Headless = false }; + var mockContext = new Mock(MockBehavior.Strict); + var mockPage = new Mock(MockBehavior.Strict); + + MockTestState.Setup(x => x.GetTestSettings()).Returns(settings); + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new List() { mockPage.Object }); + mockPage.Setup(x => x.PauseAsync()).Returns(Task.CompletedTask); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + // Act + module.Execute(); + + // Assert + MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Successfully finished executing Pause function."), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void SkipExecute() + { + // Arrange + + var module = new PauseFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockLogger.Object); + var settings = new TestSettings() { Headless = true }; + var mockContext = new Mock(MockBehavior.Strict); + var mockPage = new Mock(MockBehavior.Strict); + + MockTestState.Setup(x => x.GetTestSettings()).Returns(settings); + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new List() { mockPage.Object }); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + // Act + module.Execute(); + + // Assert + MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Skip Pause function as in headless mode."), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + } +} \ No newline at end of file diff --git a/src/testengine.module.pause.tests/PauseModuleTests.cs b/src/testengine.module.pause.tests/PauseModuleTests.cs new file mode 100644 index 000000000..7d1f0dfeb --- /dev/null +++ b/src/testengine.module.pause.tests/PauseModuleTests.cs @@ -0,0 +1,89 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; + +namespace testengine.module.browserlocale.tests +{ + public class PauseModuleTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + + public PauseModuleTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + } + + [Fact] + public void ExtendBrowserContextOptionsLocaleUpdate() + { + // Arrange + var module = new PauseModule(); + var options = new BrowserNewContextOptions(); + var settings = new TestSettings() { }; + + // Act + module.ExtendBrowserContextOptions(options, settings); + } + + [Fact] + public void RegisterPowerFxFunction() + { + // Arrange + var module = new PauseModule(); + + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + + // Act + module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + + // Assert + MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Registered Pause()"), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public async Task RegisterNetworkRoute() + { + // Arrange + var module = new PauseModule(); + + + // Act + await module.RegisterNetworkRoute(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPage.Object, TestNetworkRequestMock); + + // Assert + } + } +} \ No newline at end of file diff --git a/src/testengine.module.pause.tests/Usings.cs b/src/testengine.module.pause.tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/src/testengine.module.pause.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/testengine.module.pause.tests/testengine.module.pause.tests.csproj b/src/testengine.module.pause.tests/testengine.module.pause.tests.csproj new file mode 100644 index 000000000..fe0a013af --- /dev/null +++ b/src/testengine.module.pause.tests/testengine.module.pause.tests.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/testengine.module.pause/PauseFunction.cs b/src/testengine.module.pause/PauseFunction.cs new file mode 100644 index 000000000..2c37ae713 --- /dev/null +++ b/src/testengine.module.pause/PauseFunction.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx.Types; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; + +namespace testengine.module +{ + /// + /// This will pause the current test and allow the user to interact with the browser and inspect state when headless mode is false + /// + public class PauseFunction : ReflectionFunction + { + private readonly ITestInfraFunctions _testInfraFunctions; + private readonly ITestState _testState; + private readonly ILogger _logger; + + public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testState, ILogger logger) + : base("Pause", FormulaType.Blank) + { + _testInfraFunctions = testInfraFunctions; + _testState = testState; + _logger = logger; + } + + public BlankValue Execute() + { + _logger.LogInformation("------------------------------\n\n" + + "Executing Pause function."); + + if (!_testState.GetTestSettings().Headless) + { + var page = _testInfraFunctions.GetContext().Pages.First(); + page.PauseAsync().Wait(); + _logger.LogInformation("Successfully finished executing Pause function."); + } + else + { + _logger.LogInformation("Skip Pause function as in headless mode."); + } + + return FormulaValue.NewBlank(); + } + } +} + diff --git a/src/testengine.module.pause/PauseModule.cs b/src/testengine.module.pause/PauseModule.cs new file mode 100644 index 000000000..2be127a89 --- /dev/null +++ b/src/testengine.module.pause/PauseModule.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx; +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Playwright; + +namespace testengine.module +{ + [Export(typeof(ITestEngineModule))] + public class PauseModule : ITestEngineModule + { + public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings) + { + + } + + public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) + { + ILogger logger = singleTestInstanceState.GetLogger(); + config.AddFunction(new PauseFunction(testInfraFunctions, testState, logger)); + logger.LogInformation("Registered Pause()"); + } + + public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock) + { + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/testengine.module.pause/testengine.module.pause.csproj b/src/testengine.module.pause/testengine.module.pause.csproj new file mode 100644 index 000000000..2aab259f0 --- /dev/null +++ b/src/testengine.module.pause/testengine.module.pause.csproj @@ -0,0 +1,51 @@ + + + + net6.0 + enable + enable + Microsoft + crmsdk,Microsoft + Alpha Release: Pause browser PowerFx action MEF extension + + Notice: + This package is an ALPHA release. - Use at your own risk. + + Intial Alpha release of Microsoft.PowerAppsTestEngine + + true + ../../35MSSharedLib1024.snk + true + © Microsoft Corporation. All rights reserved. + true + 1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/testengine.module.sample/SampleFunction.cs b/src/testengine.module.sample/SampleFunction.cs new file mode 100644 index 000000000..031648a96 --- /dev/null +++ b/src/testengine.module.sample/SampleFunction.cs @@ -0,0 +1,18 @@ +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace testengine.module.sample +{ + public class SampleFunction : ReflectionFunction + { + public SampleFunction() : base("Sample", FormulaType.Blank) + { + } + + public BlankValue Execute() + { + Console.WriteLine("!!! SAMPLE !!!"); + return BlankValue.NewBlank(); + } + } +} diff --git a/src/testengine.module.sample/SampleModule.cs b/src/testengine.module.sample/SampleModule.cs new file mode 100644 index 000000000..ef034c402 --- /dev/null +++ b/src/testengine.module.sample/SampleModule.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace testengine.module.sample +{ + [Export(typeof(ITestEngineModule))] + public class TestEngineSampleModule : ITestEngineModule + { + public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings) + { + + } + + public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) + { + ILogger logger = singleTestInstanceState.GetLogger(); + config.AddFunction(new SampleFunction()); + logger.LogInformation("Registered Sample()"); + } + + public async Task RegisterNetworkRoute(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock) + { + return; + } + } +} diff --git a/src/testengine.module.sample/testengine.module.sample.csproj b/src/testengine.module.sample/testengine.module.sample.csproj new file mode 100644 index 000000000..ff7b8abc8 --- /dev/null +++ b/src/testengine.module.sample/testengine.module.sample.csproj @@ -0,0 +1,43 @@ + + + + net6.0 + enable + enable + Microsoft + crmsdk,Microsoft + Alpha Release: Sample action MEF extension + + Notice: + This package is an ALPHA release. - Use at your own risk. + + Intial Alpha release of Microsoft.PowerAppsTestEngine + + true + ../../35MSSharedLib1024.snk + true + © Microsoft Corporation. All rights reserved. + true + 1.0 + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppFunctionsTest.cs b/src/testengine.provider.canvas.tests/PowerAppFunctionsTest.cs similarity index 92% rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppFunctionsTest.cs rename to src/testengine.provider.canvas.tests/PowerAppFunctionsTest.cs index 6cb253ed7..f881c4722 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerApps/PowerAppFunctionsTest.cs +++ b/src/testengine.provider.canvas.tests/PowerAppFunctionsTest.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; @@ -304,7 +304,7 @@ public void GetPropertyValueFromControlThrowsOnInvalidInputTest(string itemPathS } [Fact] - public async Task LoadPowerAppsObjectModelAsyncTest() + public async Task LoadObjectModelAsyncTest() { var expectedFormulaTypes = TestData.CreateExpectedFormulaTypesForSampleJsPropertyModelList(); var button1RecordType = RecordType.Empty(); @@ -345,7 +345,7 @@ public async Task LoadPowerAppsObjectModelAsyncTest() MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - var objectModel = await powerAppFunctions.LoadPowerAppsObjectModelAsync(); + var objectModel = await powerAppFunctions.LoadObjectModelAsync(); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, (string)null, LogLevel.Debug, Times.Exactly(2)); @@ -368,7 +368,7 @@ public async Task LoadPowerAppsObjectModelAsyncTest() } [Fact] - public async Task LoadPowerAppsObjectModelAsyncWithDuplicatesDoesNotThrowTest() + public async Task LoadObjectModelAsyncWithDuplicatesDoesNotThrowTest() { var jsObjectModel = new JSObjectModel() { @@ -394,7 +394,7 @@ public async Task LoadPowerAppsObjectModelAsyncWithDuplicatesDoesNotThrowTest() MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - var objectModel = await powerAppFunctions.LoadPowerAppsObjectModelAsync(); + var objectModel = await powerAppFunctions.LoadObjectModelAsync(); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, (string)"Control: Label1 already added", LogLevel.Trace, Times.Once()); @@ -407,7 +407,7 @@ public async Task LoadPowerAppsObjectModelAsyncWithDuplicatesDoesNotThrowTest() [InlineData("")] [InlineData("{}")] [InlineData("{ controls: [] }")] - public async Task LoadPowerAppsObjectModelAsyncWithNoModelTest(string jsObjectModelString) + public async Task LoadObjectModelAsyncWithNoModelTest(string jsObjectModelString) { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -417,7 +417,7 @@ public async Task LoadPowerAppsObjectModelAsyncWithNoModelTest(string jsObjectMo MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); @@ -520,7 +520,7 @@ public void GetItemCountThrowsOnInvalidInputTest(string itemPathString) } [Fact] - public async Task LoadPowerAppsObjectModelAsyncWaitsForAppToLoad() + public async Task LoadObjectModelAsyncWaitsForAppToLoad() { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -531,14 +531,14 @@ public async Task LoadPowerAppsObjectModelAsyncWaitsForAppToLoad() MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); } [Fact] - public async Task LoadPowerAppsObjectModelAsyncWaitsForAppToLoadWithExceptions() + public async Task LoadObjectModelAsyncWaitsForAppToLoadWithExceptions() { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -550,14 +550,14 @@ public async Task LoadPowerAppsObjectModelAsyncWaitsForAppToLoadWithExceptions() MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); } [Fact] - public async Task LoadPowerAppsObjectModelAsyncEmbedJSUndefined() + public async Task LoadObjectModelAsyncEmbedJSUndefined() { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -569,14 +569,14 @@ public async Task LoadPowerAppsObjectModelAsyncEmbedJSUndefined() LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); } [Fact] - public async Task LoadPowerAppsObjectModelAsyncEmbedJSDefined() + public async Task LoadObjectModelAsyncEmbedJSDefined() { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -588,7 +588,7 @@ public async Task LoadPowerAppsObjectModelAsyncEmbedJSDefined() LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.AtLeastOnce()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); @@ -634,7 +634,9 @@ public async Task GetDebugInfoReturnsNull() public async Task TestEngineReadyReturnsTrue() { // Arrange - MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction)) + var test = new PowerAppFunctions(null, null, null); + + MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction)) .Returns(Task.FromResult("function")); MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync("PowerAppsTestEngine.testEngineReady()")) .Returns(Task.FromResult(true)); @@ -646,7 +648,7 @@ public async Task TestEngineReadyReturnsTrue() // Assertion Assert.True(readyValue); - MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction), Times.Once()); + MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction), Times.Once()); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.testEngineReady()"), Times.Once()); } @@ -655,7 +657,9 @@ public async Task TestEngineReadyUndefinedWebplayerReturnsTrue() { // Arrange // Mock to return undefined >> scenario where webplayer JSSDK does not have testEngineReady function - MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction)) + var test = new PowerAppFunctions(null, null, null); + + MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction)) .Returns(Task.FromResult("undefined")); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -665,7 +669,7 @@ public async Task TestEngineReadyUndefinedWebplayerReturnsTrue() // Assertion Assert.True(readyValue); - MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction), Times.Once()); + MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction), Times.Once()); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.testEngineReady()"), Times.Never()); } @@ -673,7 +677,9 @@ public async Task TestEngineReadyUndefinedWebplayerReturnsTrue() public async Task TestEngineReadyPublishedAppWithoutJSSDKErrorCodeReturnsTrue() { // Arrange - MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction)) + var test = new PowerAppFunctions(null, null, null); + + MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction)) .Returns(Task.FromResult("function")); // Mock to return error code 1 // Scenario where error thrown is PublishedAppWithoutJSSDKErrorCode @@ -688,7 +694,7 @@ public async Task TestEngineReadyPublishedAppWithoutJSSDKErrorCodeReturnsTrue() // Assertion // readyValue still returns true >> supporting old msapps without ready function Assert.True(readyValue); - MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction), Times.Once()); + MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction), Times.Once()); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.testEngineReady()"), Times.Once()); } @@ -696,8 +702,10 @@ public async Task TestEngineReadyPublishedAppWithoutJSSDKErrorCodeReturnsTrue() public async Task TestEngineReadyNonPublishedAppWithoutJSSDKErrorCodeThrows() { // Arrange - MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); - MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction)) + var test = new PowerAppFunctions(null, null, null); + + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + MockTestInfraFunctions.Setup(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction)) .Returns(Task.FromResult("function")); // Mock to return error code 0 // Scenario where error thrown from app for reason other than PublishedAppWithoutJSSDKErrorCode @@ -709,14 +717,14 @@ public async Task TestEngineReadyNonPublishedAppWithoutJSSDKErrorCodeThrows() // Act and Assertion var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); await Assert.ThrowsAsync(() => powerAppFunctions.TestEngineReady()); - MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(PowerAppFunctions.CheckPowerAppsTestEngineReadyFunction), Times.Once()); + MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync(test.CheckTestEngineReadyFunction), Times.Once()); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.testEngineReady()"), Times.Once()); MockLogger.Verify(); } // Start Published App JSSDK not found tests [Fact] - public async Task LoadPowerAppsObjectModelAsyncNoPublishedAppFunction() + public async Task LoadObjectModelAsyncNoPublishedAppFunction() { MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); MockTestInfraFunctions.Setup(x => x.AddScriptTagAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); @@ -728,7 +736,7 @@ public async Task LoadPowerAppsObjectModelAsyncNoPublishedAppFunction() MockTestState.Setup(x => x.GetTestSettings()).Returns(testSettings); LoggingTestHelper.SetupMock(MockLogger); var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); - await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadPowerAppsObjectModelAsync(); }); + await Assert.ThrowsAsync(async () => { await powerAppFunctions.LoadObjectModelAsync(); }); MockTestInfraFunctions.Verify(x => x.RunJavascriptAsync("PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"), Times.Once()); LoggingTestHelper.VerifyLogging(MockLogger, "Start to load power apps object model", LogLevel.Debug, Times.Once()); @@ -864,5 +872,55 @@ public async Task GetPropertyAsyncNoPublishedAppFunction() } // End Published App JSSDK not found tests + + [Theory] + [InlineData("myEnvironment", "apps.powerapps.com", "myApp", "appId", "myTenant", "https://apps.powerapps.com/play/e/myEnvironment/an/myApp?tenantId=myTenant&source=testengine&enablePATest=true&patestSDKVersion=0.0.1", "&enablePATest=true&patestSDKVersion=0.0.1")] + [InlineData("myEnvironment", "apps.powerapps.com", "myApp", "appId", "myTenant", "https://apps.powerapps.com/play/e/myEnvironment/an/myApp?tenantId=myTenant&source=testengine", "")] + [InlineData("defaultEnvironment", "apps.test.powerapps.com", "defaultApp", "appId", "defaultTenant", "https://apps.test.powerapps.com/play/e/defaultEnvironment/an/defaultApp?tenantId=defaultTenant&source=testengine", "")] + [InlineData("defaultEnvironment", "apps.powerapps.com", null, "appId", "defaultTenant", "https://apps.powerapps.com/play/e/defaultEnvironment/a/appId?tenantId=defaultTenant&source=testengine", "")] + public void GenerateAppUrlTest(string environmentId, string domain, string appLogicalName, string appId, string tenantId, string expectedAppUrl, string queryParams) + { + MockTestState.Setup(x => x.GetEnvironment()).Returns(environmentId); + MockTestState.Setup(x => x.GetDomain()).Returns(domain); + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(new TestSuiteDefinition() { AppLogicalName = appLogicalName, AppId = appId }); + MockTestState.Setup(x => x.GetTenant()).Returns(tenantId); + var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); + Assert.Equal(expectedAppUrl, powerAppFunctions.GenerateTestUrl(domain, queryParams)); + MockTestState.Verify(x => x.GetEnvironment(), Times.Once()); + MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); + MockTestState.Verify(x => x.GetTenant(), Times.Once()); + } + + [Theory] + [InlineData("", "appLogicalName", "appId", "tenantId")] + [InlineData(null, "appLogicalName", "appId", "tenantId")] + [InlineData("environmentId", "", "", "tenantId")] + [InlineData("environmentId", null, null, "tenantId")] + [InlineData("environmentId", "appLogicalName", "appId", "")] + [InlineData("environmentId", "appLogicalName", "appId", null)] + public void GenerateLoginUrlThrowsOnInvalidSetupTest(string environmentId, string appLogicalName, string appId, string tenantId) + { + MockTestState.Setup(x => x.GetEnvironment()).Returns(environmentId); + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(new TestSuiteDefinition() { AppLogicalName = appLogicalName, AppId = appId }); + MockTestState.Setup(x => x.GetTenant()).Returns(tenantId); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); + Assert.Throws(() => powerAppFunctions.GenerateTestUrl("", "")); + } + + [Fact] + public void GenerateLoginUrlThrowsOnInvalidTestDefinitionTest() + { + TestSuiteDefinition testDefinition = null; + MockTestState.Setup(x => x.GetEnvironment()).Returns("environmentId"); + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testDefinition); + MockTestState.Setup(x => x.GetTenant()).Returns("tenantId"); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + var powerAppFunctions = new PowerAppFunctions(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object); + Assert.Throws(() => powerAppFunctions.GenerateTestUrl("", "")); + } + } } diff --git a/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs b/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs new file mode 100644 index 000000000..1d66ec61e --- /dev/null +++ b/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Moq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.PowerApps +{ + public class PowerAppsUrlMapperTests + { + private Mock MockTestState; + private Mock MockSingleTestInstanceState; + private Mock MockLogger; + + public PowerAppsUrlMapperTests() + { + MockTestState = new Mock(MockBehavior.Strict); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockLogger = new Mock(MockBehavior.Strict); + } + + } +} diff --git a/src/testengine.provider.canvas.tests/Usings.cs b/src/testengine.provider.canvas.tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/src/testengine.provider.canvas.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/testengine.provider.canvas.tests/testengine.provider.canvas.tests.csproj b/src/testengine.provider.canvas.tests/testengine.provider.canvas.tests.csproj new file mode 100644 index 000000000..4a6c0f3c8 --- /dev/null +++ b/src/testengine.provider.canvas.tests/testengine.provider.canvas.tests.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + + false + true + ../../35MSSharedLib1024.snk + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Microsoft.PowerApps.TestEngine/JS/CanvasAppSdk.js b/src/testengine.provider.canvas/JS/CanvasAppSdk.js similarity index 100% rename from src/Microsoft.PowerApps.TestEngine/JS/CanvasAppSdk.js rename to src/testengine.provider.canvas/JS/CanvasAppSdk.js diff --git a/src/Microsoft.PowerApps.TestEngine/JS/PublishedAppTesting.js b/src/testengine.provider.canvas/JS/PublishedAppTesting.js similarity index 100% rename from src/Microsoft.PowerApps.TestEngine/JS/PublishedAppTesting.js rename to src/testengine.provider.canvas/JS/PublishedAppTesting.js diff --git a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppFunctions.cs b/src/testengine.provider.canvas/PowerAppFunctions.cs similarity index 66% rename from src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppFunctions.cs rename to src/testengine.provider.canvas/PowerAppFunctions.cs index 572a126e5..6f8ce27b6 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerApps/PowerAppFunctions.cs +++ b/src/testengine.provider.canvas/PowerAppFunctions.cs @@ -1,44 +1,57 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.ComponentModel.Composition; using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.PowerApps.PowerFxModel; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx.Types; using Newtonsoft.Json; -namespace Microsoft.PowerApps.TestEngine.PowerApps +namespace Microsoft.PowerApps.TestEngine.Providers { /// /// Functions for interacting with the Power App /// - public class PowerAppFunctions : IPowerAppFunctions + [Export(typeof(ITestWebProvider))] + public class PowerAppFunctions : ITestWebProvider { - private readonly ITestInfraFunctions _testInfraFunctions; - private readonly ISingleTestInstanceState _singleTestInstanceState; - private readonly ITestState _testState; - public static string EmbeddedJSFolderPath = "JS"; public static string PublishedAppIframeName = "fullscreen-app-host"; - public static string CheckPowerAppsTestEngineObject = "typeof PowerAppsTestEngine"; - public static string CheckPowerAppsTestEngineReadyFunction = "typeof PowerAppsTestEngine.testEngineReady"; + public string CheckTestEngineObject { get; } = "typeof PowerAppsTestEngine"; + public string CheckTestEngineReadyFunction { get; } = "typeof PowerAppsTestEngine.testEngineReady"; private string GetItemCountErrorMessage = "Something went wrong when Test Engine tried to get item count."; private string GetPropertyValueErrorMessage = "Something went wrong when Test Engine tried to get property value."; - private string LoadObjectModelErrorMessage = "Something went wrong when Test Engine tried to load object model."; + //private string LoadObjectModelErrorMessage = "Something went wrong when Test Engine tried to load object model."; private string FileNotFoundErrorMessage = "Something went wrong when Test Engine tried to load required dependencies."; private TypeMapping TypeMapping = new TypeMapping(); - public PowerAppFunctions(ITestInfraFunctions testInfraFunctions, ISingleTestInstanceState singleTestInstanceState, ITestState testState) + public ITestInfraFunctions? TestInfraFunctions { get; set; } + + public ISingleTestInstanceState? SingleTestInstanceState { get; set; } + + public ITestState? TestState { get; set; } + + public ITestProviderState? ProviderState { get; set; } + + public PowerAppFunctions() { - _testInfraFunctions = testInfraFunctions; - _singleTestInstanceState = singleTestInstanceState; - _testState = testState; + } + public PowerAppFunctions(ITestInfraFunctions? testInfraFunctions, ISingleTestInstanceState? singleTestInstanceState, ITestState? testState) + { + this.TestInfraFunctions = testInfraFunctions; + this.SingleTestInstanceState = singleTestInstanceState; + this.TestState = testState; + } + + public string Name { get { return "canvas"; } } + private async Task GetPropertyValueFromControlAsync(ItemPath itemPath) { try @@ -46,11 +59,11 @@ private async Task GetPropertyValueFromControlAsync(ItemPath itemPath) ValidateItemPath(itemPath, true); var itemPathString = JsonConvert.SerializeObject(itemPath); var expression = $"PowerAppsTestEngine.getPropertyValue({itemPathString}).then((propertyValue) => JSON.stringify(propertyValue))"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -59,7 +72,7 @@ public T GetPropertyValueFromControl(ItemPath itemPath) { var getProperty = GetPropertyValueFromControlAsync(itemPath).GetAwaiter(); - PollingHelper.Poll(getProperty, (x) => !x.IsCompleted, null, _testState.GetTimeout(), _singleTestInstanceState.GetLogger(), GetPropertyValueErrorMessage); + PollingHelper.Poll(getProperty, (x) => !x.IsCompleted, null, TestState.GetTimeout(), SingleTestInstanceState.GetLogger(), GetPropertyValueErrorMessage); return getProperty.GetResult(); } @@ -79,47 +92,47 @@ private string GetFilePath(string file) } } - public async Task CheckIfAppIsIdleAsync() + public async Task CheckIsIdleAsync() { try { var expression = "PowerAppsTestEngine.getAppStatus()"; - return (await _testInfraFunctions.RunJavascriptAsync(expression)) == "Idle"; + return (await TestInfraFunctions.RunJavascriptAsync(expression)) == "Idle"; } catch (Exception ex) { if (ex.Message?.ToString() == ExceptionHandlingHelper.PublishedAppWithoutJSSDKErrorCode) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } - _singleTestInstanceState.GetLogger().LogDebug(ex.ToString()); + SingleTestInstanceState.GetLogger().LogDebug(ex.ToString()); return false; } } - private async Task> LoadPowerAppsObjectModelAsyncHelper(Dictionary controlDictionary) + private async Task> LoadObjectModelAsyncHelper(Dictionary controlDictionary) { try { var expression = "PowerAppsTestEngine.buildObjectModel().then((objectModel) => JSON.stringify(objectModel))"; - var controlObjectModelJsonString = await _testInfraFunctions.RunJavascriptAsync(expression); + var controlObjectModelJsonString = await TestInfraFunctions.RunJavascriptAsync(expression); if (!string.IsNullOrEmpty(controlObjectModelJsonString)) { var jsObjectModel = JsonConvert.DeserializeObject(controlObjectModelJsonString); if (jsObjectModel != null && jsObjectModel.Controls != null) { - _singleTestInstanceState.GetLogger().LogTrace("Listing all skipped properties for each control."); + SingleTestInstanceState.GetLogger().LogTrace("Listing all skipped properties for each control."); foreach (var control in jsObjectModel.Controls) { if (controlDictionary.ContainsKey(control.Name)) { // Components get declared twice at the moment so prevent it from throwing. - _singleTestInstanceState.GetLogger().LogTrace($"Control: {control.Name} already added"); + SingleTestInstanceState.GetLogger().LogTrace($"Control: {control.Name} already added"); } else { @@ -142,7 +155,7 @@ private async Task> LoadPowerAppsObjectMo if (everSkipped) { - _singleTestInstanceState.GetLogger().LogTrace(skipMessage); + SingleTestInstanceState.GetLogger().LogTrace(skipMessage); } TypeMapping.AddMapping(control.Name, controlType); @@ -160,7 +173,7 @@ private async Task> LoadPowerAppsObjectMo catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -171,41 +184,41 @@ private async Task GetPowerAppsTestEngineObject() try { - result = await _testInfraFunctions.RunJavascriptAsync(CheckPowerAppsTestEngineObject); + result = await TestInfraFunctions.RunJavascriptAsync(CheckTestEngineObject); } catch (NullReferenceException) { } return result; } - public async Task CheckAndHandleIfLegacyPlayerAsync() + public async Task CheckProviderAsync() { try { // See if using legacy player try { - await PollingHelper.PollAsync("undefined", (x) => x.ToLower() == "undefined", () => GetPowerAppsTestEngineObject(), _testState.GetTestSettings().Timeout, _singleTestInstanceState.GetLogger()); + await PollingHelper.PollAsync("undefined", (x) => x.ToLower() == "undefined", () => GetPowerAppsTestEngineObject(), TestState.GetTestSettings().Timeout, SingleTestInstanceState.GetLogger()); } catch (TimeoutException) { - _singleTestInstanceState.GetLogger().LogInformation("Legacy WebPlayer in use, injecting embedded JS."); - await _testInfraFunctions.AddScriptTagAsync(GetFilePath(Path.Combine(EmbeddedJSFolderPath, "CanvasAppSdk.js")), null); - await _testInfraFunctions.AddScriptTagAsync(GetFilePath(Path.Combine(EmbeddedJSFolderPath, "PublishedAppTesting.js")), PublishedAppIframeName); + SingleTestInstanceState.GetLogger().LogInformation("Legacy WebPlayer in use, injecting embedded JS."); + await TestInfraFunctions.AddScriptTagAsync(GetFilePath(Path.Combine(EmbeddedJSFolderPath, "CanvasAppSdk.js")), null); + await TestInfraFunctions.AddScriptTagAsync(GetFilePath(Path.Combine(EmbeddedJSFolderPath, "PublishedAppTesting.js")), PublishedAppIframeName); } } catch (Exception ex) { - _singleTestInstanceState.GetLogger().LogDebug(ex.ToString()); + SingleTestInstanceState.GetLogger().LogDebug(ex.ToString()); } } - public async Task> LoadPowerAppsObjectModelAsync() + public async Task> LoadObjectModelAsync() { var controlDictionary = new Dictionary(); - _singleTestInstanceState.GetLogger().LogDebug("Start to load power apps object model"); - await PollingHelper.PollAsync(controlDictionary, (x) => x.Keys.Count == 0, (x) => LoadPowerAppsObjectModelAsyncHelper(x), _testState.GetTestSettings().Timeout, _singleTestInstanceState.GetLogger(), LoadObjectModelErrorMessage); - _singleTestInstanceState.GetLogger().LogDebug($"Finish loading. Loaded {controlDictionary.Keys.Count} controls"); + // SingleTestInstanceState.GetLogger().LogDebug("Start to load power apps object model"); + //await PollingHelper.PollAsync(controlDictionary, (x) => x.Keys.Count == 0, (x) => LoadObjectModelAsyncHelper(x), TestState.GetTestSettings().Timeout, SingleTestInstanceState.GetLogger(), LoadObjectModelErrorMessage); + //SingleTestInstanceState.GetLogger().LogDebug($"Finish loading. Loaded {controlDictionary.Keys.Count} controls"); return controlDictionary; } @@ -217,11 +230,11 @@ public async Task SelectControlAsync(ItemPath itemPath) ValidateItemPath(itemPath, false); var itemPathString = JsonConvert.SerializeObject(itemPath); var expression = $"PowerAppsTestEngine.select({itemPathString})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -256,11 +269,11 @@ public async Task SetPropertyAsync(ItemPath itemPath, FormulaValue value) ValidateItemPath(itemPath, false); var expression = $"PowerAppsTestEngine.setPropertyValue({JsonConvert.SerializeObject(itemPath)}, {JsonConvert.SerializeObject(objectValue)})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -278,11 +291,11 @@ public async Task SetPropertyDateAsync(ItemPath itemPath, DateValue value) // Date.parse() parses the date to unix timestamp var expression = $"PowerAppsTestEngine.setPropertyValue({itemPathString},{{{propertyNameString}:Date.parse(\"{recordValue}\")}})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -301,11 +314,11 @@ public async Task SetPropertyRecordAsync(ItemPath itemPath, RecordValue va var checkVal = JsonConvert.SerializeObject(json); var expression = $"PowerAppsTestEngine.setPropertyValue({itemPathString},{{{propertyNameString}:{checkVal}}})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -337,11 +350,11 @@ public async Task SetPropertyTableAsync(ItemPath itemPath, TableValue tabl var checkVal = JsonConvert.SerializeObject(jsonArr); var expression = $"PowerAppsTestEngine.setPropertyValue({itemPathString},{{{propertyNameString}:{checkVal}}})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -350,8 +363,8 @@ private void ValidateItemPath(ItemPath itemPath, bool requirePropertyName) { if (string.IsNullOrEmpty(itemPath.ControlName)) { - _singleTestInstanceState.GetLogger().LogTrace("ItemPath's ControlName: " + nameof(itemPath.ControlName)); - _singleTestInstanceState.GetLogger().LogError("ItemPath's ControlName has a null value."); + SingleTestInstanceState.GetLogger().LogTrace("ItemPath's ControlName: " + nameof(itemPath.ControlName)); + SingleTestInstanceState.GetLogger().LogError("ItemPath's ControlName has a null value."); throw new ArgumentNullException(); } @@ -361,8 +374,8 @@ private void ValidateItemPath(ItemPath itemPath, bool requirePropertyName) { // Property name is required on certain functions // It is also required when accessing elements in a gallery, so if an index is specified, it needs to be there - _singleTestInstanceState.GetLogger().LogTrace("ItemPath's PropertyName: '" + nameof(itemPath.PropertyName)); - _singleTestInstanceState.GetLogger().LogError("ItemPath's PropertyName has a null value."); + SingleTestInstanceState.GetLogger().LogTrace("ItemPath's PropertyName: '" + nameof(itemPath.PropertyName)); + SingleTestInstanceState.GetLogger().LogError("ItemPath's PropertyName has a null value."); throw new ArgumentNullException(); } } @@ -380,11 +393,11 @@ private async Task GetItemCountAsync(ItemPath itemPath) ValidateItemPath(itemPath, false); var itemPathString = JsonConvert.SerializeObject(itemPath); var expression = $"PowerAppsTestEngine.getItemCount({itemPathString})"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception ex) { - ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, _singleTestInstanceState.GetLogger()); + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); throw; } } @@ -393,7 +406,7 @@ public int GetItemCount(ItemPath itemPath) { var getItemCount = GetItemCountAsync(itemPath).GetAwaiter(); - PollingHelper.Poll(getItemCount, (x) => !x.IsCompleted, null, _testState.GetTimeout(), _singleTestInstanceState.GetLogger(), GetItemCountErrorMessage); + PollingHelper.Poll(getItemCount, (x) => !x.IsCompleted, null, TestState.GetTimeout(), SingleTestInstanceState.GetLogger(), GetItemCountErrorMessage); return getItemCount.GetResult(); } @@ -403,7 +416,7 @@ public async Task GetDebugInfo() try { var expression = $"PowerAppsTestEngine.debugInfo"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } catch (Exception) { @@ -416,11 +429,11 @@ public async Task TestEngineReady() try { // check if ready function exists in the webplayer JSSDK, older versions won't have this new function - var checkIfReadyExists = await _testInfraFunctions.RunJavascriptAsync(CheckPowerAppsTestEngineReadyFunction); + var checkIfReadyExists = await TestInfraFunctions.RunJavascriptAsync(CheckTestEngineReadyFunction); if (checkIfReadyExists != "undefined") { var expression = $"PowerAppsTestEngine.testEngineReady()"; - return await _testInfraFunctions.RunJavascriptAsync(expression); + return await TestInfraFunctions.RunJavascriptAsync(expression); } // To support webplayer version without ready function @@ -437,9 +450,53 @@ public async Task TestEngineReady() } // If the error returned is anything other than PublishedAppWithoutJSSDKErrorCode capture that and throw - _singleTestInstanceState.GetLogger().LogDebug(ex.ToString()); + SingleTestInstanceState.GetLogger().LogDebug(ex.ToString()); throw; } } + + public string GenerateTestUrl(string domain, string additionalQueryParams) + { + var environment = TestState.GetEnvironment(); + if (string.IsNullOrEmpty(environment)) + { + SingleTestInstanceState.GetLogger().LogError("Environment cannot be empty."); + throw new InvalidOperationException(); + } + + var testSuiteDefinition = SingleTestInstanceState.GetTestSuiteDefinition(); + if (testSuiteDefinition == null) + { + SingleTestInstanceState.GetLogger().LogError("Test definition must be specified."); + throw new InvalidOperationException(); + } + + var appLogicalName = testSuiteDefinition.AppLogicalName; + var appId = testSuiteDefinition.AppId; + + if (string.IsNullOrEmpty(appLogicalName) && string.IsNullOrEmpty(appId)) + { + SingleTestInstanceState.GetLogger().LogError("At least one of the App Logical Name or App Id must be defined."); + throw new InvalidOperationException(); + } + + var tenantId = TestState.GetTenant(); + if (string.IsNullOrEmpty(tenantId)) + { + SingleTestInstanceState.GetLogger().LogError("Tenant cannot be empty."); + throw new InvalidOperationException(); + } + + var queryParametersForTestUrl = GetQueryParametersForTestUrl(tenantId, additionalQueryParams); + + return !string.IsNullOrEmpty(appLogicalName) ? + $"https://{domain}/play/e/{environment}/an/{appLogicalName}{queryParametersForTestUrl}" : + $"https://{domain}/play/e/{environment}/a/{appId}{queryParametersForTestUrl}"; + } + + private static string GetQueryParametersForTestUrl(string tenantId, string additionalQueryParams) + { + return $"?tenantId={tenantId}&source=testengine{additionalQueryParams}"; + } } } diff --git a/src/testengine.provider.canvas/testengine.provider.canvas.csproj b/src/testengine.provider.canvas/testengine.provider.canvas.csproj new file mode 100644 index 000000000..ec13089a9 --- /dev/null +++ b/src/testengine.provider.canvas/testengine.provider.canvas.csproj @@ -0,0 +1,48 @@ + + + + net6.0 + enable + enable + true + ../../35MSSharedLib1024.snk + true + © Microsoft Corporation. All rights reserved. + true + 1.0 + + + + + + + + + + PreserveNewest + true + + + PreserveNewest + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs new file mode 100644 index 000000000..41ea42f15 --- /dev/null +++ b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs @@ -0,0 +1,70 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Microsoft.Extensions.Logging; +using testengine.user.environment; + +namespace testengine.user.environment.tests +{ + public class BrowserUserManagerModuleTests + { + private Mock MockBrowserState; + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockSingleTestInstanceState; + private Mock MockEnvironmentVariable; + private Mock MockLogger; + private Mock MockBrowserContext; + private Mock MockPage; + private Mock MockElementHandle; + private Mock MockFileSystem; + + public BrowserUserManagerModuleTests() + { + MockBrowserState = new Mock(MockBehavior.Strict); + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockEnvironmentVariable = new Mock(MockBehavior.Strict); + MockLogger = new Mock(MockBehavior.Strict); + MockBrowserContext = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + MockElementHandle = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + } + + [Theory] + [InlineData(false, true, "", true)] + [InlineData(true, false, "", true)] + [InlineData(true, false, "a.txt", false)] + public async Task LoginWithBrowserState(bool exists, bool isDirectoryCreated, string files, bool willPause) + { + // Arrange + if ( willPause ) { + MockPage.Setup(x => x.PauseAsync()).Returns(Task.CompletedTask); + } + + var created = false; + + var userManager = new BrowserUserManagerModule(); + userManager.DirectoryExists = (path) => exists; + userManager.CreateDirectory = (path) => created = true; + userManager.GetFiles = (path) => files.Split(','); + userManager.Page = MockPage.Object; + + // Act + await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object); + + // Assert + Assert.True(created == isDirectoryCreated); + } + } +} diff --git a/src/testengine.user.browser.tests/Usings.cs b/src/testengine.user.browser.tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/src/testengine.user.browser.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj b/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj new file mode 100644 index 000000000..64e306966 --- /dev/null +++ b/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/testengine.user.browser/BrowserUserManagerModule.cs b/src/testengine.user.browser/BrowserUserManagerModule.cs new file mode 100644 index 000000000..14b0fc386 --- /dev/null +++ b/src/testengine.user.browser/BrowserUserManagerModule.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx; +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Playwright; +using System; +using System.Text.RegularExpressions; +using System.Linq; +using Microsoft.PowerApps.TestEngine.Users; + +namespace testengine.user.environment +{ + [Export(typeof(IUserManager))] + public class BrowserUserManagerModule : IUserManager + { + public string Name { get { return "browser"; } } + + public int Priority { get { return 100; } } + + public bool UseStaticContext { get { return true; } } + + public string Location { get; set; } = "BrowserContext"; + + private IBrowserContext? Context { get;set; } + + public IPage? Page { get;set; } + + public Func DirectoryExists { get;set; } = (location) => Directory.Exists(location); + + public Action CreateDirectory { get;set; } = (location) => Directory.CreateDirectory(location); + + public Func GetFiles { get;set; } = (path) => Directory.GetFiles(path); + + public async Task LoginAsUserAsync( + string desiredUrl, + IBrowserContext context, + ITestState testState, + ISingleTestInstanceState singleTestInstanceState, + IEnvironmentVariable environmentVariable) + { + Context = context; + + if ( ! DirectoryExists(Location)) { + CreateDirectory(Location); + } + + if ( GetFiles(Location).Count() == 0 ) { + ValidatePage(); + await Page.PauseAsync(); + } + } + + private void ValidatePage() + { + if (Page == null) + { + Page = Context.Pages.First(); + } + } + } +} diff --git a/src/testengine.user.browser/testengine.user.browser.csproj b/src/testengine.user.browser/testengine.user.browser.csproj new file mode 100644 index 000000000..7515b1bca --- /dev/null +++ b/src/testengine.user.browser/testengine.user.browser.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs new file mode 100644 index 000000000..e842bf4dd --- /dev/null +++ b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs @@ -0,0 +1,354 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Microsoft.Extensions.Logging; +using testengine.user.environment; + +namespace testengine.user.environment.tests +{ + public class EnvironmentUserManagerModuleTests + { + private Mock MockBrowserState; + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockSingleTestInstanceState; + private Mock MockEnvironmentVariable; + private TestSuiteDefinition TestSuiteDefinition; + private Mock MockLogger; + private Mock MockBrowserContext; + private Mock MockPage; + private Mock MockElementHandle; + private Mock MockFileSystem; + + public EnvironmentUserManagerModuleTests() + { + MockBrowserState = new Mock(MockBehavior.Strict); + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockEnvironmentVariable = new Mock(MockBehavior.Strict); + TestSuiteDefinition = new TestSuiteDefinition() + { + TestSuiteName = "Test1", + TestSuiteDescription = "First test", + AppLogicalName = "logicalAppName1", + Persona = "User1", + TestCases = new List() + { + new TestCase + { + TestCaseName = "Test Case Name", + TestCaseDescription = "Test Case Description", + TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" + } + } + }; + MockLogger = new Mock(MockBehavior.Strict); + MockBrowserContext = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + MockElementHandle = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + } + + [Fact] + public async Task LoginAsUserSuccessTest() + { + var userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = "user1Email", + PasswordKey = "user1Password" + }; + + var email = "someone@example.com"; + var password = "myPassword1234"; + + MockLogger = new Mock(); + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.PasswordKey)).Returns(password); + MockTestInfraFunctions.Setup(x => x.GoToUrlAsync(It.IsAny())).Returns(Task.CompletedTask); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + var keyboard = new Mock(MockBehavior.Strict); + + // Email Address + MockPage.Setup(x => x.Locator(EnvironmentUserManagerModule.EmailSelector, null)).Returns(new Mock().Object); + MockPage.Setup(x => x.TypeAsync(EnvironmentUserManagerModule.EmailSelector, email, It.IsAny())).Returns(Task.CompletedTask); + keyboard.Setup(x => x.PressAsync("Tab", It.IsAny())) + .Returns(Task.CompletedTask); + MockPage.SetupGet(x => x.Keyboard).Returns(keyboard.Object); + MockPage.Setup(x => x.ClickAsync(EnvironmentUserManagerModule.SubmitButtonSelector, null)).Returns(Task.CompletedTask); + + // Enter Password and keep me signed in + MockPage.Setup(x => x.Locator(EnvironmentUserManagerModule.PasswordSelector, null)).Returns(new Mock().Object); + MockPage.Setup(x => x.FillAsync(EnvironmentUserManagerModule.PasswordSelector, password, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync(EnvironmentUserManagerModule.SubmitButtonSelector, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync(EnvironmentUserManagerModule.StaySignedInSelector, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync(EnvironmentUserManagerModule.KeepMeSignedInNoSelector, null)).Returns(Task.CompletedTask); + + // Now wait for the requested URL assuming login now complete + MockPage.Setup(x => x.WaitForURLAsync("*", null)).Returns(Task.CompletedTask); + + var userManager = new EnvironmentUserManagerModule(); + userManager.Page = MockPage.Object; + + await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object); + + MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); + MockTestState.Verify(x => x.GetUserConfiguration(userConfiguration.PersonaName), Times.Once()); + MockEnvironmentVariable.Verify(x => x.GetVariable(userConfiguration.EmailKey), Times.Once()); + MockEnvironmentVariable.Verify(x => x.GetVariable(userConfiguration.PasswordKey), Times.Once()); + } + + [Fact] + public async Task LoginUserAsyncThrowsOnNullTestDefinitionTest() + { + TestSuiteDefinition testSuiteDefinition = null; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new EnvironmentUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task LoginUserAsyncThrowsOnInvalidPersonaTest(string persona) + { + var testSuiteDefinition = new TestSuiteDefinition() + { + TestSuiteName = "Test1", + TestSuiteDescription = "First test", + AppLogicalName = "logicalAppName1", + Persona = persona, + TestCases = new List() + { + new TestCase + { + TestCaseName = "Test Case Name", + TestCaseDescription = "Test Case Description", + TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" + } + } + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new EnvironmentUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object)); + } + + [Fact] + public async Task LoginUserAsyncThrowsOnNullUserConfigTest() + { + UserConfiguration userConfiguration = null; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new EnvironmentUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object)); + } + + [Theory] + [InlineData(null, "myPassword1234")] + [InlineData("", "myPassword1234")] + [InlineData("user1Email", null)] + [InlineData("user1Email", "")] + public async Task LoginUserAsyncThrowsOnInvalidUserConfigTest(string emailKey, string passwordKey) + { + UserConfiguration userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = emailKey, + PasswordKey = passwordKey + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new EnvironmentUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object)); + } + + [Theory] + [InlineData(null, "user1Password")] + [InlineData("", "user1Password")] + [InlineData("someone@example.com", null)] + [InlineData("someone@example.com", "")] + public async Task LoginUserAsyncThrowsOnInvalidEnviromentVariablesTest(string email, string password) + { + UserConfiguration userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = "user1Email", + PasswordKey = "user1Password" + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.PasswordKey)).Returns(password); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List { MockPage.Object }); + + var userManager = new EnvironmentUserManagerModule(); + userManager.Page = MockPage.Object; + + var ex = await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object)); + + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); + if (String.IsNullOrEmpty(email)) + { + LoggingTestHelper.VerifyLogging(MockLogger, "User email cannot be null. Please check if the environment variable is set properly.", LogLevel.Error, Times.Once()); + } + if (String.IsNullOrEmpty(password)) + { + LoggingTestHelper.VerifyLogging(MockLogger, "Password cannot be null. Please check if the environment variable is set properly.", LogLevel.Error, Times.Once()); + } + } + + [Fact] + public async Task HandleUserPasswordScreen() + { + string testSelector = "input:has-text('Password')"; + string testTextEntry = "*****"; + string desiredUrl = "https://make.powerapps.com"; + + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var mockLocator = new Mock(MockBehavior.Strict); + MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); + mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); + + MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); + // Assume ask already logged in + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.IsAny())).Returns(Task.FromResult(MockElementHandle.Object)); + // Simulate Click to stay signed in + MockPage.Setup(x => x.ClickAsync("[id=\"idBtn_Back\"]", null)).Returns(Task.CompletedTask); + // Wait until login is complete and redirect to desired page + MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Returns(Task.CompletedTask); + + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List{ MockPage.Object}); + + var userManagerModule = new EnvironmentUserManagerModule(); + userManagerModule.Page = MockPage.Object; + + await userManagerModule.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl, MockLogger.Object); + + MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); + MockPage.Verify(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.Is(v => v.Timeout >= 8000))); + } + + [Fact] + public async Task HandleUserPasswordScreenErrorEntry() + { + string testSelector = "input:has-text('Password')"; + string testTextEntry = "*****"; + string desiredUrl = "https://make.powerapps.com"; + + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var mockLocator = new Mock(MockBehavior.Strict); + MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); + mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); + + MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); + // Not ask to sign in as selector not found + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.IsAny())).Throws(new TimeoutException()); + // Simulate error response for password error + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.IsAny())).Returns(Task.FromResult(MockElementHandle.Object)); + // Throw exception as not make it to desired url + MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); + + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List {MockPage.Object }); + + var userManagerModule = new EnvironmentUserManagerModule(); + userManagerModule.Page = MockPage.Object; + + // scenario where password error or missing + var ex = await Assert.ThrowsAsync(async () => await userManagerModule.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl, MockLogger.Object)); + + MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); + MockPage.Verify(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.Is(v => v.Timeout >= 2000))); + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); + } + + [Fact] + public async Task HandleUserPasswordScreenUnknownError() + { + string testSelector = "input:has-text('Password')"; + string testTextEntry = "*****"; + string desiredUrl = "https://make.powerapps.com"; + + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + var mockLocator = new Mock(MockBehavior.Strict); + MockPage.Setup(x => x.Locator(It.IsAny(), null)).Returns(mockLocator.Object); + mockLocator.Setup(x => x.WaitForAsync(null)).Returns(Task.CompletedTask); + + MockPage.Setup(x => x.FillAsync(testSelector, testTextEntry, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync("input[type=\"submit\"]", null)).Returns(Task.CompletedTask); + // Not ask to sign in as selector not found + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", null)).Throws(new TimeoutException()); + // Also not able to find password error, must be some other error + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"passwordError\"]", It.IsAny())).Throws(new TimeoutException()); + // Throw exception as not make it to desired url + MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); + + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List{ MockPage.Object }); + + var environmentUserManager = new EnvironmentUserManagerModule(); + environmentUserManager.Page = MockPage.Object; + + await Assert.ThrowsAsync(async () => await environmentUserManager.HandleUserPasswordScreen(testSelector, testTextEntry, desiredUrl, MockLogger.Object)); + + MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); + } + } +} \ No newline at end of file diff --git a/src/testengine.user.environment.tests/Usings.cs b/src/testengine.user.environment.tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/src/testengine.user.environment.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj b/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj new file mode 100644 index 000000000..0d8eec56c --- /dev/null +++ b/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/testengine.user.environment/EnvironmentUserManagerModule.cs b/src/testengine.user.environment/EnvironmentUserManagerModule.cs new file mode 100644 index 000000000..60a640a92 --- /dev/null +++ b/src/testengine.user.environment/EnvironmentUserManagerModule.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx; +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Playwright; +using System; +using System.Text.RegularExpressions; +using System.Linq; +using Microsoft.PowerApps.TestEngine.Users; + +namespace testengine.user.environment +{ + [Export(typeof(IUserManager))] + public class EnvironmentUserManagerModule : IUserManager + { + public string Name { get { return "environment"; } } + + public int Priority { get { return 0; } } + + public bool UseStaticContext { get { return false; } } + + public string Location { get; set; } = string.Empty; + + public static string EmailSelector = "input[type=\"email\"]"; + public static string PasswordSelector = "input[type=\"password\"]"; + public static string SubmitButtonSelector = "input[type=\"submit\"]"; + public static string StaySignedInSelector = "[id=\"KmsiCheckboxField\"]"; + public static string KeepMeSignedInNoSelector = "[id=\"idBtn_Back\"]"; + + private IBrowserContext? Context { get;set; } + + public IPage? Page { get;set; } + + public async Task LoginAsUserAsync( + string desiredUrl, + IBrowserContext context, + ITestState testState, + ISingleTestInstanceState singleTestInstanceState, + IEnvironmentVariable environmentVariable) + { + Context = context; + + var testSuiteDefinition = singleTestInstanceState.GetTestSuiteDefinition(); + var logger = singleTestInstanceState.GetLogger(); + + if (testSuiteDefinition == null) + { + logger.LogError("Test definition cannot be null"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(testSuiteDefinition.Persona)) + { + logger.LogError("Persona cannot be empty"); + throw new InvalidOperationException(); + } + + var userConfig = testState.GetUserConfiguration(testSuiteDefinition.Persona); + + if (userConfig == null) + { + logger.LogError("Cannot find user config for persona"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(userConfig.EmailKey)) + { + logger.LogError("Email key for persona cannot be empty"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(userConfig.PasswordKey)) + { + logger.LogError("Password key for persona cannot be empty"); + throw new InvalidOperationException(); + } + + var user = environmentVariable.GetVariable(userConfig.EmailKey); + var password = environmentVariable.GetVariable(userConfig.PasswordKey); + + bool missingUserOrPassword = false; + + if (string.IsNullOrEmpty(user)) + { + logger.LogError(("User email cannot be null. Please check if the environment variable is set properly.")); + missingUserOrPassword = true; + } + + if (string.IsNullOrEmpty(password)) + { + logger.LogError("Password cannot be null. Please check if the environment variable is set properly."); + missingUserOrPassword = true; + } + + if (missingUserOrPassword) + { + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + + if ( Page == null ) { + Page = context.Pages.First(); + } + + await HandleUserEmailScreen(EmailSelector, user); + + await Page.ClickAsync(SubmitButtonSelector); + + // Wait for the sliding animation to finish + await Task.Delay(1000); + + await HandleUserPasswordScreen(PasswordSelector, password, desiredUrl, logger); + } + + private void ValidatePage() + { + if (Page == null) + { + Page = Context.Pages.First(); + } + } + + private async Task ClickAsync(string selector) + { + ValidatePage(); + await Page.ClickAsync(selector); + } + + public async Task HandleUserEmailScreen(string selector, string value) + { + ValidatePage(); + await Page.Locator(selector).WaitForAsync(); + await Page.TypeAsync(selector, value, new PageTypeOptions { Delay = 50 }); + await Page.Keyboard.PressAsync("Tab", new KeyboardPressOptions { Delay = 20 }); + } + + public async Task HandleUserPasswordScreen(string selector, string value, string desiredUrl, ILogger logger) + { + ValidatePage(); + + try + { + // Find the password box + await Page.Locator(selector).WaitForAsync(); + + // Fill in the password + await Page.FillAsync(selector, value); + + // Submit password form + await this.ClickAsync(EnvironmentUserManagerModule.SubmitButtonSelector); + + PageWaitForSelectorOptions selectorOptions = new PageWaitForSelectorOptions(); + selectorOptions.Timeout = 8000; + + // For instances where there is a 'Stay signed in?' dialogue box + try + { + logger.LogDebug("Checking if asked to stay signed in."); + + // Check if we received a 'Stay signed in?' box? + await Page.WaitForSelectorAsync(EnvironmentUserManagerModule.StaySignedInSelector, selectorOptions); + logger.LogDebug("Was asked to 'stay signed in'."); + + // Click to stay signed in + await Page.ClickAsync(EnvironmentUserManagerModule.KeepMeSignedInNoSelector); + } + // If there is no 'Stay signed in?' box, an exception will throw; just catch and continue + catch (Exception ssiException) + { + logger.LogDebug("Exception encountered: " + ssiException.ToString()); + + // Keep record if passwordError was encountered + bool hasPasswordError = false; + + try + { + selectorOptions.Timeout = 2000; + + // Check if we received a password error + await Page.WaitForSelectorAsync("[id=\"passwordError\"]", selectorOptions); + hasPasswordError = true; + } + catch (Exception peException) + { + logger.LogDebug("Exception encountered: " + peException.ToString()); + } + + // If encountered password error, exit program + if (hasPasswordError) + { + logger.LogError("Incorrect password entered. Make sure you are using the correct credentials."); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + // If not, continue + else + { + logger.LogDebug("Did not encounter an invalid password error."); + } + + logger.LogDebug("Was not asked to 'stay signed in'."); + } + + await Page.WaitForURLAsync(desiredUrl); + } + catch (TimeoutException) + { + logger.LogError("Timed out during login attempt. In order to determine why, it may be beneficial to view the output recording. Make sure that your login credentials are correct."); + throw new TimeoutException(); + } + + logger.LogDebug("Logged in successfully."); + } + } +} diff --git a/src/testengine.user.environment/testengine.user.environment.csproj b/src/testengine.user.environment/testengine.user.environment.csproj new file mode 100644 index 000000000..cb6f55433 --- /dev/null +++ b/src/testengine.user.environment/testengine.user.environment.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + From dea980f7faa671dfcb48a62af7e914041cb5bcad Mon Sep 17 00:00:00 2001 From: snamilikonda Date: Tue, 21 May 2024 16:34:27 -0700 Subject: [PATCH 02/40] setting log level, loading object model, adding browser config to testsetting (#323) * setting log level, loading object model * adding the browserconfig to testsetting * test modificaiton and browserconfig * actions on push and PR * formatting * formatting changes * formatting changes * build validation * debug build * release build * permissions * test verfication * detailed test logs * windows image * release tests * resetting the tests * using powershell for report check * using new version * upload summary as artifact * separate job * separate job * separate job * low threshold image change * test plan changes * build full * no integration tests * increased threshold --- .github/workflows/build-test.yml | 162 ++++++++++-------- .github/workflows/dotnet-format.yml | 4 +- .github/workflows/yaml-integration-tests.yml | 4 +- samples/basicgallery/testPlan.fx.yaml | 2 + samples/testSettings.yaml | 2 + .../PowerFx/Functions/SelectFunctionTests.cs | 4 +- .../Functions/SetPropertyFunctionTests.cs | 2 +- .../PowerFx/Functions/WaitFunctionTests.cs | 2 +- .../PowerFx/PowerFxEngineTests.cs | 2 +- .../SingleTestRunnerTests.cs | 4 +- .../PlaywrightTestInfraFunctionTests.cs | 2 + .../Modules/TestEngineExtensionChecker.cs | 5 +- .../Modules/TestEngineModuleMEFLoader.cs | 2 +- .../PowerFx/PowerFxEngine.cs | 4 +- .../Providers/ITestWebProvider.cs | 4 +- .../SingleTestRunner.cs | 2 +- .../TestInfra/PlaywrightTestInfraFunctions.cs | 13 +- src/PowerAppsTestEngine/Program.cs | 13 +- .../PauseFunctionTests.cs | 12 +- .../PauseModuleTests.cs | 18 +- src/testengine.module.pause.tests/Usings.cs | 2 +- src/testengine.module.pause/PauseFunction.cs | 8 +- src/testengine.module.pause/PauseModule.cs | 6 +- .../PowerAppsUrlMapperTests.cs | 2 +- .../Usings.cs | 2 +- .../PowerAppFunctions.cs | 10 +- .../BrowserUserManagerModuleTests.cs | 15 +- src/testengine.user.browser.tests/Usings.cs | 2 +- .../BrowserUserManagerModule.cs | 28 +-- .../EnvironmentUserManagerModuleTests.cs | 16 +- .../Usings.cs | 2 +- .../EnvironmentUserManagerModule.cs | 19 +- 32 files changed, 201 insertions(+), 174 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 33f5b846b..e2eccad86 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -4,16 +4,21 @@ on: workflow_dispatch: push: branches: - - main + - integration pull_request: branches: - - main + - integration schedule: - cron: '0 6 * * 1,3,5' +permissions: + checks: write + contents: write + pull-requests: write + jobs: build: - runs-on: ubuntu-latest + runs-on: windows-latest strategy: matrix: dotnet-version: ['6.0.x'] @@ -44,7 +49,7 @@ jobs: - name: Test run: | cd src - dotnet test --no-restore --verbosity normal --logger:trx --collect:"XPlat Code Coverage" --results-directory ./TestResults + dotnet test --configuration Release --no-restore --verbosity normal --logger:trx --collect:"XPlat Code Coverage" --results-directory ./TestResults - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 @@ -57,21 +62,32 @@ jobs: path: | **/*.trx reporter: dotnet-trx # Format of test results + + - name: Upload test results + uses: actions/upload-artifact@v2 + with: + name: test-results-coverage-report + path: src/TestResults/**/coverage.cobertura.xml - - name: Copy Coverage report - run: cp src/TestResults/**/coverage.cobertura.xml coverage.cobertura.xml - - - name: Code coverage report - uses: irongut/CodeCoverageSummary@v1.2.0 + test-coverage: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download test coverage report + uses: actions/download-artifact@v2 with: - filename: coverage.cobertura.xml + name: test-results-coverage-report + path: . + - name: Run CodeCoverageSummary + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: ./**/coverage.cobertura.xml badge: true fail_below_min: true format: markdown indicators: true output: both - thresholds: '85 90' - + thresholds: '15 15' - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 if: github.event_name == 'pull_request' @@ -79,64 +95,66 @@ jobs: recreate: true path: code-coverage-results.md - yaml-integration-tests-prod: - needs: build - uses: ./.github/workflows/yaml-integration-tests.yml - with: - parameters: - '[{ "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdPreview.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' - secrets: inherit +# commenting to run these stages not on pr but nightly + # yaml-integration-tests-prod: + # needs: build + # uses: ./.github/workflows/yaml-integration-tests.yml + # with: + # parameters: + # '[{ "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdPreview.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "ceb95cca-da1d-ed58-8af8-117cb4081f16", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' + # secrets: inherit - yaml-integration-tests-preprod: - needs: build - uses: ./.github/workflows/yaml-integration-tests.yml - with: - parameters: - '[{ "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" } - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdPreprod.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' - secrets: inherit - yaml-integration-tests-test: - needs: build - uses: ./.github/workflows/yaml-integration-tests.yml - with: - parameters: - '[{ "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdTest.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, - { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' - secrets: inherit - notification: - if: github.event_name == 'schedule' && (failure() || github.run_attempt > 1) #send notification only for schedule failure or reruns - needs: [yaml-integration-tests-prod, yaml-integration-tests-preprod, yaml-integration-tests-test] - runs-on: ubuntu-latest - name: Send Notification To Teams - steps: - - name: Send a Notification to Teams - id: notify - uses: thechetantalwar/teams-notify@v2 - with: - teams_webhook_url: ${{ secrets.TEAM_HOOK }} - message: "${{ job.status }}: Github Action ${{ github.run_number }} (attempt #${{ github.run_attempt }}) triggered by ${{ github.triggering_actor }}. See https://github.com/microsoft/PowerApps-TestEngine/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} details." + # yaml-integration-tests-preprod: + # needs: build + # uses: ./.github/workflows/yaml-integration-tests.yml + # with: + # parameters: + # '[{ "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" } + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdPreprod.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "98abc6e1-c9ae-e911-9bb3-a30701a3e3d0", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.preprod.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' + # secrets: inherit + # yaml-integration-tests-test: + # needs: build + # uses: ./.github/workflows/yaml-integration-tests.yml + # with: + # parameters: + # '[{ "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/basicgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/buttonclicker/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/calculator/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/connector/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/containers/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanAppIdTest.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/differentvariabletypes/testPlanForScriptInjection.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/nestedgallery/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }, + # { "environmentId": "c12a52de-7404-e19d-9f6e-90f8548a90f2", "tenantId": "f2c52b3d-d88e-4892-9785-d5b7c7016725", "domain": "apps.test.powerapps.com", "testPlanFile": "../../samples/pcfcomponent/testPlan.fx.yaml", "outputDirectory": "../../TestResults" }]' + # secrets: inherit + + # notification: + # if: github.event_name == 'schedule' && (failure() || github.run_attempt > 1) #send notification only for schedule failure or reruns + # needs: [yaml-integration-tests-prod, yaml-integration-tests-preprod, yaml-integration-tests-test] + # runs-on: ubuntu-latest + # name: Send Notification To Teams + # steps: + # - name: Send a Notification to Teams + # id: notify + # uses: thechetantalwar/teams-notify@v2 + # with: + # teams_webhook_url: ${{ secrets.TEAM_HOOK }} + # message: "${{ job.status }}: Github Action ${{ github.run_number }} (attempt #${{ github.run_attempt }}) triggered by ${{ github.triggering_actor }}. See https://github.com/microsoft/PowerApps-TestEngine/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} details." diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index f95dd3445..4aa6a6a99 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -4,10 +4,10 @@ on: workflow_dispatch: push: branches: - - main + - integration pull_request: branches: - - main + - integration jobs: check-format: diff --git a/.github/workflows/yaml-integration-tests.yml b/.github/workflows/yaml-integration-tests.yml index 6839276b4..19675fb46 100644 --- a/.github/workflows/yaml-integration-tests.yml +++ b/.github/workflows/yaml-integration-tests.yml @@ -16,9 +16,9 @@ jobs: user1Email: '${{ secrets.POWER_PLATFORM_USER }}' user1Password: '${{ secrets.POWER_PLATFORM_PASSWORD }}' run: | - cd src/PowerAppsTestEngine + cd src dotnet build - cd ../../ + cd ../ pwsh bin/Debug/PowerAppsTestEngine/playwright.ps1 install cd build-pipelines/scripts chmod +x yaml-integration-tests.sh diff --git a/samples/basicgallery/testPlan.fx.yaml b/samples/basicgallery/testPlan.fx.yaml index da98d3000..3d57e03eb 100644 --- a/samples/basicgallery/testPlan.fx.yaml +++ b/samples/basicgallery/testPlan.fx.yaml @@ -26,6 +26,8 @@ testSuite: testSettings: locale: "en-US" recordVideo: true + extensionModules: + enable: true browserConfigurations: - browser: Chromium diff --git a/samples/testSettings.yaml b/samples/testSettings.yaml index 3ac8428d8..59f28d657 100644 --- a/samples/testSettings.yaml +++ b/samples/testSettings.yaml @@ -1,4 +1,6 @@ locale: "en-US" recordVideo: true +extensionModules: + enable: true browserConfigurations: - browser: Chromium diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs index 99f74d634..8d681f4ba 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectFunctionTests.cs @@ -6,10 +6,10 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.Providers; -using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; using Moq; diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs index cb30d931b..41792f386 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetPropertyFunctionTests.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; -using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; using Moq; diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs index 8fadc932b..2a4d230c0 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/WaitFunctionTests.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; -using Microsoft.PowerApps.TestEngine.PowerFx.Functions; using Microsoft.PowerApps.TestEngine.Tests.Helpers; using Microsoft.PowerFx.Types; using Moq; diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs index 0b9d81e54..4d5caa263 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/PowerFxEngineTests.cs @@ -11,9 +11,9 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Providers.PowerFxModel; -using Microsoft.PowerApps.TestEngine.PowerFx; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs index 8b53a0155..fa185eae4 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs @@ -11,8 +11,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; -using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; @@ -132,6 +132,8 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str MockTestWebProvider.Setup(x => x.GenerateTestUrl("", "")).Returns(appUrl); MockTestWebProvider.SetupSet(x => x.TestInfraFunctions = MockTestInfraFunctions.Object); + MockTestWebProvider.SetupSet(x => x.TestState = MockTestState.Object); + MockTestWebProvider.SetupSet(x => x.SingleTestInstanceState = MockSingleTestInstanceState.Object); MockTestLogger.Setup(x => x.WriteToLogsFile(It.IsAny(), It.IsAny())); MockTestLogger.Setup(x => x.WriteExceptionToDebugLogsFile(It.IsAny(), It.IsAny())); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs index 55834de09..a6c5f5a4f 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs @@ -75,6 +75,7 @@ public async Task SetupAsyncTest(string browser, string device, int? screenWidth var testSettings = new TestSettings() { + BrowserConfigurations = new List() { browserConfig }, RecordVideo = true, Timeout = 15 }; @@ -165,6 +166,7 @@ public async Task SetupAsyncExtensionLocaleTest(string testLocale) var testSettings = new TestSettings() { + BrowserConfigurations = new List() { browserConfig }, ExtensionModules = new TestSettingExtensions() { Enable = true } }; diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs index 75a225cf3..81d7c90ab 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs @@ -133,9 +133,10 @@ public virtual bool Verify(TestSettingExtensions settings, string file) private static bool VerifyCertificates() { #if RELEASE - return true; -#endif + return true; +#else return false; +#endif } private List GetTrustedSources(TestSettingExtensions settings) diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs index 98bd6d80a..f5461ceff 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs @@ -89,7 +89,7 @@ public AggregateCatalog LoadModules(TestSettingExtensions settings) var possibleUserManager = DirectoryGetFiles(location, "testengine.user.*.dll"); foreach (var possibleModule in possibleUserManager) { - if ( Checker.Verify(settings, possibleModule)) + if (Checker.Verify(settings, possibleModule)) { match.Add(LoadAssembly(possibleModule)); } diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs index e5adf2258..d40db895b 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs @@ -5,14 +5,12 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -using NuGet.Configuration; -using System.Text.RegularExpressions; namespace Microsoft.PowerApps.TestEngine.PowerFx { diff --git a/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs b/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs index 84b9fa635..7ed786ac6 100644 --- a/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/ITestWebProvider.cs @@ -13,7 +13,7 @@ namespace Microsoft.PowerApps.TestEngine.Providers /// public interface ITestWebProvider { - #nullable enable +#nullable enable public ITestInfraFunctions? TestInfraFunctions { get; set; } public ISingleTestInstanceState? SingleTestInstanceState { get; set; } @@ -21,8 +21,8 @@ public interface ITestWebProvider public ITestState? TestState { get; set; } public ITestProviderState? ProviderState { get; set; } - #nullable disable +#nullable disable /// /// The name of the provider /// diff --git a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs index 198c239fc..1b5fd1dab 100644 --- a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs +++ b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs @@ -5,8 +5,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Helpers; -using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs index ed6095fb6..3714c0670 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs @@ -27,8 +27,6 @@ public class PlaywrightTestInfraFunctions : ITestInfraFunctions private IBrowser Browser { get; set; } private IBrowserContext BrowserContext { get; set; } private IPage Page { get; set; } - - public PlaywrightTestInfraFunctions(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, ITestWebProvider testWebProvider) { _testState = testState; @@ -102,9 +100,9 @@ public async Task SetupAsync(IUserManager userManager) if (!userManager.UseStaticContext) { // Check if a channel has been specified - if (testSettings.BrowserConfigurations.Any(c => !string.IsNullOrEmpty(c.Channel))) + if (!string.IsNullOrEmpty(browserConfig.Channel)) { - launchOptions.Channel = testSettings.BrowserConfigurations.First(c => !string.IsNullOrEmpty(c.Channel)).Channel; + launchOptions.Channel = browserConfig.Channel; } Browser = await browser.LaunchAsync(launchOptions); @@ -157,11 +155,10 @@ public async Task SetupAsync(IUserManager userManager) _singleTestInstanceState.GetLogger().LogInformation($"Using static context in '{location}' using {userManager.Name}"); // Check if a channel has been specified - if (testSettings.BrowserConfigurations.Any(c => !string.IsNullOrEmpty(c.Channel))) + if (!string.IsNullOrEmpty(browserConfig.Channel)) { - staticContext.Channel = testSettings.BrowserConfigurations.First(c => !string.IsNullOrEmpty(c.Channel)).Channel; + staticContext.Channel = browserConfig.Channel; } - BrowserContext = await browser.LaunchPersistentContextAsync(location, staticContext); } else @@ -272,7 +269,7 @@ public async Task GoToUrlAsync(string url) if ((uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp)) { - if ( url != "about:blank") + if (url != "about:blank") { _singleTestInstanceState.GetLogger().LogError("Url must be http/https"); throw new InvalidOperationException(); diff --git a/src/PowerAppsTestEngine/Program.cs b/src/PowerAppsTestEngine/Program.cs index b0007c511..1abbbe1f1 100644 --- a/src/PowerAppsTestEngine/Program.cs +++ b/src/PowerAppsTestEngine/Program.cs @@ -9,8 +9,8 @@ using Microsoft.PowerApps.TestEngine; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Modules; -using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.PowerFx; +using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Reporting; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; @@ -115,13 +115,13 @@ } var logLevel = LogLevel.Information; // Default log level - if (string.IsNullOrEmpty(inputOptions.LogLevel)) + if (string.IsNullOrEmpty(inputOptions.LogLevel) || !Enum.TryParse(inputOptions.LogLevel, true, out logLevel)) { Console.WriteLine($"Unable to parse log level: {inputOptions.LogLevel}, using default"); - Enum.TryParse(inputOptions.LogLevel, true, out logLevel); + logLevel = LogLevel.Information; } - var userAuth ="browser"; // Default to brower authentication + var userAuth = "browser"; // Default to brower authentication if (!string.IsNullOrEmpty(inputOptions.UserAuth)) { userAuth = inputOptions.UserAuth; @@ -158,7 +158,8 @@ } return userManagers.Where(x => x.Name.Equals(userAuth)).First(); }) - .AddTransient(sp => { + .AddTransient(sp => + { var testState = sp.GetRequiredService(); var testWebProviders = testState.GetTestEngineWebProviders(); if (testWebProviders.Count == 0) @@ -177,7 +178,7 @@ .AddScoped() .AddSingleton() .AddSingleton() - + .BuildServiceProvider(); TestEngine testEngine = serviceProvider.GetRequiredService(); diff --git a/src/testengine.module.pause.tests/PauseFunctionTests.cs b/src/testengine.module.pause.tests/PauseFunctionTests.cs index cd310bceb..eddc084e2 100644 --- a/src/testengine.module.pause.tests/PauseFunctionTests.cs +++ b/src/testengine.module.pause.tests/PauseFunctionTests.cs @@ -1,11 +1,11 @@ -using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.PowerFx; +using Microsoft.Extensions.Logging; using Microsoft.Playwright; -using Moq; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; -using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Moq; namespace testengine.module.browserlocale.tests { @@ -80,7 +80,7 @@ public void SkipExecute() MockTestState.Setup(x => x.GetTestSettings()).Returns(settings); MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockContext.Object); mockContext.Setup(x => x.Pages).Returns(new List() { mockPage.Object }); - + MockLogger.Setup(x => x.Log( It.IsAny(), It.IsAny(), @@ -99,4 +99,4 @@ public void SkipExecute() It.IsAny>()), Times.AtLeastOnce); } } -} \ No newline at end of file +} diff --git a/src/testengine.module.pause.tests/PauseModuleTests.cs b/src/testengine.module.pause.tests/PauseModuleTests.cs index 7d1f0dfeb..73abdf2ff 100644 --- a/src/testengine.module.pause.tests/PauseModuleTests.cs +++ b/src/testengine.module.pause.tests/PauseModuleTests.cs @@ -1,11 +1,11 @@ -using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.PowerFx; +using Microsoft.Extensions.Logging; using Microsoft.Playwright; -using Moq; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; -using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Moq; namespace testengine.module.browserlocale.tests { @@ -67,9 +67,9 @@ public void RegisterPowerFxFunction() // Assert MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), - It.IsAny(), - It.Is((v, t) => v.ToString() == "Registered Pause()"), - It.IsAny(), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Registered Pause()"), + It.IsAny(), It.IsAny>()), Times.AtLeastOnce); } @@ -79,11 +79,11 @@ public async Task RegisterNetworkRoute() // Arrange var module = new PauseModule(); - + // Act await module.RegisterNetworkRoute(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPage.Object, TestNetworkRequestMock); // Assert } } -} \ No newline at end of file +} diff --git a/src/testengine.module.pause.tests/Usings.cs b/src/testengine.module.pause.tests/Usings.cs index 8c927eb74..9df1d4217 100644 --- a/src/testengine.module.pause.tests/Usings.cs +++ b/src/testengine.module.pause.tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/src/testengine.module.pause/PauseFunction.cs b/src/testengine.module.pause/PauseFunction.cs index 2c37ae713..ce19f7a49 100644 --- a/src/testengine.module.pause/PauseFunction.cs +++ b/src/testengine.module.pause/PauseFunction.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.PowerFx.Types; -using Microsoft.PowerFx; -using Microsoft.Playwright; -using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; namespace testengine.module { diff --git a/src/testengine.module.pause/PauseModule.cs b/src/testengine.module.pause/PauseModule.cs index 2be127a89..c2ec41d6a 100644 --- a/src/testengine.module.pause/PauseModule.cs +++ b/src/testengine.module.pause/PauseModule.cs @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.PowerFx; using System.ComponentModel.Composition; using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Modules; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.Playwright; +using Microsoft.PowerFx; namespace testengine.module { @@ -33,4 +33,4 @@ public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceStat await Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs b/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs index 1d66ec61e..8b67cba7f 100644 --- a/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs +++ b/src/testengine.provider.canvas.tests/PowerAppsUrlMapperTests.cs @@ -24,5 +24,5 @@ public PowerAppsUrlMapperTests() MockLogger = new Mock(MockBehavior.Strict); } - } + } } diff --git a/src/testengine.provider.canvas.tests/Usings.cs b/src/testengine.provider.canvas.tests/Usings.cs index 8c927eb74..9df1d4217 100644 --- a/src/testengine.provider.canvas.tests/Usings.cs +++ b/src/testengine.provider.canvas.tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/src/testengine.provider.canvas/PowerAppFunctions.cs b/src/testengine.provider.canvas/PowerAppFunctions.cs index 6f8ce27b6..d61bb33dd 100644 --- a/src/testengine.provider.canvas/PowerAppFunctions.cs +++ b/src/testengine.provider.canvas/PowerAppFunctions.cs @@ -26,7 +26,7 @@ public class PowerAppFunctions : ITestWebProvider private string GetItemCountErrorMessage = "Something went wrong when Test Engine tried to get item count."; private string GetPropertyValueErrorMessage = "Something went wrong when Test Engine tried to get property value."; - //private string LoadObjectModelErrorMessage = "Something went wrong when Test Engine tried to load object model."; + private string LoadObjectModelErrorMessage = "Something went wrong when Test Engine tried to load object model."; private string FileNotFoundErrorMessage = "Something went wrong when Test Engine tried to load required dependencies."; private TypeMapping TypeMapping = new TypeMapping(); @@ -50,7 +50,7 @@ public PowerAppFunctions(ITestInfraFunctions? testInfraFunctions, ISingleTestIns this.TestState = testState; } - public string Name { get { return "canvas"; } } + public string Name { get { return "canvas"; } } private async Task GetPropertyValueFromControlAsync(ItemPath itemPath) { @@ -216,9 +216,9 @@ public async Task CheckProviderAsync() public async Task> LoadObjectModelAsync() { var controlDictionary = new Dictionary(); - // SingleTestInstanceState.GetLogger().LogDebug("Start to load power apps object model"); - //await PollingHelper.PollAsync(controlDictionary, (x) => x.Keys.Count == 0, (x) => LoadObjectModelAsyncHelper(x), TestState.GetTestSettings().Timeout, SingleTestInstanceState.GetLogger(), LoadObjectModelErrorMessage); - //SingleTestInstanceState.GetLogger().LogDebug($"Finish loading. Loaded {controlDictionary.Keys.Count} controls"); + SingleTestInstanceState.GetLogger().LogDebug("Start to load power apps object model"); + await PollingHelper.PollAsync(controlDictionary, (x) => x.Keys.Count == 0, (x) => LoadObjectModelAsyncHelper(x), TestState.GetTestSettings().Timeout, SingleTestInstanceState.GetLogger(), LoadObjectModelErrorMessage); + SingleTestInstanceState.GetLogger().LogDebug($"Finish loading. Loaded {controlDictionary.Keys.Count} controls"); return controlDictionary; } diff --git a/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs index 41ea42f15..b54fa4736 100644 --- a/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs +++ b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs @@ -1,11 +1,11 @@ -using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.PowerFx; +using Microsoft.Extensions.Logging; using Microsoft.Playwright; -using Moq; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; -using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; +using Moq; using testengine.user.environment; namespace testengine.user.environment.tests @@ -44,10 +44,11 @@ public BrowserUserManagerModuleTests() public async Task LoginWithBrowserState(bool exists, bool isDirectoryCreated, string files, bool willPause) { // Arrange - if ( willPause ) { - MockPage.Setup(x => x.PauseAsync()).Returns(Task.CompletedTask); + if (willPause) + { + MockPage.Setup(x => x.PauseAsync()).Returns(Task.CompletedTask); } - + var created = false; var userManager = new BrowserUserManagerModule(); diff --git a/src/testengine.user.browser.tests/Usings.cs b/src/testengine.user.browser.tests/Usings.cs index 8c927eb74..9df1d4217 100644 --- a/src/testengine.user.browser.tests/Usings.cs +++ b/src/testengine.user.browser.tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/src/testengine.user.browser/BrowserUserManagerModule.cs b/src/testengine.user.browser/BrowserUserManagerModule.cs index 14b0fc386..95fa86cfa 100644 --- a/src/testengine.user.browser/BrowserUserManagerModule.cs +++ b/src/testengine.user.browser/BrowserUserManagerModule.cs @@ -1,24 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.PowerFx; +using System; using System.ComponentModel.Composition; +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Modules; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.Playwright; -using System; -using System.Text.RegularExpressions; -using System.Linq; using Microsoft.PowerApps.TestEngine.Users; +using Microsoft.PowerFx; namespace testengine.user.environment { [Export(typeof(IUserManager))] public class BrowserUserManagerModule : IUserManager - { + { public string Name { get { return "browser"; } } public int Priority { get { return 100; } } @@ -27,15 +27,15 @@ public class BrowserUserManagerModule : IUserManager public string Location { get; set; } = "BrowserContext"; - private IBrowserContext? Context { get;set; } + private IBrowserContext? Context { get; set; } - public IPage? Page { get;set; } + public IPage? Page { get; set; } - public Func DirectoryExists { get;set; } = (location) => Directory.Exists(location); + public Func DirectoryExists { get; set; } = (location) => Directory.Exists(location); - public Action CreateDirectory { get;set; } = (location) => Directory.CreateDirectory(location); + public Action CreateDirectory { get; set; } = (location) => Directory.CreateDirectory(location); - public Func GetFiles { get;set; } = (path) => Directory.GetFiles(path); + public Func GetFiles { get; set; } = (path) => Directory.GetFiles(path); public async Task LoginAsUserAsync( string desiredUrl, @@ -46,11 +46,13 @@ public async Task LoginAsUserAsync( { Context = context; - if ( ! DirectoryExists(Location)) { + if (!DirectoryExists(Location)) + { CreateDirectory(Location); } - if ( GetFiles(Location).Count() == 0 ) { + if (GetFiles(Location).Count() == 0) + { ValidatePage(); await Page.PauseAsync(); } diff --git a/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs index e842bf4dd..d57621aa6 100644 --- a/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs +++ b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs @@ -1,12 +1,12 @@ -using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.PowerFx; +using Microsoft.Extensions.Logging; using Microsoft.Playwright; -using Moq; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerApps.TestEngine.Tests.Helpers; -using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; +using Moq; using testengine.user.environment; namespace testengine.user.environment.tests @@ -272,7 +272,7 @@ public async Task HandleUserPasswordScreen() // Wait until login is complete and redirect to desired page MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Returns(Task.CompletedTask); - MockBrowserContext.SetupGet(x => x.Pages).Returns(new List{ MockPage.Object}); + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List { MockPage.Object }); var userManagerModule = new EnvironmentUserManagerModule(); userManagerModule.Page = MockPage.Object; @@ -306,7 +306,7 @@ public async Task HandleUserPasswordScreenErrorEntry() // Throw exception as not make it to desired url MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); - MockBrowserContext.SetupGet(x => x.Pages).Returns(new List {MockPage.Object }); + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List { MockPage.Object }); var userManagerModule = new EnvironmentUserManagerModule(); userManagerModule.Page = MockPage.Object; @@ -341,7 +341,7 @@ public async Task HandleUserPasswordScreenUnknownError() // Throw exception as not make it to desired url MockPage.Setup(x => x.WaitForURLAsync(desiredUrl, null)).Throws(new TimeoutException()); - MockBrowserContext.SetupGet(x => x.Pages).Returns(new List{ MockPage.Object }); + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List { MockPage.Object }); var environmentUserManager = new EnvironmentUserManagerModule(); environmentUserManager.Page = MockPage.Object; @@ -351,4 +351,4 @@ public async Task HandleUserPasswordScreenUnknownError() MockPage.Verify(x => x.Locator(It.Is(v => v.Equals(testSelector)), null)); } } -} \ No newline at end of file +} diff --git a/src/testengine.user.environment.tests/Usings.cs b/src/testengine.user.environment.tests/Usings.cs index 8c927eb74..9df1d4217 100644 --- a/src/testengine.user.environment.tests/Usings.cs +++ b/src/testengine.user.environment.tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/src/testengine.user.environment/EnvironmentUserManagerModule.cs b/src/testengine.user.environment/EnvironmentUserManagerModule.cs index 60a640a92..5e0906413 100644 --- a/src/testengine.user.environment/EnvironmentUserManagerModule.cs +++ b/src/testengine.user.environment/EnvironmentUserManagerModule.cs @@ -1,24 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using Microsoft.PowerFx; +using System; using System.ComponentModel.Composition; +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; +using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Modules; using Microsoft.PowerApps.TestEngine.System; using Microsoft.PowerApps.TestEngine.TestInfra; -using Microsoft.Playwright; -using System; -using System.Text.RegularExpressions; -using System.Linq; using Microsoft.PowerApps.TestEngine.Users; +using Microsoft.PowerFx; namespace testengine.user.environment { [Export(typeof(IUserManager))] public class EnvironmentUserManagerModule : IUserManager - { + { public string Name { get { return "environment"; } } public int Priority { get { return 0; } } @@ -33,9 +33,9 @@ public class EnvironmentUserManagerModule : IUserManager public static string StaySignedInSelector = "[id=\"KmsiCheckboxField\"]"; public static string KeepMeSignedInNoSelector = "[id=\"idBtn_Back\"]"; - private IBrowserContext? Context { get;set; } + private IBrowserContext? Context { get; set; } - public IPage? Page { get;set; } + public IPage? Page { get; set; } public async Task LoginAsUserAsync( string desiredUrl, @@ -103,7 +103,8 @@ public async Task LoginAsUserAsync( throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); } - if ( Page == null ) { + if (Page == null) + { Page = context.Pages.First(); } From 6408f999af86fbc6d2fce084c123a1381a7f7e04 Mon Sep 17 00:00:00 2001 From: snamilikonda Date: Fri, 2 Aug 2024 13:25:45 -0700 Subject: [PATCH 03/40] User/snamilikonda/cba (#349) * cba deraft with local certificate provider * refactoring new interface * extracted mef cert module and extendable interface to users * stay signed in dialog * test modifications * whitespace * tests * modifying httpclient for testability and adding tests * pipeline modification to check percentage * pipeline src update * uuid regex for file match * simple pattern since regex not ided * runner admin only * tests * doc for cba * certificate store retrieval * adding certstore implementation and certificate subject name * thresh * thresh --- .github/workflows/build-test.yml | 2 +- .../CertificateBasedAuthentication.md | 14 + samples/buttonclicker/testPlan.cba.fx.yaml | 33 ++ .../Config/TestStateTests.cs | 19 - .../Config/YamlTestConfigParserTests.cs | 19 +- .../SingleTestRunnerTests.cs | 33 +- .../Config/ITestState.cs | 2 + .../Config/IUserCertificateProvider.cs | 13 + .../Config/IUserManagerLogin.cs | 11 + .../Config/TestState.cs | 21 +- .../Config/UserConfiguration.cs | 5 + .../Config/UserManagerLogin.cs | 18 + .../Modules/MefComponents.cs | 4 + .../Modules/TestEngineModuleMEFLoader.cs | 9 + .../SingleTestRunner.cs | 7 +- .../Users/IUserManager.cs | 4 +- src/PowerAppsTestEngine.sln | 45 ++ src/PowerAppsTestEngine/InputOptions.cs | 1 + src/PowerAppsTestEngine/Program.cs | 22 +- .../CertificateStoreProviderTests.cs | 88 ++++ .../Usings.cs | 1 + ...tengine.auth.certificatestore.tests.csproj | 35 ++ .../CertificateStoreProvider.cs | 56 ++ .../testengine.auth.certificatestore.csproj | 28 + .../LocalUserCertificateProviderTests.cs | 61 +++ .../Usings.cs | 1 + ...tengine.auth.localcertificate.tests.csproj | 35 ++ .../LocalUserCertificateProvider.cs | 49 ++ .../testengine.auth.localcertificate.csproj | 27 + .../BrowserUserManagerModuleTests.cs | 5 +- .../BrowserUserManagerModule.cs | 3 +- .../CertificateUserManagerModuleTests.cs | 492 ++++++++++++++++++ .../Usings.cs | 1 + .../testengine.user.certificate.tests.csproj | 38 ++ .../CertificateUserManagerModule.cs | 349 +++++++++++++ .../testengine.user.certificate.csproj | 27 + .../EnvironmentUserManagerModuleTests.cs | 20 +- .../EnvironmentUserManagerModule.cs | 3 +- 38 files changed, 1544 insertions(+), 57 deletions(-) create mode 100644 docs/Extensions/CertificateBasedAuthentication.md create mode 100644 samples/buttonclicker/testPlan.cba.fx.yaml create mode 100644 src/Microsoft.PowerApps.TestEngine/Config/IUserCertificateProvider.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Config/IUserManagerLogin.cs create mode 100644 src/Microsoft.PowerApps.TestEngine/Config/UserManagerLogin.cs create mode 100644 src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs create mode 100644 src/testengine.auth.certificatestore.tests/Usings.cs create mode 100644 src/testengine.auth.certificatestore.tests/testengine.auth.certificatestore.tests.csproj create mode 100644 src/testengine.auth.certificatestore/CertificateStoreProvider.cs create mode 100644 src/testengine.auth.certificatestore/testengine.auth.certificatestore.csproj create mode 100644 src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs create mode 100644 src/testengine.auth.localcertificate.tests/Usings.cs create mode 100644 src/testengine.auth.localcertificate.tests/testengine.auth.localcertificate.tests.csproj create mode 100644 src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs create mode 100644 src/testengine.auth.localcertificate/testengine.auth.localcertificate.csproj create mode 100644 src/testengine.user.certificate.tests/CertificateUserManagerModuleTests.cs create mode 100644 src/testengine.user.certificate.tests/Usings.cs create mode 100644 src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj create mode 100644 src/testengine.user.certificate/CertificateUserManagerModule.cs create mode 100644 src/testengine.user.certificate/testengine.user.certificate.csproj diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e2eccad86..ad5db8211 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -87,7 +87,7 @@ jobs: format: markdown indicators: true output: both - thresholds: '15 15' + thresholds: '10 10' - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 if: github.event_name == 'pull_request' diff --git a/docs/Extensions/CertificateBasedAuthentication.md b/docs/Extensions/CertificateBasedAuthentication.md new file mode 100644 index 000000000..3bf5e19f8 --- /dev/null +++ b/docs/Extensions/CertificateBasedAuthentication.md @@ -0,0 +1,14 @@ +# Certificate based authentication mechanism + +Certificate-based authentication (CBA) in Power Apps environments provides a robust way to secure access to your apps and data by using digital certificates instead of traditional username and password credentials. +This document outlines the feature to configure and author tests that use CBA allowing the Test engine to login and succeed in running the tests. + +An additional option to the existing "UserAuth" ("-u") input option is available where the value for this flag can be set to "certificate" (Previously only "browser" and "environment" options were available). With certificate option the username for the user that runs the test is still fetched from the environment variable configured from the test YAML file, however the certificate fetched using the IUserCertificateProvider implementation that can be plugged in as a MEF module. IUserCertificateProvider provides a X509Certificate2 object for a given certificate's subjectname for a user. The subjectname is provideed as input by the user in the form of another environment variable "CertificateSubjectKey" as part of the UserConfiguration. A sample yaml file testPlan.cba.fx.yaml is provided in the buttonclicker sample. + +Multiple certificate store providers can be added in which offload certificate retrieval into a separate module. A default certificate provider is implemented and can be configured as part of the test configuration using input option "UserAuthType" ("-a") with value defaulting to "localcert". This is a sample implementation that fetches all the pfx files from the "LocalCertificates" folder at the base of the executable and creates a dictionary that has the pfx subject name as key and the file as value. Another such implemntation is "certstore" that fetches the certificate from the local certificate store based on the subjectname, the username and the certificate subject name will be matched and used for the authentication. + +Thus to enable usage of CBA the following changes are required in the YAML. +1. Configure UserAuth as certificate +2. Configure UserAuthType as localcert or certstore (optional unless a different IUserCertificateProvider implementation is provided) +3. If localcert is used then generate a folder LocalCertificates at the executable base and place the pfx for the certificate in it or populate the certificate in the Certificate store for the current user in personal store. +4. Provide the email and the certificate subject name in the environment variables user the persona in the test yaml. \ No newline at end of file diff --git a/samples/buttonclicker/testPlan.cba.fx.yaml b/samples/buttonclicker/testPlan.cba.fx.yaml new file mode 100644 index 000000000..11e0d8791 --- /dev/null +++ b/samples/buttonclicker/testPlan.cba.fx.yaml @@ -0,0 +1,33 @@ +testSuite: + testSuiteName: Button Clicker + testSuiteDescription: Verifies that counter increments when the button is clicked + persona: User1 + appLogicalName: new_buttonclicker_0a877 + onTestCaseStart: Screenshot("buttonclicker_onTestCaseStart.png"); + onTestCaseComplete: Select(ResetButton); + onTestSuiteComplete: Screenshot("buttonclicker_onTestSuiteComplete.png"); + + testCases: + - testCaseName: Case1 + testCaseDescription: Wait for the label to be set to 0 + testSteps: | + = Wait(Label1, "Text", "0"); + //Wait(Label1.Text = "0"); + - testCaseName: Case2 + testCaseDescription: Click the button + testSteps: | + = Select(Button1); + Assert(Label1.Text = "1", "Counter should be incremented to 1"); + Select(Button1); + Assert(Label1.Text = "2", "Counter should be incremented to 2"); + Select(Button1); + Assert(Label1.Text = "3"); + +testSettings: + filePath: ../../../samples/testSettings.yaml + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + certificateSubjectKey: user1CertificateSubject diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs index 94be9c184..bd53943e9 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Config/TestStateTests.cs @@ -532,25 +532,6 @@ public void ParseAndSetTestStateThrowsOnNoEmailKeyInUserConfigurationInEnvironme LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); } - [Theory] - [InlineData("")] - [InlineData(null)] - public void ParseAndSetTestStateThrowsOnNoPasswordKeyInUserConfigurationInEnvironmentVariables(string passwordKey) - { - var state = new TestState(MockTestConfigParser.Object); - var testConfigFile = "testPlan.fx.yaml"; - var testPlanDefinition = GenerateTestPlanDefinition(); - testPlanDefinition.EnvironmentVariables.Users[0].PasswordKey = passwordKey; - MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); - LoggingTestHelper.SetupMock(MockLogger); - var expectedErrorMessage = "Invalid User Input(s): Missing password key"; - MockTestConfigParser.Setup(x => x.ParseTestConfig(It.IsAny(), MockLogger.Object)).Returns(testPlanDefinition); - - var ex = Assert.Throws(() => state.ParseAndSetTestState(testConfigFile, MockLogger.Object)); - Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionTestConfig.ToString(), ex.Message); - LoggingTestHelper.VerifyLogging(MockLogger, expectedErrorMessage, LogLevel.Error, Times.Once()); - } - [Fact] public void ParseAndSetTestStateThrowsOnTestSuiteDefinitionUserNotDefined() { diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Config/YamlTestConfigParserTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Config/YamlTestConfigParserTests.cs index d84306d0a..42ea36282 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Config/YamlTestConfigParserTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Config/YamlTestConfigParserTests.cs @@ -67,7 +67,8 @@ public void YamlTestConfigParserParseTestPlanWithAppLogicalNameTest() users: - personaName: User1 emailKey: user1Email - passwordKey: user1Password"; + passwordKey: user1Password + certificateSubjectKey: user1CertificateSubject"; var filePath = "testplan.fx.yaml"; mockFileSystem.Setup(f => f.ReadAllText(It.IsAny())).Returns(yamlFile); @@ -100,7 +101,7 @@ public void YamlTestConfigParserParseTestPlanWithAppLogicalNameTest() Assert.Equal("User1", testPlan.EnvironmentVariables?.Users[0].PersonaName); Assert.Equal("user1Email", testPlan.EnvironmentVariables?.Users[0].EmailKey); Assert.Equal("user1Password", testPlan.EnvironmentVariables?.Users[0].PasswordKey); - + Assert.Equal("user1CertificateSubject", testPlan.EnvironmentVariables?.Users[0].CertificateSubjectKey); } [Fact] @@ -147,7 +148,8 @@ public void YamlTestConfigParserParseTestPlanWithAppIDTest() users: - personaName: User1 emailKey: user1Email - passwordKey: user1Password"; + passwordKey: user1Password + certificateSubjectKey: user1CertificateSubject"; var filePath = "testplan.fx.yaml"; mockFileSystem.Setup(f => f.ReadAllText(It.IsAny())).Returns(yamlFile); @@ -181,7 +183,7 @@ public void YamlTestConfigParserParseTestPlanWithAppIDTest() Assert.Equal("User1", testPlan.EnvironmentVariables?.Users[0].PersonaName); Assert.Equal("user1Email", testPlan.EnvironmentVariables?.Users[0].EmailKey); Assert.Equal("user1Password", testPlan.EnvironmentVariables?.Users[0].PasswordKey); - + Assert.Equal("user1CertificateSubject", testPlan.EnvironmentVariables?.Users[0].CertificateSubjectKey); } [Fact] @@ -229,7 +231,8 @@ public void YamlTestConfigParserParseTestPlanWithLocaleTest() users: - personaName: User1 emailKey: user1Email - passwordKey: user1Password"; + passwordKey: user1Password + certificateSubjectKey: user1CertificateSubject"; var filePath = "testplan.fx.yaml"; mockFileSystem.Setup(f => f.ReadAllText(It.IsAny())).Returns(yamlFile); @@ -264,7 +267,7 @@ public void YamlTestConfigParserParseTestPlanWithLocaleTest() Assert.Equal("User1", testPlan.EnvironmentVariables?.Users[0].PersonaName); Assert.Equal("user1Email", testPlan.EnvironmentVariables?.Users[0].EmailKey); Assert.Equal("user1Password", testPlan.EnvironmentVariables?.Users[0].PasswordKey); - + Assert.Equal("user1CertificateSubject", testPlan.EnvironmentVariables?.Users[0].CertificateSubjectKey); } [Fact] @@ -276,7 +279,8 @@ public void YamlTestConfigParserParseEnvironmentVariablesTest() var environmentVariablesFile = @"users: - personaName: User1 emailKey: user1Email - passwordKey: user1Password"; + passwordKey: user1Password + certificateSubjectKey: user1CertificateSubject"; var filePath = "environmentVariables.fx.yaml"; mockFileSystem.Setup(f => f.ReadAllText(It.IsAny())).Returns(environmentVariablesFile); @@ -290,6 +294,7 @@ public void YamlTestConfigParserParseEnvironmentVariablesTest() Assert.Equal("User1", environmentVariables.Users[0].PersonaName); Assert.Equal("user1Email", environmentVariables.Users[0].EmailKey); Assert.Equal("user1Password", environmentVariables.Users[0].PasswordKey); + Assert.Equal("user1CertificateSubject", environmentVariables.Users[0].CertificateSubjectKey); } [Fact] diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs index fa185eae4..dcc0e4684 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/SingleTestRunnerTests.cs @@ -41,6 +41,7 @@ public class SingleTestRunnerTests private Mock MockTestEngineEventHandler; private Mock MockEnvironmentVariable; private Mock MockBrowserContext; + private Mock MockUserManagerLogin; public SingleTestRunnerTests() { @@ -58,6 +59,7 @@ public SingleTestRunnerTests() MockTestEngineEventHandler = new Mock(MockBehavior.Strict); MockEnvironmentVariable = new Mock(MockBehavior.Strict); MockBrowserContext = new Mock(MockBehavior.Strict); + MockUserManagerLogin = new Mock(MockBehavior.Strict); } private void SetupMocks(string testRunId, string testSuiteId, string testId, string appUrl, TestSuiteDefinition testSuiteDefinition, bool powerFxTestSuccess, string[]? additionalFiles, string testSuitelocale) @@ -127,7 +129,8 @@ private void SetupMocks(string testRunId, string testSuiteId, string testId, str It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny()) + It.IsAny(), + It.IsAny()) ).Returns(Task.CompletedTask); MockTestWebProvider.Setup(x => x.GenerateTestUrl("", "")).Returns(appUrl); @@ -161,7 +164,8 @@ private void VerifySuccessfulTestExecution(string testResultDirectory, TestSuite It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny()), Times.Once()); + It.IsAny(), + It.IsAny()), Times.Once()); MockTestInfraFunctions.Verify(x => x.SetupNetworkRequestMockAsync(), Times.Once()); MockTestWebProvider.Verify(x => x.GenerateTestUrl("", ""), Times.Once()); MockTestInfraFunctions.Verify(x => x.GoToUrlAsync(appUrl), Times.Once()); @@ -211,7 +215,8 @@ public async Task SingleTestRunnerSuccessWithTestDataOneTest(string[]? additiona MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); @@ -242,7 +247,8 @@ public async Task SingleTestRunnerSuccessWithTestDataTwoTest(string[]? additiona MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataTwo(); @@ -270,7 +276,8 @@ public async Task SingleTestRunnerCanOnlyBeRunOnce() MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); @@ -296,7 +303,8 @@ public async Task SingleTestRunnerPowerFxTestFail() MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); @@ -322,7 +330,8 @@ public async Task SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper(Action< MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); @@ -395,7 +404,7 @@ public async Task LoginAsUserThrowsTest() await SingleTestRunnerHandlesExceptionsThrownCorrectlyHelper((Exception exceptionToThrow) => { MockUserManager.Setup(x => - x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) ).Throws(exceptionToThrow); }); } @@ -443,7 +452,8 @@ public async Task PowerFxExecuteThrowsTest() MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); @@ -476,7 +486,8 @@ public async Task UserInputExceptionHandlingTest() MockLoggerFactory.Object, MockTestEngineEventHandler.Object, MockEnvironmentVariable.Object, - MockTestWebProvider.Object); + MockTestWebProvider.Object, + MockUserManagerLogin.Object); var testData = new TestDataOne(); SetupMocks(testData.testRunId, testData.testSuiteId, testData.testId, testData.appUrl, testData.testSuiteDefinition, true, testData.additionalFiles, testData.testSuiteLocale); @@ -484,7 +495,7 @@ public async Task UserInputExceptionHandlingTest() // Specific setup for this test var exceptionToThrow = new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); - MockUserManager.Setup(x => x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(exceptionToThrow); + MockUserManager.Setup(x => x.LoginAsUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(exceptionToThrow); MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); // Act diff --git a/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs b/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs index 7de117100..b2c703616 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/ITestState.cs @@ -144,5 +144,7 @@ public interface ITestState /// Get the list of registered Test engine web test providers /// public List GetTestEngineWebProviders(); + + public List GetTestEngineAuthProviders(); } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/IUserCertificateProvider.cs b/src/Microsoft.PowerApps.TestEngine/Config/IUserCertificateProvider.cs new file mode 100644 index 000000000..cd23b4563 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Config/IUserCertificateProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Microsoft.PowerApps.TestEngine.Config +{ + public interface IUserCertificateProvider + { + public string Name { get; } + public X509Certificate2 RetrieveCertificateForUser(string userIdentifier); + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Config/IUserManagerLogin.cs b/src/Microsoft.PowerApps.TestEngine/Config/IUserManagerLogin.cs new file mode 100644 index 000000000..eb49f38a1 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Config/IUserManagerLogin.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerApps.TestEngine.Config +{ + public interface IUserManagerLogin + { + IUserCertificateProvider UserCertificateProvider { get; } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs index 505676ce2..a6d4c55c4 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestState.cs @@ -38,6 +38,8 @@ public class TestState : ITestState private List WebProviders { get; set; } = new List(); + private List CertificateProviders { get; set; } = new List(); + private bool IsValid { get; set; } = false; public TestState(ITestConfigParser testConfigParser) @@ -164,11 +166,6 @@ public void ParseAndSetTestState(string testConfigFile, ILogger logger) { userInputExceptionMessages.Add("Missing email key"); } - - if (string.IsNullOrEmpty(userConfig.PasswordKey)) - { - userInputExceptionMessages.Add("Missing password key"); - } } } @@ -315,6 +312,9 @@ public void LoadExtensionModules(ILogger logger) var webProviders = mefComponents.WebProviderModules.Select(v => v.Value).ToArray(); this.AddWebProviderModules(webProviders); + + var certificateProviders = mefComponents.CertificateProviderModules.Select(v => v.Value).ToArray(); + this.AddCertificateProviders(certificateProviders); } public void AddModules(IEnumerable modules) @@ -335,6 +335,12 @@ public void AddWebProviderModules(IEnumerable modules) WebProviders.AddRange(modules); } + public void AddCertificateProviders(IEnumerable modules) + { + CertificateProviders.Clear(); + CertificateProviders.AddRange(modules); + } + public List GetTestEngineModules() { return Modules; @@ -349,5 +355,10 @@ public List GetTestEngineWebProviders() { return WebProviders; } + + public List GetTestEngineAuthProviders() + { + return CertificateProviders; + } } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/UserConfiguration.cs b/src/Microsoft.PowerApps.TestEngine/Config/UserConfiguration.cs index f8e1923e5..78bf151bc 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/UserConfiguration.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/UserConfiguration.cs @@ -22,5 +22,10 @@ public class UserConfiguration /// Gets or sets the environment variable key for fetching the user password. /// public string PasswordKey { get; set; } = ""; + + /// + /// Gets or sets the environment variable key for fetching the user certificate subject. + /// + public string CertificateSubjectKey { get; set; } = ""; } } diff --git a/src/Microsoft.PowerApps.TestEngine/Config/UserManagerLogin.cs b/src/Microsoft.PowerApps.TestEngine/Config/UserManagerLogin.cs new file mode 100644 index 000000000..fee64d4fb --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine/Config/UserManagerLogin.cs @@ -0,0 +1,18 @@ +namespace Microsoft.PowerApps.TestEngine.Config +{ + public class UserManagerLogin : IUserManagerLogin + { + private readonly IUserCertificateProvider _userCertificateProvider; + // add any other interfaces and create instances + //private readonly IEnvironmentVariable _environmentVariableProvider; + + public UserManagerLogin(IUserCertificateProvider userCertificateProvider)//, IEnvironmentVariable environmentVariableProvider) + { + _userCertificateProvider = userCertificateProvider; + //_environmentVariableProvider = environmentVariableProvider; + } + + public IUserCertificateProvider UserCertificateProvider => _userCertificateProvider; + //public IEnvironmentVariable EnvironmentVariableProvider => _environmentVariableProvider; + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs b/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs index 39458fe4a..8e47336fb 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/MefComponents.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.ComponentModel.Composition; +using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.Providers; using Microsoft.PowerApps.TestEngine.Users; @@ -21,6 +22,9 @@ public class MefComponents [ImportMany] public IEnumerable> WebProviderModules; + + [ImportMany] + public IEnumerable> CertificateProviderModules; #pragma warning restore 0649 } } diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs index f5461ceff..a88a65745 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs @@ -104,6 +104,15 @@ public AggregateCatalog LoadModules(TestSettingExtensions settings) } } + var possibleAuthTypeProviderModule = DirectoryGetFiles(location, "testengine.auth.*.dll"); + foreach (var possibleModule in possibleAuthTypeProviderModule) + { + if (Checker.Verify(settings, possibleModule)) + { + match.Add(LoadAssembly(possibleModule)); + } + } + // Check if need to deny a module or a specific list of modules are allowed if (settings.DenyModule.Count > 0 || (settings.AllowModule.Count() > 1)) { diff --git a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs index 1b5fd1dab..1107e9d99 100644 --- a/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs +++ b/src/Microsoft.PowerApps.TestEngine/SingleTestRunner.cs @@ -30,6 +30,7 @@ public class SingleTestRunner : ISingleTestRunner private readonly ILoggerFactory _loggerFactory; private readonly ITestEngineEvents _eventHandler; private readonly IEnvironmentVariable _environmentVariable; + private readonly IUserManagerLogin _userManagerLoginType; private ITestWebProvider _testWebProvider; private ILogger Logger { get; set; } @@ -48,7 +49,8 @@ public SingleTestRunner(ITestReporter testReporter, ILoggerFactory loggerFactory, ITestEngineEvents eventHandler, IEnvironmentVariable environmentVariable, - ITestWebProvider testWebProvider) + ITestWebProvider testWebProvider, + IUserManagerLogin userManagerLogin) { _testReporter = testReporter; _powerFxEngine = powerFxEngine; @@ -61,6 +63,7 @@ public SingleTestRunner(ITestReporter testReporter, _eventHandler = eventHandler; _environmentVariable = environmentVariable; _testWebProvider = testWebProvider; + _userManagerLoginType = userManagerLogin; } public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSuiteDefinition testSuiteDefinition, BrowserConfiguration browserConfig, string domain, string queryParams, CultureInfo locale) @@ -132,7 +135,7 @@ public async Task RunTestAsync(string testRunId, string testRunDirectory, TestSu _testReporter.TestRunAppURL = desiredUrl; // Log in user - await _userManager.LoginAsUserAsync(desiredUrl, TestInfraFunctions.GetContext(), _state, TestState, _environmentVariable); + await _userManager.LoginAsUserAsync(desiredUrl, TestInfraFunctions.GetContext(), _state, TestState, _environmentVariable, _userManagerLoginType); // Set up Power Fx _powerFxEngine.Setup(); diff --git a/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs b/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs index 9e7e0432d..cc580703f 100644 --- a/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs +++ b/src/Microsoft.PowerApps.TestEngine/Users/IUserManager.cs @@ -5,6 +5,7 @@ using Microsoft.Playwright; using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.System; +using NuGet.Configuration; namespace Microsoft.PowerApps.TestEngine.Users { @@ -49,6 +50,7 @@ public Task LoginAsUserAsync( IBrowserContext context, ITestState testState, ISingleTestInstanceState singleTestInstanceState, - IEnvironmentVariable environmentVariable); + IEnvironmentVariable environmentVariable, + IUserManagerLogin userManagerLogin); } } diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index 16efe04c3..6c5f08077 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -46,6 +46,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause.tests", "testengine.module.pause.tests\testengine.module.pause.tests.csproj", "{3D9F90F2-0937-486D-AA0B-BFE425354F4A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.certificate", "testengine.user.certificate\testengine.user.certificate.csproj", "{998935DC-E292-4818-A47E-3CCA365B7C5E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auth", "Auth", "{F5DD02A2-1BA8-481C-A7ED-E36577C2CB15}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.localcertificate", "testengine.auth.localcertificate\testengine.auth.localcertificate.csproj", "{340111C2-8434-46CF-BE93-12E7C484C955}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.user.certificate.tests", "testengine.user.certificate.tests\testengine.user.certificate.tests.csproj", "{11605494-3C4E-41E8-B68E-FA347A3CA8E4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.localcertificate.tests", "testengine.auth.localcertificate.tests\testengine.auth.localcertificate.tests.csproj", "{7183776B-21BB-4318-958B-62B325CC1A7D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.certificatestore", "testengine.auth.certificatestore\testengine.auth.certificatestore.csproj", "{EF3A270A-53A4-4C08-B45B-7C6993593446}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "testengine.auth.certificatestore.tests", "testengine.auth.certificatestore.tests\testengine.auth.certificatestore.tests.csproj", "{36F79923-74AD-424E-8A74-6902628FBF58}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +114,30 @@ Global {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.Build.0 = Release|Any CPU + {998935DC-E292-4818-A47E-3CCA365B7C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {998935DC-E292-4818-A47E-3CCA365B7C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {998935DC-E292-4818-A47E-3CCA365B7C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {998935DC-E292-4818-A47E-3CCA365B7C5E}.Release|Any CPU.Build.0 = Release|Any CPU + {340111C2-8434-46CF-BE93-12E7C484C955}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {340111C2-8434-46CF-BE93-12E7C484C955}.Debug|Any CPU.Build.0 = Debug|Any CPU + {340111C2-8434-46CF-BE93-12E7C484C955}.Release|Any CPU.ActiveCfg = Release|Any CPU + {340111C2-8434-46CF-BE93-12E7C484C955}.Release|Any CPU.Build.0 = Release|Any CPU + {11605494-3C4E-41E8-B68E-FA347A3CA8E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11605494-3C4E-41E8-B68E-FA347A3CA8E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11605494-3C4E-41E8-B68E-FA347A3CA8E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11605494-3C4E-41E8-B68E-FA347A3CA8E4}.Release|Any CPU.Build.0 = Release|Any CPU + {7183776B-21BB-4318-958B-62B325CC1A7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7183776B-21BB-4318-958B-62B325CC1A7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7183776B-21BB-4318-958B-62B325CC1A7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7183776B-21BB-4318-958B-62B325CC1A7D}.Release|Any CPU.Build.0 = Release|Any CPU + {EF3A270A-53A4-4C08-B45B-7C6993593446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF3A270A-53A4-4C08-B45B-7C6993593446}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF3A270A-53A4-4C08-B45B-7C6993593446}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF3A270A-53A4-4C08-B45B-7C6993593446}.Release|Any CPU.Build.0 = Release|Any CPU + {36F79923-74AD-424E-8A74-6902628FBF58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36F79923-74AD-424E-8A74-6902628FBF58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36F79923-74AD-424E-8A74-6902628FBF58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36F79923-74AD-424E-8A74-6902628FBF58}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +155,13 @@ Global {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} {B3A02421-223D-4E80-A8CE-977B425A6EB2} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} {3D9F90F2-0937-486D-AA0B-BFE425354F4A} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {998935DC-E292-4818-A47E-3CCA365B7C5E} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} = {63A04DC1-C37E-43E6-8FEA-A480483E11F8} + {340111C2-8434-46CF-BE93-12E7C484C955} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} + {11605494-3C4E-41E8-B68E-FA347A3CA8E4} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {7183776B-21BB-4318-958B-62B325CC1A7D} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} + {EF3A270A-53A4-4C08-B45B-7C6993593446} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} + {36F79923-74AD-424E-8A74-6902628FBF58} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7E7B2C01-DDE2-4C5A-96C3-AF474B074331} diff --git a/src/PowerAppsTestEngine/InputOptions.cs b/src/PowerAppsTestEngine/InputOptions.cs index 39509fe90..aee18ab2f 100644 --- a/src/PowerAppsTestEngine/InputOptions.cs +++ b/src/PowerAppsTestEngine/InputOptions.cs @@ -15,5 +15,6 @@ public class InputOptions public string? Modules { get; set; } public string? UserAuth { get; set; } public string? Provider { get; set; } + public string? UserAuthType { get; set; } } } diff --git a/src/PowerAppsTestEngine/Program.cs b/src/PowerAppsTestEngine/Program.cs index 1abbbe1f1..7425df636 100644 --- a/src/PowerAppsTestEngine/Program.cs +++ b/src/PowerAppsTestEngine/Program.cs @@ -28,7 +28,8 @@ { "-d", "Domain" }, { "-m", "Modules" }, { "-u", "UserAuth" }, - { "-p", "Provider" } + { "-p", "Provider" }, + { "-a", "UserAuthType"} }; var inputOptions = new ConfigurationBuilder() @@ -133,6 +134,12 @@ provider = inputOptions.Provider; } + var auth = "localcert"; + if (!string.IsNullOrEmpty(inputOptions.UserAuthType)) + { + auth = inputOptions.UserAuthType; + } + try { using var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder @@ -169,6 +176,17 @@ } return testWebProviders.Where(x => x.Name.Equals(provider)).First(); }) + .AddSingleton(sp => + { + var testState = sp.GetRequiredService(); + var testAuthProviders = testState.GetTestEngineAuthProviders(); + if (testAuthProviders.Count == 0) + { + testState.LoadExtensionModules(logger); + testAuthProviders = testState.GetTestEngineAuthProviders(); + } + return testAuthProviders.Where(x => x.Name.Equals(auth)).First(); + }) .AddSingleton() .AddSingleton() .AddScoped() @@ -177,8 +195,8 @@ .AddSingleton() .AddScoped() .AddSingleton() + .AddSingleton() .AddSingleton() - .BuildServiceProvider(); TestEngine testEngine = serviceProvider.GetRequiredService(); diff --git a/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs b/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs new file mode 100644 index 000000000..b9decb56b --- /dev/null +++ b/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Moq; +using Xunit; + +namespace testengine.auth.certificatestore.tests +{ + public class CertificateStoreProviderTests + { + private readonly CertificateStoreProvider provider; + + public CertificateStoreProviderTests() + { + provider = new CertificateStoreProvider(); + } + + [Fact] + public void NameProperty_ShouldReturnLocalCert() + { + // Act + var name = provider.Name; + + // Assert + Assert.Equal("certstore", name); + } + + [Fact] + public void RetrieveCertificateForUser_BySubjectName_ReturnsCertificate() + { + // Arrange + string userSubjectName = $"CN={Guid.NewGuid().ToString()}"; + X509Certificate2 mockCertificate; + using (var rsa = RSA.Create(2048)) + { + var request = new CertificateRequest(userSubjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + mockCertificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + try + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + store.Add(mockCertificate); + store.Close(); + } + + // Act + X509Certificate2 certificate = provider.RetrieveCertificateForUser(userSubjectName); + + // Assert + Assert.NotNull(certificate); + Assert.Equal(mockCertificate, certificate); + + } + finally + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + store.Remove(mockCertificate); + store.Close(); + } + } + } + + [Fact] + public void RetrieveCertificateForUser_CertificateNotFound_ThrowsInvalidOperationException() + { + // Arrange + string userSubjectName = "nonexistentuser"; + + // Act & Assert + Assert.Null(provider.RetrieveCertificateForUser(userSubjectName)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void RetrieveCertificateForUser_NullOrEmptyUsername_ThrowsArgumentException(string userSubjectName) + { + // Act & Assert + Assert.Null(provider.RetrieveCertificateForUser(userSubjectName)); + } + } +} diff --git a/src/testengine.auth.certificatestore.tests/Usings.cs b/src/testengine.auth.certificatestore.tests/Usings.cs new file mode 100644 index 000000000..9df1d4217 --- /dev/null +++ b/src/testengine.auth.certificatestore.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/testengine.auth.certificatestore.tests/testengine.auth.certificatestore.tests.csproj b/src/testengine.auth.certificatestore.tests/testengine.auth.certificatestore.tests.csproj new file mode 100644 index 000000000..e2261b827 --- /dev/null +++ b/src/testengine.auth.certificatestore.tests/testengine.auth.certificatestore.tests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/src/testengine.auth.certificatestore/CertificateStoreProvider.cs b/src/testengine.auth.certificatestore/CertificateStoreProvider.cs new file mode 100644 index 000000000..3a684310f --- /dev/null +++ b/src/testengine.auth.certificatestore/CertificateStoreProvider.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel.Composition; +using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.PowerApps.TestEngine.Config; + +namespace testengine.auth +{ + /// + /// Functions for interacting with the Power App + /// + [Export(typeof(IUserCertificateProvider))] + public class CertificateStoreProvider : IUserCertificateProvider + { + internal static Func GetCertStore = () => new X509Store(StoreName.My, StoreLocation.CurrentUser); + + public string Name { get { return "certstore"; } } + + public CertificateStoreProvider() + { + } + + public X509Certificate2? RetrieveCertificateForUser(string userIdentifier) + { + if (string.IsNullOrEmpty(userIdentifier)) + { + return null; + } + userIdentifier = userIdentifier.Trim(); + + X509Store store = GetCertStore(); + store.Open(OpenFlags.ReadOnly); + + try + { + foreach (X509Certificate2 certificate in store.Certificates) + { + if (certificate.SubjectName.Name != null && certificate.SubjectName.Name.Contains(userIdentifier, StringComparison.OrdinalIgnoreCase)) + { + return certificate; + } + } + + return null; + } + finally + { + store.Close(); + } + } + } +} diff --git a/src/testengine.auth.certificatestore/testengine.auth.certificatestore.csproj b/src/testengine.auth.certificatestore/testengine.auth.certificatestore.csproj new file mode 100644 index 000000000..e93dff471 --- /dev/null +++ b/src/testengine.auth.certificatestore/testengine.auth.certificatestore.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + true + ../../35MSSharedLib1024.snk + true + © Microsoft Corporation. All rights reserved. + true + 1.0 + + + + + + + + + + + + + + + + diff --git a/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs b/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs new file mode 100644 index 000000000..0d5175975 --- /dev/null +++ b/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs @@ -0,0 +1,61 @@ +using Moq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace testengine.auth.tests +{ + public class LocalUserCertificateProviderTests + { + [Fact] + public void NameProperty_ShouldReturnLocalCert() + { + // Arrange + var provider = new LocalUserCertificateProvider(); + + // Act + var name = provider.Name; + + // Assert + Assert.Equal("localcert", name); + } + + [Fact] + public void Constructor_ShouldLoadCertificatesFromDirectory() + { + // Arrange + var certDir = "LocalCertificates"; + Directory.CreateDirectory(certDir); + var pfxFilePath = Path.Combine(certDir, "testcert.pfx"); + + // Create a test certificate + using (var rsa = RSA.Create(2048)) + { + var request = new CertificateRequest($"CN=testcert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + File.WriteAllBytes(pfxFilePath, certificate.Export(X509ContentType.Pfx)); + } + + // Act + var provider = new LocalUserCertificateProvider(); + + // Assert + Assert.NotNull(provider.RetrieveCertificateForUser("CN=testcert")); + + // Cleanup + Directory.Delete(certDir, true); + } + + [Fact] + public void RetrieveCertificateForUser_ShouldReturnNullForNonExistingUser() + { + // Arrange + var provider = new LocalUserCertificateProvider(); + + // Act + var cert = provider.RetrieveCertificateForUser("nonexistinguser"); + + // Assert + Assert.Null(cert); + } + } +} diff --git a/src/testengine.auth.localcertificate.tests/Usings.cs b/src/testengine.auth.localcertificate.tests/Usings.cs new file mode 100644 index 000000000..9df1d4217 --- /dev/null +++ b/src/testengine.auth.localcertificate.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/testengine.auth.localcertificate.tests/testengine.auth.localcertificate.tests.csproj b/src/testengine.auth.localcertificate.tests/testengine.auth.localcertificate.tests.csproj new file mode 100644 index 000000000..666403831 --- /dev/null +++ b/src/testengine.auth.localcertificate.tests/testengine.auth.localcertificate.tests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs b/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs new file mode 100644 index 000000000..45b5a71b3 --- /dev/null +++ b/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel.Composition; +using System.Security.Cryptography.X509Certificates; +using Microsoft.PowerApps.TestEngine.Config; + +namespace testengine.auth +{ + /// + /// Functions for interacting with the Power App + /// + [Export(typeof(IUserCertificateProvider))] + public class LocalUserCertificateProvider: IUserCertificateProvider + { + public string Name { get { return "localcert"; } } + + private Dictionary emailCertificateDict = new Dictionary(); + + public LocalUserCertificateProvider() + { + var certDir = "LocalCertificates"; + var password = ""; + if (Directory.Exists(certDir)) + { + string[] pfxFiles = Directory.GetFiles(certDir, "*.pfx"); + + foreach (var pfxFile in pfxFiles) + { + // Load the certificate + X509Certificate2 cert = new X509Certificate2(pfxFile, password); + emailCertificateDict.Add(cert.SubjectName.Name, cert); + } + } + } + + public X509Certificate2? RetrieveCertificateForUser(string userIdentifier) + { + if (emailCertificateDict.TryGetValue(userIdentifier, out X509Certificate2 certificate)) + { + return certificate; + } + else + { + return null; + } + } + } +} diff --git a/src/testengine.auth.localcertificate/testengine.auth.localcertificate.csproj b/src/testengine.auth.localcertificate/testengine.auth.localcertificate.csproj new file mode 100644 index 000000000..60ed9b3ea --- /dev/null +++ b/src/testengine.auth.localcertificate/testengine.auth.localcertificate.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + true + ../../35MSSharedLib1024.snk + true + © Microsoft Corporation. All rights reserved. + true + 1.0 + + + + + + + + + + + + + + + diff --git a/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs index b54fa4736..5d6ad61f3 100644 --- a/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs +++ b/src/testengine.user.browser.tests/BrowserUserManagerModuleTests.cs @@ -22,6 +22,7 @@ public class BrowserUserManagerModuleTests private Mock MockPage; private Mock MockElementHandle; private Mock MockFileSystem; + private Mock MockUserManagerLogin; public BrowserUserManagerModuleTests() { @@ -35,6 +36,7 @@ public BrowserUserManagerModuleTests() MockPage = new Mock(MockBehavior.Strict); MockElementHandle = new Mock(MockBehavior.Strict); MockFileSystem = new Mock(MockBehavior.Strict); + MockUserManagerLogin = new Mock(MockBehavior.Strict); } [Theory] @@ -62,7 +64,8 @@ await userManager.LoginAsUserAsync("*", MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object); // Assert Assert.True(created == isDirectoryCreated); diff --git a/src/testengine.user.browser/BrowserUserManagerModule.cs b/src/testengine.user.browser/BrowserUserManagerModule.cs index 95fa86cfa..5d584b37a 100644 --- a/src/testengine.user.browser/BrowserUserManagerModule.cs +++ b/src/testengine.user.browser/BrowserUserManagerModule.cs @@ -42,7 +42,8 @@ public async Task LoginAsUserAsync( IBrowserContext context, ITestState testState, ISingleTestInstanceState singleTestInstanceState, - IEnvironmentVariable environmentVariable) + IEnvironmentVariable environmentVariable, + IUserManagerLogin userManagerLogin) { Context = context; diff --git a/src/testengine.user.certificate.tests/CertificateUserManagerModuleTests.cs b/src/testengine.user.certificate.tests/CertificateUserManagerModuleTests.cs new file mode 100644 index 000000000..0b74604cb --- /dev/null +++ b/src/testengine.user.certificate.tests/CertificateUserManagerModuleTests.cs @@ -0,0 +1,492 @@ +using System.Net; +using System.Reflection; +using System.Runtime.ConstrainedExecution; +using System.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerApps.TestEngine.Tests.Helpers; +using Moq; +using Moq.Protected; +using testengine.auth; +using testengine.user.environment; + +namespace testengine.user.environment.tests +{ + public class CertificateUserManagerModuleTests + { + private Mock MockBrowserState; + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockSingleTestInstanceState; + private Mock MockEnvironmentVariable; + private TestSuiteDefinition TestSuiteDefinition; + private Mock MockLogger; + private Mock MockBrowserContext; + private Mock MockPage; + private Mock MockElementHandle; + private Mock MockFileSystem; + private Mock MockUserManagerLogin; + private Mock MockUserCertificateProvider; + private X509Certificate2 MockCert; + + public CertificateUserManagerModuleTests() + { + MockBrowserState = new Mock(MockBehavior.Strict); + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockEnvironmentVariable = new Mock(MockBehavior.Strict); + TestSuiteDefinition = new TestSuiteDefinition() + { + TestSuiteName = "Test1", + TestSuiteDescription = "First test", + AppLogicalName = "logicalAppName1", + Persona = "User1", + TestCases = new List() + { + new TestCase + { + TestCaseName = "Test Case Name", + TestCaseDescription = "Test Case Description", + TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" + } + } + }; + MockLogger = new Mock(MockBehavior.Strict); + MockBrowserContext = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + MockElementHandle = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockUserManagerLogin = new Mock(MockBehavior.Strict); + MockUserCertificateProvider = new Mock(MockBehavior.Strict); + using (var rsa = RSA.Create(2048)) + { + var request = new CertificateRequest($"CN=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + MockCert = new X509Certificate2(certificate.Export(X509ContentType.Pfx)); + } + } + + [Fact] + public async Task LoginAsUserSuccessTest() + { + var userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = "user1Email", + CertificateSubjectKey = "user1CertificateSubject" + }; + + var email = "someone@example.com"; + var certSubject = "CN=test"; + + MockLogger = new Mock(); + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.CertificateSubjectKey)).Returns(certSubject); + MockTestInfraFunctions.Setup(x => x.GoToUrlAsync(It.IsAny())).Returns(Task.CompletedTask); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + var keyboard = new Mock(MockBehavior.Strict); + + // Email Address + MockPage.Setup(x => x.Locator(CertificateUserManagerModule.EmailSelector, null)).Returns(new Mock().Object); + MockPage.Setup(x => x.TypeAsync(CertificateUserManagerModule.EmailSelector, email, It.IsAny())).Returns(Task.CompletedTask); + keyboard.Setup(x => x.PressAsync("Tab", It.IsAny())) + .Returns(Task.CompletedTask); + MockPage.SetupGet(x => x.Keyboard).Returns(keyboard.Object); + MockPage.Setup(x => x.ClickAsync(CertificateUserManagerModule.SubmitButtonSelector, null)).Returns(Task.CompletedTask); + + // Enter Password and keep me signed in + MockPage.Setup(x => x.ClickAsync(CertificateUserManagerModule.SubmitButtonSelector, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync(CertificateUserManagerModule.StaySignedInSelector, null)).Returns(Task.CompletedTask); + MockPage.Setup(x => x.ClickAsync(CertificateUserManagerModule.KeepMeSignedInNoSelector, null)).Returns(Task.CompletedTask); + + // Now wait for the requested URL assuming login now complete + MockPage.Setup(x => x.WaitForURLAsync("*", null)).Returns(Task.CompletedTask); + + MockUserCertificateProvider.Setup(x => x.RetrieveCertificateForUser(It.IsAny())).Returns(MockCert); + MockUserManagerLogin.Setup(x => x.UserCertificateProvider).Returns(MockUserCertificateProvider.Object); + + MockPage.Setup(x => x.Url).Returns("https://contoso.powerappsportals.com"); + MockPage.Setup(x => x.RouteAsync(It.IsAny(), It.IsAny>(), null)).Returns(Task.CompletedTask); + + MockPage.Setup(x => x.WaitForSelectorAsync("[id=\"KmsiCheckboxField\"]", It.IsAny())).Returns(Task.FromResult(MockElementHandle.Object)); + // Simulate Click to stay signed in + MockPage.Setup(x => x.ClickAsync("[id=\"idBtn_Back\"]", null)).Returns(Task.CompletedTask); + // Wait until login is complete and redirect to desired page + MockPage.Setup(x => x.WaitForURLAsync("*", null)).Returns(Task.CompletedTask); + + + var mockLocatorObject = new Mock(); + mockLocatorObject.Setup(locator => locator.IsVisibleAsync(It.IsAny())).ReturnsAsync(true); + MockPage.Setup(x => x.GetByRole(It.IsAny(), It.IsAny())).Returns(mockLocatorObject.Object); + mockLocatorObject.Setup(re => re.Or(mockLocatorObject.Object)).Returns(mockLocatorObject.Object); + + var userManager = new CertificateUserManagerModule(); + + var responseReceivedField = typeof(CertificateUserManagerModule).GetField("responseReceived", BindingFlags.NonPublic | BindingFlags.Instance); + var mockResponseReceived = new TaskCompletionSource(); + mockResponseReceived.SetResult(true); + responseReceivedField.SetValue(userManager, mockResponseReceived); + + userManager.Page = MockPage.Object; + + + await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object); + + MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); + MockTestState.Verify(x => x.GetUserConfiguration(userConfiguration.PersonaName), Times.Once()); + MockEnvironmentVariable.Verify(x => x.GetVariable(userConfiguration.EmailKey), Times.Once()); + } + + [Fact] + public async Task LoginUserAsyncThrowsOnNullTestDefinitionTest() + { + TestSuiteDefinition testSuiteDefinition = null; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new CertificateUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task LoginUserAsyncThrowsOnInvalidPersonaTest(string persona) + { + var testSuiteDefinition = new TestSuiteDefinition() + { + TestSuiteName = "Test1", + TestSuiteDescription = "First test", + AppLogicalName = "logicalAppName1", + Persona = persona, + TestCases = new List() + { + new TestCase + { + TestCaseName = "Test Case Name", + TestCaseDescription = "Test Case Description", + TestSteps = "Assert(1 + 1 = 2, \"1 + 1 should be 2 \")" + } + } + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(testSuiteDefinition); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new CertificateUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); + } + + [Fact] + public async Task LoginUserAsyncThrowsOnNullUserConfigTest() + { + UserConfiguration userConfiguration = null; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new CertificateUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task LoginUserAsyncThrowsOnInvalidUserConfigTest(string emailKey) + { + UserConfiguration userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = emailKey + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + + var userManager = new CertificateUserManagerModule(); + await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); + } + + [Theory] + [InlineData(null, "", "set", "User email cannot be null. Please check if the environment variable is set properly.")] + [InlineData("", "", "set", "User email cannot be null. Please check if the environment variable is set properly.")] + [InlineData("someone@example.com", "CN=test", "setProvider", "Certificate provider cannot be null. Please ensure certificate provider for user.")] + [InlineData("someone@example.com", "CN=test", "setCert", "Certificate cannot be null. Please ensure certificate for user.")] + [InlineData("someone@example.com", null, "set", "User certificate subject name cannot be null. Please check if the environment variable is set properly.")] + [InlineData("someone@example.com", "", "set", "User certificate subject name cannot be null. Please check if the environment variable is set properly.")] + public async Task LoginUserAsyncThrowsOnInvalidEnviromentVariablesTest(string email, string certname, string setAsNull, string message) + { + UserConfiguration userConfiguration = new UserConfiguration() + { + PersonaName = "User1", + EmailKey = "user1Email", + CertificateSubjectKey = "user1CertificateSubject" + }; + + MockSingleTestInstanceState.Setup(x => x.GetTestSuiteDefinition()).Returns(TestSuiteDefinition); + MockTestState.Setup(x => x.GetUserConfiguration(It.IsAny())).Returns(userConfiguration); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.EmailKey)).Returns(email); + MockEnvironmentVariable.Setup(x => x.GetVariable(userConfiguration.CertificateSubjectKey)).Returns(certname); + + if (setAsNull == "setProvider") + { + MockUserCertificateProvider.Setup(x => x.RetrieveCertificateForUser(It.IsAny())).Returns(MockCert); + MockUserManagerLogin.Setup(x => x.UserCertificateProvider).Returns((IUserCertificateProvider)null); + } + else if(setAsNull == "setCert") + { + MockUserCertificateProvider.Setup(x => x.RetrieveCertificateForUser(It.IsAny())).Returns((X509Certificate2)null); + MockUserManagerLogin.Setup(x => x.UserCertificateProvider).Returns(MockUserCertificateProvider.Object); + } + else + { + MockUserCertificateProvider.Setup(x => x.RetrieveCertificateForUser(It.IsAny())).Returns(MockCert); + MockUserManagerLogin.Setup(x => x.UserCertificateProvider).Returns(MockUserCertificateProvider.Object); + } + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + MockBrowserContext.SetupGet(x => x.Pages).Returns(new List { MockPage.Object }); + + var userManager = new CertificateUserManagerModule(); + userManager.Page = MockPage.Object; + + var ex = await Assert.ThrowsAsync(async () => await userManager.LoginAsUserAsync("*", + MockBrowserState.Object, + MockTestState.Object, + MockSingleTestInstanceState.Object, + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); + + Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(certname)) + { + LoggingTestHelper.VerifyLogging(MockLogger, message, LogLevel.Error, Times.Once()); + } + } + + [Fact] + public async Task GetCertAuthGlob_ReturnsCorrectUrl() + { + // Arrange + var userManager = new CertificateUserManagerModule(); + string endpoint = "example.com"; + string expected = "https://*certauth.example.com/**"; + + // Act + string result = await userManager.GetCertAuthGlob(endpoint); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public async Task HandleRequest_PostMethod_Success() + { + // Arrange + var mockRequest = new Mock(); + mockRequest.Setup(r => r.Method).Returns("POST"); + mockRequest.Setup(r => r.Url).Returns("https://example.com"); + mockRequest.Setup(r => r.PostData).Returns("postData"); + mockRequest.Setup(r => r.Headers).Returns(new Dictionary()); + var mockRoute = new Mock(); + mockRoute.Setup(r => r.Request).Returns(mockRequest.Object); + + var handlerMock = new Mock(); + handlerMock.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + CertificateUserManagerModule.GetHttpClientHandler = () => handlerMock.Object; + CertificateUserManagerModule.GetHttpClient = handler => new HttpClient(handler); + var handler = new CertificateUserManagerModule(); + + // Mock the DoCertAuthPostAsync method + var httpResponse = new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent("response content") + }; + + // Act + await handler.HandleRequest(mockRoute.Object, MockCert, MockLogger.Object); + + // Assert + mockRoute.Verify(r => r.FulfillAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleRequest_PostMethod_Failure() + { + // Arrange + var mockRequest = new Mock(); + mockRequest.Setup(r => r.Method).Returns("POST"); + mockRequest.Setup(r => r.Url).Returns("https://example.com"); + mockRequest.Setup(r => r.PostData).Returns("postData"); + mockRequest.Setup(r => r.Headers).Returns(new Dictionary()); + var mockRoute = new Mock(); + mockRoute.Setup(r => r.Request).Returns(mockRequest.Object); + var loggerMock = new Mock(); + + var handlerMock = new Mock(); + handlerMock.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); + CertificateUserManagerModule.GetHttpClientHandler = () => handlerMock.Object; + CertificateUserManagerModule.GetHttpClient = handler => new HttpClient(handler); + + var handler = new CertificateUserManagerModule(); + + // Act + await handler.HandleRequest(mockRoute.Object, MockCert, loggerMock.Object); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), // It.IsAnyType is used to match any state + It.IsAny(), + It.Is>((v, t) => true)), // Function to format the log message + Times.AtLeastOnce); + mockRoute.Verify(r => r.AbortAsync("failed"), Times.Once); + } + + [Fact] + public async Task HandleRequest_NonPostMethod() + { + // Arrange + var mockRoute = new Mock(); + var mockRequest = new Mock(); + mockRequest.Setup(x => x.Method).Returns("GET"); + mockRoute.Setup(r => r.Request).Returns(mockRequest.Object); + + var handler = new CertificateUserManagerModule(); + + // Act + await handler.HandleRequest(mockRoute.Object, MockCert, MockLogger.Object); + + // Assert + mockRoute.Verify(r => r.ContinueAsync(null), Times.Once); + } + + [Fact] + public async Task DoCertAuthPostAsync_SuccessfulResponse_ReturnsResponse() + { + // Arrange + var request = new Mock(); + request.Setup(r => r.Url).Returns("https://example.com"); + request.Setup(r => r.PostData).Returns("postData"); + request.Setup(r => r.Headers).Returns(new Dictionary()); + + + var handlerMock = new Mock(); + handlerMock.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + CertificateUserManagerModule.GetHttpClientHandler = () => handlerMock.Object; + CertificateUserManagerModule.GetHttpClient = handler => new HttpClient(handler); + + var module = new CertificateUserManagerModule(); + + // Act + var response = await module.DoCertAuthPostAsync(request.Object, MockCert, MockLogger.Object); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task DoCertAuthPostAsync_UnsuccessfulResponse_ThrowsHttpRequestException() + { + // Arrange + var request = new Mock(); + request.Setup(r => r.Url).Returns("https://example.com"); + request.Setup(r => r.PostData).Returns("postData"); + request.Setup(r => r.Headers).Returns(new Dictionary()); + var loggerMock = new Mock(); + var handlerMock = new Mock(); + handlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); + + CertificateUserManagerModule.GetHttpClientHandler = () => handlerMock.Object; + CertificateUserManagerModule.GetHttpClient = handler => new HttpClient(handler); + + var module = new CertificateUserManagerModule(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await module.DoCertAuthPostAsync(request.Object, MockCert, loggerMock.Object)); + } + + [Fact] + public async Task DoCertAuthPostAsync_ExceptionThrown_LogsErrorAndThrows() + { + // Arrange + var request = new Mock(); + request.Setup(r => r.Url).Returns("https://example.com"); + request.Setup(r => r.PostData).Returns("postData"); + request.Setup(r => r.Headers).Returns(new Dictionary()); + + var handlerMock = new Mock(); + handlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException("Request failed")); + + CertificateUserManagerModule.GetHttpClientHandler = () => handlerMock.Object; + CertificateUserManagerModule.GetHttpClient = handler => new HttpClient(handler); + var loggerMock = new Mock(); + var module = new CertificateUserManagerModule(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await module.DoCertAuthPostAsync(request.Object, MockCert, loggerMock.Object)); + + // Verify logging + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), // It.IsAnyType is used to match any state + It.IsAny(), + It.Is>((v, t) => true)), // Function to format the log message + Times.Once); + } + } +} diff --git a/src/testengine.user.certificate.tests/Usings.cs b/src/testengine.user.certificate.tests/Usings.cs new file mode 100644 index 000000000..9df1d4217 --- /dev/null +++ b/src/testengine.user.certificate.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj b/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj new file mode 100644 index 000000000..d81d75d78 --- /dev/null +++ b/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj @@ -0,0 +1,38 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + \ No newline at end of file diff --git a/src/testengine.user.certificate/CertificateUserManagerModule.cs b/src/testengine.user.certificate/CertificateUserManagerModule.cs new file mode 100644 index 000000000..562f07eff --- /dev/null +++ b/src/testengine.user.certificate/CertificateUserManagerModule.cs @@ -0,0 +1,349 @@ +// Copyright(c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.ComponentModel.Composition; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerApps.TestEngine.Users; +using Microsoft.PowerFx; + +[assembly: InternalsVisibleTo("testengine.user.certificate.tests")] +namespace testengine.user.environment +{ + [Export(typeof(IUserManager))] + public class CertificateUserManagerModule : IUserManager + { + // defining these 2 for improved testability + internal static Func GetHttpClientHandler = () => new HttpClientHandler(); + internal static Func GetHttpClient = handler => new HttpClient(handler); + + public string Name { get { return "certificate"; } } + + public int Priority { get { return 50; } } + + public bool UseStaticContext { get { return false; } } + + public string Location { get; set; } = string.Empty; + + public IPage? Page { get; set; } + + private IBrowserContext? Context { get; set; } + + public static string EmailSelector = "input[type=\"email\"]"; + public static string SubmitButtonSelector = "input[type=\"submit\"]"; + public static string StaySignedInSelector = "[id=\"KmsiCheckboxField\"]"; + public static string KeepMeSignedInNoSelector = "[id=\"idBtn_Back\"]"; + + private TaskCompletionSource responseReceived = new TaskCompletionSource(); + + public async Task LoginAsUserAsync( + string desiredUrl, + IBrowserContext context, + ITestState testState, + ISingleTestInstanceState singleTestInstanceState, + IEnvironmentVariable environmentVariable, + IUserManagerLogin userManagerLogin) + { + Context = context; + + var testSuiteDefinition = singleTestInstanceState.GetTestSuiteDefinition(); + var logger = singleTestInstanceState.GetLogger(); + logger.LogDebug("Beginning certificate login authentication."); + if (testSuiteDefinition == null) + { + logger.LogError("Test definition cannot be null"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(testSuiteDefinition.Persona)) + { + logger.LogError("Persona cannot be empty"); + throw new InvalidOperationException(); + } + + var userConfig = testState.GetUserConfiguration(testSuiteDefinition.Persona); + + if (userConfig == null) + { + logger.LogError("Cannot find user config for persona"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(userConfig.EmailKey)) + { + logger.LogError("Email key for persona cannot be empty"); + throw new InvalidOperationException(); + } + + if (string.IsNullOrEmpty(userConfig.CertificateSubjectKey)) + { + logger.LogError("Certificate subject name key for persona cannot be empty"); + throw new InvalidOperationException(); + } + + var user = environmentVariable.GetVariable(userConfig.EmailKey); + var certName = environmentVariable.GetVariable(userConfig.CertificateSubjectKey); + bool missingUserOrCert = false; + if (string.IsNullOrEmpty(user)) + { + logger.LogError("User email cannot be null. Please check if the environment variable is set properly."); + missingUserOrCert = true; + } + if (string.IsNullOrEmpty(certName)) + { + logger.LogError("User certificate subject name cannot be null. Please check if the environment variable is set properly."); + missingUserOrCert = true; + } + if (missingUserOrCert) + { + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + + X509Certificate2 cert = null; + var userCertificateProvider = userManagerLogin.UserCertificateProvider; + if (userCertificateProvider != null) + { + cert = userCertificateProvider.RetrieveCertificateForUser(certName); + } + else + { + logger.LogError("Certificate provider cannot be null. Please ensure certificate provider for user."); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + + if (cert == null) + { + logger.LogError("Certificate cannot be null. Please ensure certificate for user."); + missingUserOrCert = true; + } + + if (missingUserOrCert) + { + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + + if (Page == null) + { + Page = context.Pages.First(); + } + + await HandleUserEmailScreen(EmailSelector, user); + + var endpoint = new Uri(Page.Url).Host; + var ep = string.IsNullOrEmpty(endpoint) ? "login.microsoftonline.com" : endpoint; + var interceptUri = await GetCertAuthGlob(ep); + + // start listener to intercept certificate auth call + await InterceptRestApiCallsAsync(Page, interceptUri, cert, logger); + + await Page.ClickAsync(SubmitButtonSelector); + + + // Handle pre-authentication dialogs + var workOrSchoolAccount = Page.GetByRole(AriaRole.Button, new() { Name = "Work or school account" }); + var useCertificateAuth = Page.GetByRole(AriaRole.Link, new() { Name = "Use a certificate or smart card" }).Or(Page.GetByRole(AriaRole.Button, new() { Name = "certificate" })); + await Task.WhenAny(workOrSchoolAccount.Or(useCertificateAuth).IsVisibleAsync(), responseReceived.Task); + + if (responseReceived.Task.IsCompletedSuccessfully) + { + await ClickStaySignedIn(desiredUrl, logger); + return; + } + + if (await workOrSchoolAccount.IsVisibleAsync()) + { + await workOrSchoolAccount.ClickAsync(); + } + await Task.WhenAny(useCertificateAuth.WaitForAsync(), responseReceived.Task); + + + if (responseReceived.Task.IsCompletedSuccessfully) + { + await ClickStaySignedIn(desiredUrl, logger); + return; + } + + if (await useCertificateAuth.IsVisibleAsync()) + { + await useCertificateAuth.ClickAsync(); + } + + // Wait for certificate authentication response + await responseReceived.Task; + await ClickStaySignedIn(desiredUrl, logger); + } + + internal async Task ClickStaySignedIn(string desiredUrl, ILogger logger) + { + PageWaitForSelectorOptions selectorOptions = new PageWaitForSelectorOptions(); + selectorOptions.Timeout = 8000; + + // For instances where there is a 'Stay signed in?' dialogue box + try + { + logger.LogDebug("Checking if asked to stay signed in."); + + // Check if we received a 'Stay signed in?' box? + await Page.WaitForSelectorAsync(StaySignedInSelector, selectorOptions); + logger.LogDebug("Was asked to 'stay signed in'."); + + // Click to stay signed in + await Page.ClickAsync(KeepMeSignedInNoSelector); + } + // If there is no 'Stay signed in?' box, an exception will throw; just catch and continue + catch (Exception ssiException) + { + logger.LogDebug("Exception encountered: " + ssiException.ToString()); + + // Keep record if certificateError was encountered + bool hasCertficateError = false; + + try + { + selectorOptions.Timeout = 2000; + + // Check if we received a password error + //await Page.WaitForSelectorAsync("[id=\"passwordError\"]", selectorOptions); + hasCertficateError = true; + } + catch (Exception peException) + { + logger.LogDebug("Exception encountered: " + peException.ToString()); + } + + // If encountered password error, exit program + if (hasCertficateError) + { + logger.LogError("Incorrect certificate entered. Make sure you are using the correct credentials."); + throw new UserInputException(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString()); + } + // If not, continue + else + { + logger.LogDebug("Did not encounter an invalid certificate error."); + } + + logger.LogDebug("Was not asked to 'stay signed in'."); + } + + await Page.WaitForURLAsync(desiredUrl); + } + + public async Task HandleUserEmailScreen(string selector, string value) + { + ValidatePage(); + await Page.Locator(selector).WaitForAsync(); + await Page.TypeAsync(selector, value, new PageTypeOptions { Delay = 50 }); + await Page.Keyboard.PressAsync("Tab", new KeyboardPressOptions { Delay = 20 }); + } + + internal async Task GetCertAuthGlob(string endpoint) + { + return $"https://*certauth.{endpoint}/**"; + } + public async Task InterceptRestApiCallsAsync(IPage page, string endpoint, X509Certificate2 cert, ILogger logger) + { + // Define the route to intercept + await page.RouteAsync(endpoint, async route => + { + await HandleRequest(route, cert, logger); + }); + } + + internal async Task HandleRequest(IRoute route, X509Certificate2 cert, ILogger logger) + { + var request = route.Request; + + Console.WriteLine($"Intercepted request: {request.Method} {request.Url}"); + if (request.Method == "POST") + { + try + { + var response = await DoCertAuthPostAsync(request, cert, logger); + + // Convert HttpResponseMessage to Playwright response + var headers = new Dictionary(); + foreach (var header in response.Headers) + { + headers[header.Key] = string.Join(",", header.Value); + } + + await route.FulfillAsync(new RouteFulfillOptions + { + ContentType = "text/html", + Status = (int)response.StatusCode, + Headers = headers, + Body = await response.Content.ReadAsStringAsync() + }); + responseReceived.SetResult(true); + } + catch (Exception ex) + { + logger.LogError($"Failed to handle request: {ex.Message}"); + await route.AbortAsync("failed"); + } + } + else + { + await route.ContinueAsync(); + } + } + + public async Task DoCertAuthPostAsync(IRequest request, X509Certificate2 cert, ILogger logger) + { + using (var handler = GetHttpClientHandler()) + { + handler.ClientCertificates.Add(cert); + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; + + using (var httpClient = GetHttpClient(handler)) + { + // Prepare the request + var httpRequest = new HttpRequestMessage(HttpMethod.Post, request.Url); + var content = new StringContent(request.PostData); + foreach (var header in request.Headers) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + httpRequest.Content = content; + + try + { + // Send the request + var response = await httpClient.SendAsync(httpRequest); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Cert auth request failed: {response.StatusCode} {response.ReasonPhrase}"); + } + + return response; + } + catch (Exception ex) + { + logger.LogError($"Failed to send cert auth request: {ex.Message}"); + throw; + } + } + } + } + + private void ValidatePage() + { + if (Page == null) + { + Page = Context.Pages.First(); + } + } + } +} diff --git a/src/testengine.user.certificate/testengine.user.certificate.csproj b/src/testengine.user.certificate/testengine.user.certificate.csproj new file mode 100644 index 000000000..598b30810 --- /dev/null +++ b/src/testengine.user.certificate/testengine.user.certificate.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs index d57621aa6..2ab5b207e 100644 --- a/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs +++ b/src/testengine.user.environment.tests/EnvironmentUserManagerModuleTests.cs @@ -24,6 +24,7 @@ public class EnvironmentUserManagerModuleTests private Mock MockPage; private Mock MockElementHandle; private Mock MockFileSystem; + private Mock MockUserManagerLogin; public EnvironmentUserManagerModuleTests() { @@ -53,6 +54,7 @@ public EnvironmentUserManagerModuleTests() MockPage = new Mock(MockBehavior.Strict); MockElementHandle = new Mock(MockBehavior.Strict); MockFileSystem = new Mock(MockBehavior.Strict); + MockUserManagerLogin = new Mock(MockBehavior.Strict); } [Fact] @@ -103,7 +105,8 @@ await userManager.LoginAsUserAsync("*", MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object); MockSingleTestInstanceState.Verify(x => x.GetTestSuiteDefinition(), Times.Once()); MockTestState.Verify(x => x.GetUserConfiguration(userConfiguration.PersonaName), Times.Once()); @@ -125,7 +128,8 @@ await Assert.ThrowsAsync(async () => await userManage MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object)); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); } [Theory] @@ -159,7 +163,8 @@ await Assert.ThrowsAsync(async () => await userManage MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object)); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); } [Fact] @@ -177,7 +182,8 @@ await Assert.ThrowsAsync(async () => await userManage MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object)); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); } [Theory] @@ -204,7 +210,8 @@ await Assert.ThrowsAsync(async () => await userManage MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object)); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); } [Theory] @@ -236,7 +243,8 @@ public async Task LoginUserAsyncThrowsOnInvalidEnviromentVariablesTest(string em MockBrowserState.Object, MockTestState.Object, MockSingleTestInstanceState.Object, - MockEnvironmentVariable.Object)); + MockEnvironmentVariable.Object, + MockUserManagerLogin.Object)); Assert.Equal(UserInputException.ErrorMapping.UserInputExceptionLoginCredential.ToString(), ex.Message); if (String.IsNullOrEmpty(email)) diff --git a/src/testengine.user.environment/EnvironmentUserManagerModule.cs b/src/testengine.user.environment/EnvironmentUserManagerModule.cs index 5e0906413..fb6e87ac7 100644 --- a/src/testengine.user.environment/EnvironmentUserManagerModule.cs +++ b/src/testengine.user.environment/EnvironmentUserManagerModule.cs @@ -42,7 +42,8 @@ public async Task LoginAsUserAsync( IBrowserContext context, ITestState testState, ISingleTestInstanceState singleTestInstanceState, - IEnvironmentVariable environmentVariable) + IEnvironmentVariable environmentVariable, + IUserManagerLogin userManagerLogin) { Context = context; From 0f639777e88ca7a4f3f2d43bc4636d152149f520 Mon Sep 17 00:00:00 2001 From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:59:12 -0700 Subject: [PATCH 04/40] Model Driven Application Provider (#359) * Initial read only version * Add docs * Docs and sample update * Adding entity record * MDA Provider updates #342 * Adding custom page support * Documentation update * WIP entitylist support * Foundations for CoE Kit test * Adding TestEngine.ConsentDialog * Adding TestEngine.ConsentDialog * Description update * removing format space and restoreing launchsettings * removing white space * Update sample application --------- Co-authored-by: Sourabh Namilikonda --- .../ModelDrivenApplicationProvider/README.md | 134 +++++ .../controls.md | 31 ++ samples/coe-kit-setup-wizard/README.md | 18 + samples/coe-kit-setup-wizard/testPlan.fx.yaml | 26 + samples/mda/MDASample_1_0_0_1.zip | Bin 0 -> 38685 bytes samples/mda/README.md | 29 + samples/mda/testPlan.fx.yaml | 26 + .../PlaywrightTestInfraFunctionTests.cs | 25 + .../PowerFx/PowerFxEngine.cs | 2 +- .../PowerFxModel/ControlRecordValue.cs | 6 + .../TestInfra/ITestInfraFunctions.cs | 12 + .../TestInfra/PlaywrightTestInfraFunctions.cs | 10 +- src/PowerAppsTestEngine.sln | 48 +- .../Properties/launchSettings.json | 2 +- .../ConsentDialogFunctionTests.cs | 151 +++++ .../ModelDrivenApplicationModuleTests.cs | 68 +++ src/testengine.module.mda.tests/Usings.cs | 1 + .../testengine.module.mda.tests.csproj | 36 ++ .../ConsentDialogFunction.cs | 127 +++++ .../ModelDrivenApplicationModule.cs | 36 ++ .../testengine.module.mda.csproj | 48 ++ .../ModelDrivenApplicationMock.js | 270 +++++++++ ...lDrivenApplicationProvideEntityListTest.cs | 217 ++++++++ ...odelDrivenApplicationProviderCommonTest.cs | 50 ++ ...DrivenApplicationProviderCustomPageTest.cs | 293 ++++++++++ .../ModelDrivenApplicationProviderTest.cs | 515 +++++++++++++++++ src/testengine.provider.mda.tests/Usings.cs | 1 + .../testengine.provider.mda.tests.csproj | 46 ++ .../ModelDrivenApplicationProvider.cs | 524 ++++++++++++++++++ .../PowerAppsTestEngineMDA.js | 475 ++++++++++++++++ .../testengine.provider.mda.csproj | 34 ++ 31 files changed, 3257 insertions(+), 4 deletions(-) create mode 100644 docs/Extensions/ModelDrivenApplicationProvider/README.md create mode 100644 docs/Extensions/ModelDrivenApplicationProvider/controls.md create mode 100644 samples/coe-kit-setup-wizard/README.md create mode 100644 samples/coe-kit-setup-wizard/testPlan.fx.yaml create mode 100644 samples/mda/MDASample_1_0_0_1.zip create mode 100644 samples/mda/README.md create mode 100644 samples/mda/testPlan.fx.yaml create mode 100644 src/testengine.module.mda.tests/ConsentDialogFunctionTests.cs create mode 100644 src/testengine.module.mda.tests/ModelDrivenApplicationModuleTests.cs create mode 100644 src/testengine.module.mda.tests/Usings.cs create mode 100644 src/testengine.module.mda.tests/testengine.module.mda.tests.csproj create mode 100644 src/testengine.module.mda/ConsentDialogFunction.cs create mode 100644 src/testengine.module.mda/ModelDrivenApplicationModule.cs create mode 100644 src/testengine.module.mda/testengine.module.mda.csproj create mode 100644 src/testengine.provider.mda.tests/ModelDrivenApplicationMock.js create mode 100644 src/testengine.provider.mda.tests/ModelDrivenApplicationProvideEntityListTest.cs create mode 100644 src/testengine.provider.mda.tests/ModelDrivenApplicationProviderCommonTest.cs create mode 100644 src/testengine.provider.mda.tests/ModelDrivenApplicationProviderCustomPageTest.cs create mode 100644 src/testengine.provider.mda.tests/ModelDrivenApplicationProviderTest.cs create mode 100644 src/testengine.provider.mda.tests/Usings.cs create mode 100644 src/testengine.provider.mda.tests/testengine.provider.mda.tests.csproj create mode 100644 src/testengine.provider.mda/ModelDrivenApplicationProvider.cs create mode 100644 src/testengine.provider.mda/PowerAppsTestEngineMDA.js create mode 100644 src/testengine.provider.mda/testengine.provider.mda.csproj diff --git a/docs/Extensions/ModelDrivenApplicationProvider/README.md b/docs/Extensions/ModelDrivenApplicationProvider/README.md new file mode 100644 index 000000000..9bd25afbf --- /dev/null +++ b/docs/Extensions/ModelDrivenApplicationProvider/README.md @@ -0,0 +1,134 @@ +# Overview of the Model Driven Application Web Test Provider for the Power Apps Test Engine + +## Purpose + +The Model Driven Application Provider implements a [Managed Extensibility Framework](https://learn.microsoft.comdotnet/framework/mef/) (MEF) extension enhances the Power Apps Test Engine by enabling comprehensive unit and integration testing for web-based Model Driven Applications (MDA). + +The provider encapsulates the XRM SDK using Playwright by allowing tests to be created with low code Power Fx commands for test for automation, ensuring a seamless and efficient testing process. + +This extension build on the Test Engine Yaml definitions to define and execute test suites and cases, generating test results as TRX files, which can be integrated into CI/CD processes. + +## Testing Model-Driven Apps with Power Apps Test Engine + +When testing model-driven apps with the Power Apps Test Engine, you have a spectrum of choices ranging from unit tests with mocked data operations to full integration tests against a Dataverse environment. It's important to consider several key factors to ensure robust and efficient testing. + +### Key Points to Consider: + +- **Speed of Execution** + - **Unit Tests with Mock Data**: Typically faster as they do not interact with the actual Dataverse environment. + - **Integration Tests with Dataverse**: Slower due to real-time interaction with the Dataverse environment, but provide more comprehensive validation. + +- **Management of Test State as Dataverse Records vs Mock Data** + - **Mock Data**: Easier to control and reset between tests, ensuring consistency and reliability of test results. + - **Dataverse Records**: Requires careful management to avoid data pollution and ensure the environment is in a known state before each test. + +- **Setup and Configuration of Dataverse Environment** + - **Test Environment Setup**: Establishing a dedicated test environment that mirrors your production setup is crucial for realistic integration testing. + - **Data Seeding**: Pre-loading necessary test data into the Dataverse environment can save time during test execution. + +- **Testing Multiple Personas and Security Roles** + - **Security Roles**: Ensure that different security roles are accurately represented in your tests to validate role-based access and permissions. + - **User Personas**: Simulate different user personas to test how varying roles and permissions affect the application's behavior. + +By carefully considering these factors, you can create a balanced and effective testing strategy that leverages the strengths of both unit tests and integration tests for your model-driven apps. + +## Key Features + +1. **Unit and Integration Testing**: + - The extension supports comprehensive unit and integration testing to ensure that both individual components and the interactions between different modules within Model Driven Applications operate correctly. + - Unit testing verifies the proper functioning of individual application elements. + - The ability to mock data connections to isolate the behavior of the application + - Integration testing ensures that the overall application perform as expected when connected to dependent components + +2. **Low Code Extensibility with Power Fx**: + - The provider encapsulates the XRM SDK within low code Power Fx commands, enabling users to efficiently interact with the Model Driven Application’s data and logic. + - This approach simplifies test scripting, making it accessible to users with limited coding experience. + - Power Fx commands allow testers and developers to write and execute test cases without requiring deep expertise in JavaScript. + - By working with the JavaScript object model tests are abstracted from changes in the Document Object Model (DOM) used to represent the application + +3. **Playwright for Web Automation**: + - The extension leverages Playwright to automate testing for web-based Model Driven Applications. + - Playwright enables reliable and consistent automation of the application’s user interface, verifying that UI components behave as expected under various conditions. + - Provide for multi browser testing + - ALlow for recorded videos and headless testing + +4. **YAML-Based Test Definition and Execution**: + - The extension uses YAML definitions to structure and manage test suites and test cases. + - This supports clear and concise test configurations, facilitating easier maintenance and updates. + - Tests can be executed automatically, streamlining the testing process within development pipelines. + +5. **Generation of TRX Files for CI/CD Integration**: + - After test execution, the results are generated as TRX files. + - These TRX files can be integrated into CI/CD pipelines, providing feedback on the quality and reliability of the application. + - This integration helps maintain high standards and continuous quality assurance throughout the development lifecycle. + +6. **Seamless Integration with Power Apps**: + - The MEF extension integrates smoothly with the existing Power Apps Test Engine, ensuring a cohesive testing environment. + - It supports a consistent testing framework across all stages of application development, from initial development to pre-deployment validation. + +7. **Enhanced Developer and Tester Productivity**: + - The low code approach with Power Fx commands and the use of Playwright for automation, combined with YAML-based definitions, enhances productivity. + - Developers and testers can rapidly create and execute test cases, locate issues, and iterate on solutions efficiently. + +## Benefits + +- **High-Quality Applications**: Ensures comprehensive testing of web-based Model Driven Applications, leading to reliable and robust deployments. +- **Efficient Development and Testing**: Reduces the time and effort needed for testing, allowing teams to focus on innovation and development. +- **Accessibility**: Low code Power Fx commands make testing accessible to a broader range of users, including non-developers. +- **Consistency**: Provides a standardized testing framework, ensuring predictable and manageable processes. +- **CI/CD Integration**: Enhances continuous integration and continuous deployment processes by incorporating test results seamlessly, ensuring ongoing quality assurance. + +## Roadmap + +1. **Generative AI for Test Case Creation**: + - Future enhancements could include the ability to use generative AI to automate the process of converting natural language descriptions into test cases and Power Fx test steps. + - This will significantly accelerate the creation and maintenance of tests by enabling users to describe testing scenarios in plain language. + - The AI will interpret these descriptions and automatically generate the corresponding test cases and steps, reducing the manual effort involved and increasing the accuracy and consistency of test scripts. + +2. **Enhanced Natural Language Processing (NLP)**: + - Improved NLP capabilities will allow for more sophisticated understanding and interpretation of natural language inputs. + - This will enable more complex and nuanced test scenarios to be accurately translated into executable test steps. + - This could include synthetic test data generation. + +## Examples + +[Basic MDA - Assert Form Properties](../../../samples/mda/README.md) and associated [testPlan.fx.yaml](../../../samples/mda/testPlan.fx.yaml) + +## Capabilities + +The following table outlines the scope of testing Model Driven Applications and current support for features in the provider + +| Capability | Description | Supported | +|--------------------------|-------------------------------------------------------------------------------------------------|-----------| +| Command Bars and Actions | Ability to interact with command bars to automate and validate command bar actions and custom buttons.| +| Forms (Get) | Ability to read all [controls](./controls.md) as Power Fx variables. | Y +| Forms (Set) | Ability to set Power Fx variables which change form data. | +| Navigation | Provides methods for navigating forms and items in Model Driven Apps. | +| Panels | Provides a method to display a web page in the side pane of Model Driven Apps forms. | +| Views (Get) | Ability to query grids +| Views (Actions) | Ability to select and take actions on view items. | +| Security and Roles | Provides methods to manage and test role-based security within the application. | +| Custom Pages | The ability to test custom pages | +| Web API | Supports operations for both Online and Offline Web API. | +| Data Operations | Allows CRUD (Create, Read, Update, Delete) operations on records within Model Driven Apps. | +| Mock Data Operations | Provide the ability to provide mock values for CRUD (Create, Read, Update, Delete) operations on records within Model Driven Apps. | +| Workflow Execution | Enables triggering workflows and monitoring their progress. | +| Business Logic | Ability to trigger and test custom business logic and validations. | +| Notifications | Capability to display and manage system and user notifications within the application. | +| Entities and Attributes | Powers querying and manipulation of entity attributes and relationships. | +| User Context | Methods to retrieve and utilize user context information within tests. | +| Global Variables | Supports the use of global variables for maintaining state and shared data across tests. | +| Audit and Logs | Ability to access and interact with audit logs and system logs for compliance and debugging. | +| Solutions | Allows for importing, exporting, and managing solutions within the application. | +| Localization | Ability to specify locale for localization of navigation, commands and labels | + +## Page Types + +Model driven applications provide a range of different page types + +| Page Type | Description | Supported | +|--------------------------|-------------------------------------------------------------------------------------------------|-----------| +| Custom | [Overview of custom pages for model-driven apps](https://learn.microsoft.com/power-apps/maker/model-driven-apps/model-app-page-overview)|N +| Dashboards | [Track your progress with dashboards and charts](https://learn.microsoft.com/power-apps/user/track-your-progress-with-dashboard-and-charts) | N +| Forms | [Create and design model-driven app forms](https://learn.microsoft.com/power-apps/maker/model-driven-apps/create-design-forms) | Y +| Views | [Understand model-driven app views](https://learn.microsoft.com/power-apps/maker/model-driven-apps/create-edit-views) | N diff --git a/docs/Extensions/ModelDrivenApplicationProvider/controls.md b/docs/Extensions/ModelDrivenApplicationProvider/controls.md new file mode 100644 index 000000000..749c32791 --- /dev/null +++ b/docs/Extensions/ModelDrivenApplicationProvider/controls.md @@ -0,0 +1,31 @@ +# Overview + +## Common Properties + +The following table outlines common properties that could apply and the current state of support for the property in the provider + +| Property | Description | Supported | +|----------|-------------|-----------| +| Disabled | Get or set whether the control is disabled | Get only | +| Label | The label for the control | Get only | +| Name | The name for the control | Get only | +| Value | The value for the control | Get only | +| Visible | If the control is visible or not | Get only | + +## Grid Controls + +Used for displaying and interacting with data in views and subgrids. Includes editable grids. + +Items to consider: +- Get rows, rows and cells +- Selected rows +- Total row count +- Refresh table + +## Display Controls + +Components like calendars, star ratings, AI Builder business card reader, and Power BI reports. + +## Input Controls + +Includes checkboxes, number inputs, toggles, and more. diff --git a/samples/coe-kit-setup-wizard/README.md b/samples/coe-kit-setup-wizard/README.md new file mode 100644 index 000000000..e265fe094 --- /dev/null +++ b/samples/coe-kit-setup-wizard/README.md @@ -0,0 +1,18 @@ +# Overview + +The Power Platform Center of Excellence (CoE) starter kit is made up of a number of Power Platform low code solution elements. Amoung these is a model driven application that can be used to setup and upgrade the CoE Starter Kit. + +This sample includes Power Apps Test Engine tests that can be used to automate and test ket elemenets of the expected behaviour of the Setup and Upgrade Wizard + +## Getting Started + +To get started ensure that you have followed the [Build locally](../../README.md) to have a working version of the Power Apps Test Engine available + +## Usage + +You can execute this sample using the following commands + +```bash +cd bin/Debug/PowerAppsTestEngine +dotnet PowerAppsTestEngine.dll -i ../../../samples/coe-kit-setup-wizard/testPlan.fx.yaml -u browser -p mda -d https://contoso.crm.dynamics.com/main.aspx?appid=06f88e88-163e-ef11-840a-0022481fcc8d&pagetype=custom&name=admin_initialsetuppage_d45cf +``` diff --git a/samples/coe-kit-setup-wizard/testPlan.fx.yaml b/samples/coe-kit-setup-wizard/testPlan.fx.yaml new file mode 100644 index 000000000..18802b258 --- /dev/null +++ b/samples/coe-kit-setup-wizard/testPlan.fx.yaml @@ -0,0 +1,26 @@ +testSuite: + testSuiteName: CoE Starter Kit Setup Wizard + testSuiteDescription: Verify custom page of CoE Starter Kit Setup Wizard and step through install + persona: User1 + appLogicalName: NotNeeded + + testCases: + - testCaseName: Verify + testCaseDescription: Verify setup and upgrade Wizard of the CoE Starter Kit + testSteps: | + = TestEngine.ConsentDialog(Table({Text: "Center of Excellence Setup Wizard"})); + +testSettings: + headless: false + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: NotNeeded + passwordKey: NotNeeded diff --git a/samples/mda/MDASample_1_0_0_1.zip b/samples/mda/MDASample_1_0_0_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..bcd8ce6e9995683a142bffe5293a178284097ef3 GIT binary patch literal 38685 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

l!$wQ>E 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 literal 0 HcmV?d00001 diff --git a/samples/mda/README.md b/samples/mda/README.md new file mode 100644 index 000000000..9170ce8c2 --- /dev/null +++ b/samples/mda/README.md @@ -0,0 +1,29 @@ +# Overview + +This Power Apps Test Engine sample demonstrates how to assert the values of controls in a model driven application form. + +## Usage + +1. Build the Test Engine solution + +2. Get the URL of model driven application form + +3. Modify the [testPlan.fx.yaml](./testPlan.fx.yaml) to assert expected values of the form controls. + + > [!NOTE] The controls are referenced using the [logical name](https://learn.microsoft.com/power-apps/developer/data-platform/entity-metadata#table-names). + +4. Update the domain url for your model driven application + +| Url Part | Description | +|----------|-------------| +| appid=a1234567-cccc-44444-9999-a123456789123 | The unique identifier of your model driven application | +| etn=test | The name of the entity being validated | +| id=26bafa27-ca7d-ee11-8179-0022482a91f4 | The unique identifier the record being edited | +| pagetype=entityrecord | The type of page to open. + +5. Execute the test for custom page changing the example below to the url of your organization + +```pwsh +cd bin\Debug\PowerAppsEngine +dotnet PowerAppsTestEngine.dll -i ..\..\..\samples\mda\testPlan.fx.yaml -e 00000000-0000-0000-0000-11112223333 -t 11112222-3333-4444-5555-666677778888 -u browser -p mda -d "https://contoso.crm4.dynamics.com/main.aspx?appid=9e9c25f3-1851-ef11-bfe2-6045bd8f802c&pagetype=custom&name=sample_custom_cf8e6" +``` diff --git a/samples/mda/testPlan.fx.yaml b/samples/mda/testPlan.fx.yaml new file mode 100644 index 000000000..17900f71e --- /dev/null +++ b/samples/mda/testPlan.fx.yaml @@ -0,0 +1,26 @@ +testSuite: + testSuiteName: MDA Custom Page tests + testSuiteDescription: Verify model driven application + persona: User1 + appLogicalName: NotNeeded + + testCases: + - testCaseName: Open Page + testCaseDescription: Check initial state + testSteps: | + = Assert(Total.Text, "0"); + +testSettings: + headless: false + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: NotNeeded + passwordKey: NotNeeded diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs index a6c5f5a4f..b8f888abf 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/TestInfra/PlaywrightTestInfraFunctionTests.cs @@ -767,5 +767,30 @@ public async Task RouteNetworkRequestTest() await playwrightTestInfraFunctions.RouteNetworkRequest(MockRoute.Object, mock); MockRoute.Verify(x => x.ContinueAsync(It.IsAny()), Times.Once); } + + [Fact] + public async Task LoadScriptContent() + { + // Arrange + var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(MockTestState.Object, MockSingleTestInstanceState.Object, + MockFileSystem.Object, browserContext: MockBrowserContext.Object); + + playwrightTestInfraFunctions.Page = MockPage.Object; + + PageAddScriptTagOptions tagOptions = null; + + MockPage.Setup(m => m.AddScriptTagAsync(It.IsAny())) + .Callback((PageAddScriptTagOptions options) => tagOptions = options) + .Returns(Task.FromResult(MockElementHandle.Object)); + + var javaScript = "var test = 1"; + + // Act + + await playwrightTestInfraFunctions.AddScriptContentAsync(javaScript); + + // Assert + Assert.Equal(javaScript, tagOptions.Content); + } } } diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs index d40db895b..f739eb8f2 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs @@ -68,7 +68,7 @@ public void Setup() { if (TestState.GetTestEngineModules().Count == 0) { - Logger.LogError("Extension enabled not none loaded"); + Logger.LogError("Extension enabled, none loaded"); } foreach (var module in TestState.GetTestEngineModules()) { diff --git a/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs index dc7477475..90dfd2e82 100644 --- a/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs +++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/ControlRecordValue.cs @@ -110,6 +110,12 @@ protected override bool TryGetField(FormulaType fieldType, string fieldName, out if (jsPropertyValueModel != null) { + if (string.IsNullOrEmpty(jsPropertyValueModel.PropertyValue)) + { + result = null; + return false; + } + if (fieldType is NumberType) { result = NumberValue.New(double.Parse(jsPropertyValueModel.PropertyValue)); diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs index daa0d48a6..5f0c92b67 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs @@ -13,6 +13,11 @@ namespace Microsoft.PowerApps.TestEngine.TestInfra /// public interface ITestInfraFunctions { + ///

+ /// The current page to execute actions + /// + public IPage Page { get; set; } + /// /// Return the current browser context /// @@ -80,6 +85,13 @@ public interface ITestInfraFunctions /// Task public Task AddScriptTagAsync(string scriptTag, string frameName); + /// + /// Adds a script content to page + /// + /// The script to add + /// Task + public Task AddScriptContentAsync(string content); + /// /// Runs javascript on the page /// diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs index 3714c0670..7e6afea27 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Runtime; using Microsoft.Extensions.Logging; using Microsoft.Playwright; @@ -26,7 +27,7 @@ public class PlaywrightTestInfraFunctions : ITestInfraFunctions private IPlaywright PlaywrightObject { get; set; } private IBrowser Browser { get; set; } private IBrowserContext BrowserContext { get; set; } - private IPage Page { get; set; } + public IPage Page { get; set; } public PlaywrightTestInfraFunctions(ITestState testState, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, ITestWebProvider testWebProvider) { _testState = testState; @@ -372,5 +373,12 @@ public async Task RunJavascriptAsync(string jsExpression) return await Page.EvaluateAsync(jsExpression); } + + public async Task AddScriptContentAsync(string content) + { + ValidatePage(); + + await Page.AddScriptTagAsync(new PageAddScriptTagOptions { Content = content }); + } } } diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index 6c5f08077..ff85e1882 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -58,7 +58,32 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.localcertif EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.certificatestore", "testengine.auth.certificatestore\testengine.auth.certificatestore.csproj", "{EF3A270A-53A4-4C08-B45B-7C6993593446}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "testengine.auth.certificatestore.tests", "testengine.auth.certificatestore.tests\testengine.auth.certificatestore.tests.csproj", "{36F79923-74AD-424E-8A74-6902628FBF58}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.auth.certificatestore.tests", "testengine.auth.certificatestore.tests\testengine.auth.certificatestore.tests.csproj", "{36F79923-74AD-424E-8A74-6902628FBF58}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items (2)", "Solution Items (2)", "{910250A5-D84E-4A3F-A5C6-C0A7AE27C8BD}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{4E409805-A766-4F65-B058-C5264BA1F1CE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{1009A533-2243-4EBF-BA85-8A8CC02F3FB9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{65FAC4EC-553A-4293-8484-2427A76BB4A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.provider.mda", "testengine.provider.mda\testengine.provider.mda.csproj", "{25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.provider.mda.tests", "testengine.provider.mda.tests\testengine.provider.mda.tests.csproj", "{B7C8ED63-A77B-4FA0-B441-A07BE40E3264}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ModelDrivenApplicationProvider", "ModelDrivenApplicationProvider", "{A465E028-ED7F-4113-B37B-4B830A5512D5}" + ProjectSection(SolutionItems) = preProject + ..\docs\Extensions\ModelDrivenApplicationProvider\controls.md = ..\docs\Extensions\ModelDrivenApplicationProvider\controls.md + ..\docs\Extensions\ModelDrivenApplicationProvider\README.md = ..\docs\Extensions\ModelDrivenApplicationProvider\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.mda", "testengine.module.mda\testengine.module.mda.csproj", "{16C931C8-47F8-4ABA-A657-3349DC8BEBF3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.mda.tests", "testengine.module.mda.tests\testengine.module.mda.tests.csproj", "{1BEF602B-9010-4455-811A-7597B700AC4C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -138,6 +163,22 @@ Global {36F79923-74AD-424E-8A74-6902628FBF58}.Debug|Any CPU.Build.0 = Debug|Any CPU {36F79923-74AD-424E-8A74-6902628FBF58}.Release|Any CPU.ActiveCfg = Release|Any CPU {36F79923-74AD-424E-8A74-6902628FBF58}.Release|Any CPU.Build.0 = Release|Any CPU + {25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94}.Release|Any CPU.Build.0 = Release|Any CPU + {B7C8ED63-A77B-4FA0-B441-A07BE40E3264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7C8ED63-A77B-4FA0-B441-A07BE40E3264}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7C8ED63-A77B-4FA0-B441-A07BE40E3264}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7C8ED63-A77B-4FA0-B441-A07BE40E3264}.Release|Any CPU.Build.0 = Release|Any CPU + {16C931C8-47F8-4ABA-A657-3349DC8BEBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16C931C8-47F8-4ABA-A657-3349DC8BEBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16C931C8-47F8-4ABA-A657-3349DC8BEBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16C931C8-47F8-4ABA-A657-3349DC8BEBF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1BEF602B-9010-4455-811A-7597B700AC4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BEF602B-9010-4455-811A-7597B700AC4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BEF602B-9010-4455-811A-7597B700AC4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BEF602B-9010-4455-811A-7597B700AC4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -162,6 +203,11 @@ Global {7183776B-21BB-4318-958B-62B325CC1A7D} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} {EF3A270A-53A4-4C08-B45B-7C6993593446} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} {36F79923-74AD-424E-8A74-6902628FBF58} = {F5DD02A2-1BA8-481C-A7ED-E36577C2CB15} + {25CEB2CE-B6D5-4BE1-ACD7-3CF00441EF94} = {D53FFBF2-F4D0-4139-9FD3-47C8216E4448} + {B7C8ED63-A77B-4FA0-B441-A07BE40E3264} = {D53FFBF2-F4D0-4139-9FD3-47C8216E4448} + {A465E028-ED7F-4113-B37B-4B830A5512D5} = {D34E437A-6149-46EC-B7DA-FF449E55CEEA} + {16C931C8-47F8-4ABA-A657-3349DC8BEBF3} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {1BEF602B-9010-4455-811A-7597B700AC4C} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7E7B2C01-DDE2-4C5A-96C3-AF474B074331} diff --git a/src/PowerAppsTestEngine/Properties/launchSettings.json b/src/PowerAppsTestEngine/Properties/launchSettings.json index 1cf8eaef6..7cef704a9 100644 --- a/src/PowerAppsTestEngine/Properties/launchSettings.json +++ b/src/PowerAppsTestEngine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "PowerAppsTestEngine": { "commandName": "Project", - "commandLineArgs": "-i ..\\..\\..\\samples\\pause\\testPlan.fx.yaml -u browser" + "commandLineArgs": "-i ..\\..\\..\\samples\\mda\\testPlan.fx.yaml -e 00000000-0000-0000-0000-11112223333 -t 11112222-3333-4444-5555-666677778888 -u browser -p mda -d \"https://contoso.crm4.dynamics.com/main.aspx?appid=9e9c25f3-1851-ef11-bfe2-6045bd8f802c&pagetype=custom&name=sample_custom_cf8e6\"" } } } \ No newline at end of file diff --git a/src/testengine.module.mda.tests/ConsentDialogFunctionTests.cs b/src/testengine.module.mda.tests/ConsentDialogFunctionTests.cs new file mode 100644 index 000000000..bf2def700 --- /dev/null +++ b/src/testengine.module.mda.tests/ConsentDialogFunctionTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Moq; + +namespace testengine.module +{ + public class ConsentDialogFunctionTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + private Mock MockBrowserContext; + private Mock