1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ using System ;
5
+ using System . Collections . Generic ;
6
+ using System . IO ;
7
+ using System . Linq ;
8
+ using System . Net ;
9
+ using System . Runtime . Versioning ;
10
+ using System . Text ;
11
+ using System . Threading . Tasks ;
12
+ using Common ;
13
+ using Microsoft . Identity . Lab . Api ;
14
+ using Microsoft . Playwright ;
15
+ using Xunit ;
16
+ using Xunit . Abstractions ;
17
+ using static System . Net . Mime . MediaTypeNames ;
18
+ using Process = System . Diagnostics . Process ;
19
+ using TC = Common . TestConstants ;
20
+
21
+ namespace HybridFlowUiTest
22
+ {
23
+ public class HybridFlowTest : IClassFixture < InstallPlaywrightBrowserFixture >
24
+ {
25
+ private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut" ;
26
+ private const uint ClientPort = 44321 ;
27
+ private const string TraceFileClassName = "OpenIDConnect-HybridFlow" ;
28
+ private const uint NumProcessRetries = 3 ;
29
+ private const string SampleSlnFileName = "2-5-HybridFlow.sln" ;
30
+ private const string SampleExeFileName = "\\ 2-5-HybridFlow.exe" ;
31
+ private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new ( ) { Timeout = 25000 } ;
32
+ private readonly string _sampleAppPath = "2-WebApp-graph-user" + Path . DirectorySeparatorChar + "2-5-HybridFlow" + Path . DirectorySeparatorChar . ToString ( ) ;
33
+ private readonly string _testAppsettingsPath = "UiTests" + Path . DirectorySeparatorChar + "HybridFlowUiTest" + Path . DirectorySeparatorChar . ToString ( ) + TC . AppSetttingsDotJson ;
34
+ private readonly string _testAssemblyLocation = typeof ( HybridFlowTest ) . Assembly . Location ;
35
+ private readonly ITestOutputHelper _output ;
36
+
37
+ public HybridFlowTest ( ITestOutputHelper output )
38
+ {
39
+ _output = output ;
40
+ }
41
+
42
+ [ Fact ]
43
+ [ SupportedOSPlatform ( "windows" ) ]
44
+ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogout ( )
45
+ {
46
+ // Setup web app and api environmental variables.
47
+ var clientEnvVars = new Dictionary < string , string >
48
+ {
49
+ { "ASPNETCORE_ENVIRONMENT" , "Development" } ,
50
+ { TC . KestrelEndpointEnvVar , TC . HttpsStarColon + ClientPort }
51
+ } ;
52
+
53
+ Dictionary < string , Process > ? processes = null ;
54
+
55
+ // Arrange Playwright setup, to see the browser UI set Headless = false.
56
+ const string TraceFileName = TraceFileClassName + "_LoginLogout" ;
57
+ using IPlaywright playwright = await Playwright . CreateAsync ( ) ;
58
+ IBrowser browser = await playwright . Chromium . LaunchAsync ( new ( ) { Headless = false } ) ;
59
+ IBrowserContext context = await browser . NewContextAsync ( new BrowserNewContextOptions { IgnoreHTTPSErrors = true } ) ;
60
+ await context . Tracing . StartAsync ( new ( ) { Screenshots = true , Snapshots = true , Sources = true } ) ;
61
+ IPage page = await context . NewPageAsync ( ) ;
62
+ string uriWithPort = TC . LocalhostUrl + ClientPort ;
63
+
64
+ try
65
+ {
66
+ // Build the sample app with correct appsettings file.
67
+ UiTestHelpers . BuildSampleUsingTestAppsettings ( _testAssemblyLocation , _sampleAppPath , _testAppsettingsPath , SampleSlnFileName ) ;
68
+
69
+ // Start the web app and api processes.
70
+ // The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding
71
+ var clientProcessOptions = new ProcessStartOptions ( _testAssemblyLocation , _sampleAppPath , SampleExeFileName , clientEnvVars ) ;
72
+
73
+ bool areProcessesRunning = UiTestHelpers . StartAndVerifyProcessesAreRunning ( [ clientProcessOptions ] , out processes , NumProcessRetries ) ;
74
+
75
+ if ( ! areProcessesRunning )
76
+ {
77
+ _output . WriteLine ( $ "Process not started after { NumProcessRetries } attempts.") ;
78
+ StringBuilder runningProcesses = new StringBuilder ( ) ;
79
+ foreach ( var process in processes )
80
+ {
81
+ #pragma warning disable CA1305 // Specify IFormatProvider
82
+ runningProcesses . AppendLine ( $ "Is { process . Key } running: { UiTestHelpers . ProcessIsAlive ( process . Value ) } ") ;
83
+ #pragma warning restore CA1305 // Specify IFormatProvider
84
+ }
85
+ Assert . Fail ( TC . WebAppCrashedString + " " + runningProcesses . ToString ( ) ) ;
86
+ }
87
+
88
+ LabResponse labResponse = await LabUserHelper . GetSpecificUserAsync ( TC . MsidLab4User ) ;
89
+
90
+ // Initial sign in
91
+ _output . WriteLine ( "Starting web app sign-in flow." ) ;
92
+ string email = labResponse . User . Upn ;
93
+ await UiTestHelpers . NavigateToWebApp ( uriWithPort , page ) ;
94
+ await page . GetByRole ( AriaRole . Link , new ( ) { Name = "Sign in" } ) . ClickAsync ( ) ;
95
+ await UiTestHelpers . FirstLogin_MicrosoftIdFlow_ValidEmailPassword ( page , email , labResponse . User . GetOrFetchPassword ( ) , _output ) ;
96
+ await Assertions . Expect ( page . GetByText ( "SPA Authorization Code" ) ) . ToBeVisibleAsync ( _assertVisibleOptions ) ;
97
+ await Assertions . Expect ( page . GetByText ( email ) ) . ToBeVisibleAsync ( _assertVisibleOptions ) ;
98
+ _output . WriteLine ( "Web app sign-in flow successful." ) ;
99
+
100
+ // Sign out
101
+ _output . WriteLine ( "Starting web app sign-out flow." ) ;
102
+ await page . GetByRole ( AriaRole . Link , new ( ) { Name = "Sign out" } ) . ClickAsync ( ) ;
103
+ await UiTestHelpers . PerformSignOut_MicrosoftIdFlow ( page , email , TC . LocalhostUrl + ClientPort + SignOutPageUriPath , _output ) ;
104
+ _output . WriteLine ( "Web app sign out successful." ) ;
105
+ }
106
+ catch ( Exception ex )
107
+ {
108
+ // Adding guid in case of multiple test runs. This will allow screenshots to be matched to their appropriate test runs.
109
+ var guid = Guid . NewGuid ( ) . ToString ( ) ;
110
+ try
111
+ {
112
+ if ( page != null )
113
+ {
114
+ await page . ScreenshotAsync ( new PageScreenshotOptions ( ) { Path = $ "ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{ guid } .png", FullPage = true } ) ;
115
+ }
116
+ }
117
+ catch
118
+ {
119
+ _output . WriteLine ( "No Screenshot." ) ;
120
+ }
121
+
122
+ string runningProcesses = UiTestHelpers . GetRunningProcessAsString ( processes ) ;
123
+ Assert . Fail ( $ "the UI automation failed: { ex } output: { ex . Message } .\n { runningProcesses } \n Test run: { guid } ") ;
124
+ }
125
+ finally
126
+ {
127
+ // Make sure all processes and their children are stopped.
128
+ UiTestHelpers . EndProcesses ( processes ) ;
129
+
130
+ // Stop tracing and export it into a zip archive.
131
+ string path = UiTestHelpers . GetTracePath ( _testAssemblyLocation , TraceFileName ) ;
132
+ await context . Tracing . StopAsync ( new ( ) { Path = path } ) ;
133
+ _output . WriteLine ( $ "Trace data for { TraceFileName } recorded to { path } .") ;
134
+
135
+ // Close the browser and stop Playwright.
136
+ await browser . CloseAsync ( ) ;
137
+ playwright . Dispose ( ) ;
138
+ }
139
+ }
140
+ }
141
+ }
0 commit comments