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