From c5f59e06c361539b65791556691271688457f592 Mon Sep 17 00:00:00 2001 From: jhkimnew Date: Tue, 12 Sep 2017 11:39:55 -0700 Subject: [PATCH] Added new test scenarios for websocket (#153) --- AspNetCoreModule.sln | 17 +- .../AspNetCoreModule.Test.csproj | 14 +- .../Framework/IISConfigUtility.cs | 18 + .../Framework/InitializeTestMachine.cs | 2 +- .../Framework/TestUtility.cs | 44 +- .../Framework/TestWebSite.cs | 199 +++++- test/AspNetCoreModule.Test/FunctionalTest.cs | 27 + .../FunctionalTestHelper.cs | 649 ++++++++++++++++-- .../WebSocketClientHelper/Frame.cs | 16 +- .../WebSocketClientHelper.cs | 122 +++- .../WebSocketClientHelper/WebSocketConnect.cs | 4 +- .../WebSocketClientHelper/WebSocketState.cs | 1 + .../Program.cs | 12 +- .../Startup.cs | 23 +- test/WebRoot/WebSocket/chatplus.html | 366 ++++++++++ .../WebRoot/WebSocket/images/select_arrow.gif | Bin 0 -> 636 bytes .../WebSocket/images/select_arrow_down.gif | Bin 0 -> 650 bytes .../WebSocket/images/select_arrow_over.gif | Bin 0 -> 639 bytes test/WebSocketClientEXE/App.config | 6 + test/WebSocketClientEXE/Program.cs | 53 ++ .../Properties/AssemblyInfo.cs | 36 + test/WebSocketClientEXE/TestUtility.cs | 15 + .../WebSocketClientEXE.csproj | 77 +++ 23 files changed, 1552 insertions(+), 149 deletions(-) create mode 100644 test/WebRoot/WebSocket/chatplus.html create mode 100644 test/WebRoot/WebSocket/images/select_arrow.gif create mode 100644 test/WebRoot/WebSocket/images/select_arrow_down.gif create mode 100644 test/WebRoot/WebSocket/images/select_arrow_over.gif create mode 100644 test/WebSocketClientEXE/App.config create mode 100644 test/WebSocketClientEXE/Program.cs create mode 100644 test/WebSocketClientEXE/Properties/AssemblyInfo.cs create mode 100644 test/WebSocketClientEXE/TestUtility.cs create mode 100644 test/WebSocketClientEXE/WebSocketClientEXE.csproj diff --git a/AspNetCoreModule.sln b/AspNetCoreModule.sln index d18cf23d4b..54c32b50f6 100644 --- a/AspNetCoreModule.sln +++ b/AspNetCoreModule.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26224.1 +VisualStudioVersion = 15.0.26815.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AspNetCore", "src\AspNetCore\AspNetCore.vcxproj", "{439824F9-1455-4CC4-BD79-B44FA0A16552}" ProjectSection(ProjectDependencies) = postProject @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NuGet.Config = NuGet.Config EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketClientEXE", "test\WebSocketClientEXE\WebSocketClientEXE.csproj", "{4062EA94-75F5-4691-86DC-C8594BA896DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,6 +79,18 @@ Global {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Release|Win32.Build.0 = Release|Any CPU {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Release|x64.ActiveCfg = Release|Any CPU {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Release|x64.Build.0 = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|Win32.Build.0 = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Debug|x64.Build.0 = Debug|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|Any CPU.Build.0 = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|Win32.ActiveCfg = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|Win32.Build.0 = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|x64.ActiveCfg = Release|Any CPU + {4062EA94-75F5-4691-86DC-C8594BA896DE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,5 +100,6 @@ Global {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE} = {FDD2EDF8-1B62-4978-9815-9D95260B8B91} {4DDA7560-AA29-4161-A5EA-A7E8F3997321} = {02F461DC-5166-4E88-AAD5-CF110016A647} {030225D8-4EE8-47E5-B692-2A96B3B51A38} = {02F461DC-5166-4E88-AAD5-CF110016A647} + {4062EA94-75F5-4691-86DC-C8594BA896DE} = {02F461DC-5166-4E88-AAD5-CF110016A647} EndGlobalSection EndGlobal diff --git a/test/AspNetCoreModule.Test/AspNetCoreModule.Test.csproj b/test/AspNetCoreModule.Test/AspNetCoreModule.Test.csproj index 8ed0ec821d..e366f51373 100644 --- a/test/AspNetCoreModule.Test/AspNetCoreModule.Test.csproj +++ b/test/AspNetCoreModule.Test/AspNetCoreModule.Test.csproj @@ -5,6 +5,7 @@ net46 true true + AspNetCoreModule.Test @@ -21,8 +22,8 @@ - - + + @@ -44,8 +45,13 @@ - - + + + + + + + diff --git a/test/AspNetCoreModule.Test/Framework/IISConfigUtility.cs b/test/AspNetCoreModule.Test/Framework/IISConfigUtility.cs index 284ef9148a..fc4c48ab3b 100644 --- a/test/AspNetCoreModule.Test/Framework/IISConfigUtility.cs +++ b/test/AspNetCoreModule.Test/Framework/IISConfigUtility.cs @@ -79,6 +79,24 @@ namespace AspNetCoreModule.Test.Framework return result; } + public static void RestoreAppHostConfig(string fileExtenstion, bool overWriteMode) + { + string tofile = Strings.AppHostConfigPath; + string fromfile = Strings.AppHostConfigPath + fileExtenstion; + if (File.Exists(fromfile)) + { + try + { + TestUtility.FileCopy(fromfile, tofile, overWrite:overWriteMode); + } + catch + { + TestUtility.LogInformation("Failed to Restore applicationhost.config"); + throw; + } + } + } + public static void RestoreAppHostConfig(bool restoreFromMasterBackupFile = true) { string masterBackupFileExtension = ".ancmtest.masterbackup"; diff --git a/test/AspNetCoreModule.Test/Framework/InitializeTestMachine.cs b/test/AspNetCoreModule.Test/Framework/InitializeTestMachine.cs index f64a1ed317..09f32d0691 100644 --- a/test/AspNetCoreModule.Test/Framework/InitializeTestMachine.cs +++ b/test/AspNetCoreModule.Test/Framework/InitializeTestMachine.cs @@ -14,7 +14,7 @@ namespace AspNetCoreModule.Test.Framework public const string ANCMTestFlagsDefaultContext = "AdminAnd64Bit"; public const string ANCMTestFlagsTestSkipContext = "SkipTest"; public const string ANCMTestFlagsUsePrivateAspNetCoreFileContext = "UsePrivate"; - public const string ANCMTestFlagsUseIISExpressContext = "UseIISExpress"; + private const string ANCMTestFlagsUseIISExpressContext = "UseIISExpress"; private static bool? _usePrivateAspNetCoreFile = null; public static bool? UsePrivateAspNetCoreFile diff --git a/test/AspNetCoreModule.Test/Framework/TestUtility.cs b/test/AspNetCoreModule.Test/Framework/TestUtility.cs index 67c6947e0f..0241427759 100644 --- a/test/AspNetCoreModule.Test/Framework/TestUtility.cs +++ b/test/AspNetCoreModule.Test/Framework/TestUtility.cs @@ -791,7 +791,47 @@ namespace AspNetCoreModule.Test.Framework return stringBuilder.ToString().Trim(new char[] { ' ', '\r', '\n' }); } - + + public static void RunPowershellScript(string scriptText, string expectedResult, int retryCount = 3) + { + bool isReady = false; + string result = string.Empty; + + for (int i = 0; i < retryCount; i++) + { + try + { + result = TestUtility.RunPowershellScript(scriptText); + } + catch + { + result = "ExceptionError"; + } + + if (expectedResult != null) + { + if (expectedResult == result) + { + isReady = true; + break; + } + else + { + System.Threading.Thread.Sleep(1000); + } + } + else + { + isReady = true; + break; + } + } + if (!isReady) + { + throw new ApplicationException("Failed to execute command: " + scriptText + ", expected result: " + expectedResult + ", actual result = " + result); + } + } + public static int RunCommand(string fileName, string arguments = null, bool checkStandardError = true, bool waitForExit=true) { int pid = -1; @@ -809,7 +849,7 @@ namespace AspNetCoreModule.Test.Framework } p.StartInfo.UseShellExecute = false; - p.StartInfo.CreateNoWindow = true; + p.StartInfo.CreateNoWindow = true; p.Start(); pid = p.Id; string standardOutput = string.Empty; diff --git a/test/AspNetCoreModule.Test/Framework/TestWebSite.cs b/test/AspNetCoreModule.Test/Framework/TestWebSite.cs index 851ee2491a..e03e438267 100644 --- a/test/AspNetCoreModule.Test/Framework/TestWebSite.cs +++ b/test/AspNetCoreModule.Test/Framework/TestWebSite.cs @@ -99,12 +99,43 @@ namespace AspNetCoreModule.Test.Framework } } + private int _workerProcessID = 0; + public int WorkerProcessID + { + get + { + if (_workerProcessID == 0) + { + try + { + if (IisServerType == ServerType.IISExpress) + { + _workerProcessID = Convert.ToInt32(TestUtility.GetProcessWMIAttributeValue("iisexpress.exe", "Handle", null)); + } + else + { + _workerProcessID = Convert.ToInt32(TestUtility.GetProcessWMIAttributeValue("w3wp.exe", "Handle", null)); + } + } + catch + { + TestUtility.LogInformation("Failed to get process id of w3wp.exe"); + } + } + return _workerProcessID; + } + set + { + _workerProcessID = value; + } + } + public ServerType IisServerType { get; set; } public string IisExpressConfigPath { get; set; } private int _siteId { get; set; } private IISConfigUtility.AppPoolBitness _appPoolBitness { get; set; } - public TestWebSite(IISConfigUtility.AppPoolBitness appPoolBitness, string loggerPrefix = "ANCMTest", bool startIISExpress = true, bool copyAllPublishedFiles = false) + public TestWebSite(IISConfigUtility.AppPoolBitness appPoolBitness, string loggerPrefix = "ANCMTest", bool startIISExpress = true, bool copyAllPublishedFiles = false, bool attachAppVerifier = false) { _appPoolBitness = appPoolBitness; @@ -165,9 +196,9 @@ namespace AspNetCoreModule.Test.Framework } // - // Currently we use only DotnetCore v1.1 + // Currently we use DotnetCore v1.0 // - string publishPath = Path.Combine(srcPath, "bin", "Debug", "netcoreapp1.1", "publish"); + string publishPath = Path.Combine(srcPath, "bin", "Debug", "netcoreapp1.0", "publish"); string publishPathOutput = Path.Combine(Environment.ExpandEnvironmentVariables("%SystemDrive%") + @"\", "inetpub", "ANCMTest", "publishPathOutput"); // @@ -177,7 +208,12 @@ namespace AspNetCoreModule.Test.Framework { string argumentForDotNet = "publish " + srcPath; TestUtility.LogInformation("TestWebSite::TestWebSite() StandardTestApp is not published, trying to publish on the fly: dotnet.exe " + argumentForDotNet); + TestUtility.DeleteDirectory(publishPath); TestUtility.RunCommand("dotnet", argumentForDotNet); + if (!File.Exists(Path.Combine(publishPath, "AspNetCoreModule.TestSites.Standard.dll"))) + { + throw new Exception("Failed to publish"); + } TestUtility.DirectoryCopy(publishPath, publishPathOutput); TestUtility.FileCopy(Path.Combine(publishPathOutput, "web.config"), Path.Combine(publishPathOutput, "web.config.bak")); @@ -283,19 +319,27 @@ namespace AspNetCoreModule.Test.Framework if (startIISExpress) { - StartIISExpress(); - } + // clean up IISExpress before starting a new instance + TestUtility.KillIISExpressProcess(); + StartIISExpress(); + + // send a startup request to IISExpress instance to make sure that it is fully ready to use before starting actual test scenarios + TestUtility.RunPowershellScript("( invoke-webrequest http://localhost:" + TcpPort + " ).StatusCode", "200"); + } TestUtility.LogInformation("TestWebSite::TestWebSite() End"); } - public void StartIISExpress(string verificationCommand = null) + public void StartIISExpress() { if (IisServerType == ServerType.IIS) { return; } + // reset workerProcessID + this.WorkerProcessID = 0; + string cmdline; string argument = "/siteid:" + _siteId + " /config:" + IisExpressConfigPath; @@ -309,41 +353,126 @@ namespace AspNetCoreModule.Test.Framework } TestUtility.LogInformation("TestWebSite::TestWebSite() Start IISExpress: " + cmdline + " " + argument); _iisExpressPidBackup = TestUtility.RunCommand(cmdline, argument, false, false); - - bool isIISExpressReady = false; - int timeout = 3; - for (int i = 0; i < timeout * 5; i++) + } + + public void AttachAppverifier() + { + string cmdline; + string processName = "iisexpress.exe"; + if (IisServerType == ServerType.IIS) { - string statusCode = string.Empty; - try - { - if (verificationCommand == null) - { - verificationCommand = "( invoke-webrequest http://localhost:" + TcpPort + " ).StatusCode"; - } - statusCode = TestUtility.RunPowershellScript(verificationCommand); - } - catch - { - statusCode = "ExceptionError"; - } - if ("200" == statusCode) - { - isIISExpressReady = true; - break; - } - else - { - System.Threading.Thread.Sleep(200); - } + processName = "w3wp.exe"; } - if (isIISExpressReady) + string argument = "-enable Heaps COM RPC Handles Locks Memory TLS Exceptions Threadpool Leak SRWLock -for " + processName; + if (Directory.Exists(Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%")) && _appPoolBitness == IISConfigUtility.AppPoolBitness.enable32Bit) { - TestUtility.LogInformation("IISExpress is ready to use"); + cmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "syswow64", "appverif.exe"); + if (!File.Exists(cmdline)) + { + throw new System.ApplicationException("Not found :" + cmdline + "; this test requires appverif.exe."); + } } else { - throw new ApplicationException("IISExpress is not responding within " + timeout + " seconds"); + cmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32", "appverif.exe"); + if (!File.Exists(cmdline)) + { + throw new System.ApplicationException("Not found :" + cmdline + "; this test requires appverif.exe."); + } + } + + try + { + TestUtility.LogInformation("Configure Appverifier: " + cmdline + " " + argument); + TestUtility.RunCommand(cmdline, argument, true, false); + } + catch + { + throw new System.ApplicationException("Failed to configure Appverifier"); + } + } + + public void AttachWinDbg(int processIdOfWorkerProcess) + { + string processName = "iisexpress.exe"; + string debuggerCmdline; + if (IisServerType == ServerType.IIS) + { + processName = "w3wp.exe"; + } + string argument = "-enable Heaps COM RPC Handles Locks Memory TLS Exceptions Threadpool Leak SRWLock -for " + processName; + if (Directory.Exists(Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%")) && _appPoolBitness == IISConfigUtility.AppPoolBitness.enable32Bit) + { + debuggerCmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%ProgramFiles%"), "Debugging Tools for Windows (x64)", "wow64", "windbg.exe"); + if (!File.Exists(debuggerCmdline)) + { + throw new System.ApplicationException("Not found :" + debuggerCmdline + "; this test requires windbg.exe."); + } + } + else + { + debuggerCmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%ProgramFiles%"), "Debugging Tools for Windows (x64)", "windbg.exe"); + if (!File.Exists(debuggerCmdline)) + { + throw new System.ApplicationException("Not found :" + debuggerCmdline + "; this test requires windbg.exe."); + } + } + + try + { + TestUtility.RunCommand(debuggerCmdline, " -g -G -p " + processIdOfWorkerProcess.ToString(), true, false); + System.Threading.Thread.Sleep(3000); + } + catch + { + throw new System.ApplicationException("Failed to attach debuger"); + } + } + + public void DetachAppverifier() + { + try + { + string cmdline; + string processName = "iisexpress.exe"; + string debuggerCmdline; + if (IisServerType == ServerType.IIS) + { + processName = "w3wp.exe"; + } + + string argument = "-disable * -for " + processName; + if (Directory.Exists(Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%")) && _appPoolBitness == IISConfigUtility.AppPoolBitness.enable32Bit) + { + cmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "syswow64", "appverif.exe"); + if (!File.Exists(cmdline)) + { + throw new System.ApplicationException("Not found :" + cmdline); + } + debuggerCmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%ProgramFiles%"), "Debugging Tools for Windows (x64)", "wow64", "windbg.exe"); + if (!File.Exists(debuggerCmdline)) + { + throw new System.ApplicationException("Not found :" + debuggerCmdline); + } + } + else + { + cmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32", "appverif.exe"); + if (!File.Exists(cmdline)) + { + throw new System.ApplicationException("Not found :" + cmdline); + } + debuggerCmdline = Path.Combine(Environment.ExpandEnvironmentVariables("%ProgramFiles%"), "Debugging Tools for Windows (x64)", "windbg.exe"); + if (!File.Exists(debuggerCmdline)) + { + throw new System.ApplicationException("Not found :" + debuggerCmdline); + } + } + TestUtility.RunCommand(cmdline, argument, true, false); + } + catch + { + TestUtility.LogInformation("Failed to detach Appverifier"); } } } diff --git a/test/AspNetCoreModule.Test/FunctionalTest.cs b/test/AspNetCoreModule.Test/FunctionalTest.cs index 93d0df1f1e..adc46727bf 100644 --- a/test/AspNetCoreModule.Test/FunctionalTest.cs +++ b/test/AspNetCoreModule.Test/FunctionalTest.cs @@ -308,6 +308,33 @@ namespace AspNetCoreModule.Test return DoClientCertificateMappingTest(appPoolBitness, useHTTPSMiddleWare); } + [ConditionalTheory] + [ANCMTestSkipCondition("%ANCMTestFlags%")] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(IISConfigUtility.AppPoolBitness.noChange, false, DoAppVerifierTest_StartUpMode.UseGracefulShutdown, DoAppVerifierTest_ShutDownMode.RecycleAppPool, 1)] + [InlineData(IISConfigUtility.AppPoolBitness.noChange, false, DoAppVerifierTest_StartUpMode.DontUseGracefulShutdown, DoAppVerifierTest_ShutDownMode.RecycleAppPool, 1)] + [InlineData(IISConfigUtility.AppPoolBitness.noChange, false, DoAppVerifierTest_StartUpMode.UseGracefulShutdown, DoAppVerifierTest_ShutDownMode.StopAndStartAppPool, 1)] + public Task AppVerifierTest(IISConfigUtility.AppPoolBitness appPoolBitness, bool shutdownTimeout, DoAppVerifierTest_StartUpMode startUpMode, DoAppVerifierTest_ShutDownMode shutDownMode, int repeatCount) + { + return DoAppVerifierTest(appPoolBitness, shutdownTimeout, startUpMode, shutDownMode, repeatCount); + } + + ////////////////////////////////////////////////////////// + // NOTE: below test scenarios are not valid for Win7 OS + ////////////////////////////////////////////////////////// + + [ConditionalTheory] + [ANCMTestSkipCondition("%ANCMTestFlags%")] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(IISConfigUtility.AppPoolBitness.enable32Bit)] + [InlineData(IISConfigUtility.AppPoolBitness.noChange)] + public Task WebSocketErrorhandlingTest(IISConfigUtility.AppPoolBitness appPoolBitness) + { + return DoWebSocketErrorhandlingTest(appPoolBitness); + } + ////////////////////////////////////////////////////////// // NOTE: below test scenarios are not valid for Win7 OS ////////////////////////////////////////////////////////// diff --git a/test/AspNetCoreModule.Test/FunctionalTestHelper.cs b/test/AspNetCoreModule.Test/FunctionalTestHelper.cs index f20551586e..075774a83b 100644 --- a/test/AspNetCoreModule.Test/FunctionalTestHelper.cs +++ b/test/AspNetCoreModule.Test/FunctionalTestHelper.cs @@ -174,6 +174,8 @@ namespace AspNetCoreModule.Test { using (var testSite = new TestWebSite(appPoolBitness, "DoRecycleApplicationAfterW3WPProcessBeingKilled")) { + string appDllFileName = testSite.AspNetCoreApp.GetArgumentFileName(); + if (testSite.IisServerType == ServerType.IISExpress) { TestUtility.LogInformation("This test is not valid for IISExpress server type"); @@ -204,6 +206,11 @@ namespace AspNetCoreModule.Test workerProcess.Kill(); Thread.Sleep(500); + + // Verify the application file can be removed after worker process is stopped + testSite.AspNetCoreApp.BackupFile(appDllFileName); + testSite.AspNetCoreApp.DeleteFile(appDllFileName); + testSite.AspNetCoreApp.RestoreFile(appDllFileName); } } } @@ -213,6 +220,8 @@ namespace AspNetCoreModule.Test using (var testSite = new TestWebSite(appPoolBitness, "DoRecycleApplicationAfterWebConfigUpdated")) { string backendProcessId_old = null; + string appDllFileName = testSite.AspNetCoreApp.GetArgumentFileName(); + const int repeatCount = 3; for (int i = 0; i < repeatCount; i++) { @@ -231,6 +240,11 @@ namespace AspNetCoreModule.Test testSite.AspNetCoreApp.MoveFile("web.config", "_web.config"); Thread.Sleep(500); testSite.AspNetCoreApp.MoveFile("_web.config", "web.config"); + + // Verify the application file can be removed after backend process is restarted + testSite.AspNetCoreApp.BackupFile(appDllFileName); + testSite.AspNetCoreApp.DeleteFile(appDllFileName); + testSite.AspNetCoreApp.RestoreFile(appDllFileName); } // restore web.config @@ -320,9 +334,9 @@ namespace AspNetCoreModule.Test Assert.True(totalNumber == (await GetResponse(testSite.AspNetCoreApp.GetUri("GetEnvironmentVariables"), HttpStatusCode.OK))); iisConfig.SetANCMConfig( - testSite.SiteName, - testSite.AspNetCoreApp.Name, - "environmentVariable", + testSite.SiteName, + testSite.AspNetCoreApp.Name, + "environmentVariable", new string[] { "ANCMTestFoo", "foo" } ); @@ -366,7 +380,7 @@ namespace AspNetCoreModule.Test // Add a new environment variable if (setEnvironmentVariableConfiguration) - { + { iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "environmentVariable", new string[] { environmentVariableName, environmentVariableValue }); // Adjust the new expected total number of environment variables @@ -377,7 +391,7 @@ namespace AspNetCoreModule.Test } } Thread.Sleep(500); - + // check JitDebugger before continuing TestUtility.ResetHelper(ResetHelperMode.KillVSJitDebugger); totalResult = (await GetResponse(testSite.AspNetCoreApp.GetUri("GetEnvironmentVariables"), HttpStatusCode.OK)); @@ -387,17 +401,17 @@ namespace AspNetCoreModule.Test // Verify other common environment variables string temp = (await GetResponse(testSite.AspNetCoreApp.GetUri("DumpEnvironmentVariables"), HttpStatusCode.OK)); - Assert.True(temp.Contains("ASPNETCORE_PORT")); - Assert.True(temp.Contains("ASPNETCORE_APPL_PATH")); - Assert.True(temp.Contains("ASPNETCORE_IIS_HTTPAUTH")); - Assert.True(temp.Contains("ASPNETCORE_TOKEN")); - Assert.True(temp.Contains("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES")); + Assert.Contains("ASPNETCORE_PORT", temp); + Assert.Contains("ASPNETCORE_APPL_PATH", temp); + Assert.Contains("ASPNETCORE_IIS_HTTPAUTH", temp); + Assert.Contains("ASPNETCORE_TOKEN", temp); + Assert.Contains("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", temp); // Verify other inherited environment variables - Assert.True(temp.Contains("PROCESSOR_ARCHITECTURE")); - Assert.True(temp.Contains("USERNAME")); - Assert.True(temp.Contains("USERDOMAIN")); - Assert.True(temp.Contains("USERPROFILE")); + Assert.Contains("PROCESSOR_ARCHITECTURE", temp); + Assert.Contains("USERNAME", temp); + Assert.Contains("USERDOMAIN", temp); + Assert.Contains("USERPROFILE", temp); } testSite.AspNetCoreApp.RestoreFile("web.config"); @@ -410,8 +424,10 @@ namespace AspNetCoreModule.Test { string backendProcessId_old = null; string fileContent = "BackEndAppOffline"; - testSite.AspNetCoreApp.CreateFile(new string[] { fileContent }, "App_Offline.Htm"); + string appDllFileName = testSite.AspNetCoreApp.GetArgumentFileName(); + testSite.AspNetCoreApp.CreateFile(new string[] { fileContent }, "App_Offline.Htm"); + for (int i = 0; i < _repeatCount; i++) { // check JitDebugger before continuing @@ -423,6 +439,11 @@ namespace AspNetCoreModule.Test // verify 503 await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), fileContent + "\r\n", HttpStatusCode.ServiceUnavailable); + // Verify the application file can be removed under app_offline mode + testSite.AspNetCoreApp.BackupFile(appDllFileName); + testSite.AspNetCoreApp.DeleteFile(appDllFileName); + testSite.AspNetCoreApp.RestoreFile(appDllFileName); + // rename app_offline.htm to _app_offline.htm and verify 200 testSite.AspNetCoreApp.MoveFile("App_Offline.Htm", "_App_Offline.Htm"); string backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); @@ -444,6 +465,8 @@ namespace AspNetCoreModule.Test { string backendProcessId_old = null; string fileContent = "BackEndAppOffline2"; + string appDllFileName = testSite.AspNetCoreApp.GetArgumentFileName(); + testSite.AspNetCoreApp.CreateFile(new string[] { fileContent }, "App_Offline.Htm"); for (int i = 0; i < _repeatCount; i++) @@ -458,6 +481,11 @@ namespace AspNetCoreModule.Test string urlForUrlRewrite = testSite.URLRewriteApp.URL + "/Rewrite2/" + testSite.AspNetCoreApp.URL + "/GetProcessId"; await VerifyResponseBody(testSite.RootAppContext.GetUri(urlForUrlRewrite), fileContent + "\r\n", HttpStatusCode.ServiceUnavailable); + // Verify the application file can be removed under app_offline mode + testSite.AspNetCoreApp.BackupFile(appDllFileName); + testSite.AspNetCoreApp.DeleteFile(appDllFileName); + testSite.AspNetCoreApp.RestoreFile(appDllFileName); + // delete app_offline.htm and verify 200 testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); string backendProcessId = await GetResponse(testSite.RootAppContext.GetUri(urlForUrlRewrite), HttpStatusCode.OK); @@ -528,7 +556,7 @@ namespace AspNetCoreModule.Test TestUtility.ResetHelper(ResetHelperMode.KillVSJitDebugger); responseBody = await GetResponse(testSite.AspNetCoreApp.GetUri(), HttpStatusCode.BadGateway); - Assert.True(responseBody.Contains("808681")); + Assert.Contains("808681", responseBody); // verify event error log Assert.True(TestUtility.RetryHelper((arg1, arg2, arg3) => VerifyApplicationEventLog(arg1, arg2, arg3), errorEventId, startTime, errorMessageContainThis)); @@ -574,7 +602,7 @@ namespace AspNetCoreModule.Test backendProcessId_old = backendProcessId; var backendProcess = Process.GetProcessById(Convert.ToInt32(backendProcessId)); Assert.Equal(backendProcess.ProcessName.ToLower().Replace(".exe", ""), testSite.AspNetCoreApp.GetProcessFileName().ToLower().Replace(".exe", "")); - + //Verifying EventID of new backend process is not necesssary and removed in order to fix some test reliablity issues //Thread.Sleep(3000); //Assert.True(TestUtility.RetryHelper((arg1, arg2) => VerifyANCMStartEvent(arg1, arg2), startTimeInsideLooping, backendProcessId), "Verifying event log of new backend process id " + backendProcessId); @@ -582,7 +610,7 @@ namespace AspNetCoreModule.Test backendProcess.Kill(); Thread.Sleep(3000); } - Assert.True(rapidFailsTriggered, "Verify 503 error"); + Assert.True(rapidFailsTriggered, "Verify 502 Bad Gateway error"); // verify event error log int errorEventId = 1003; @@ -647,7 +675,7 @@ namespace AspNetCoreModule.Test processIDs.Add(id); } } - Assert.Equal(1, processIDs.Count); + Assert.Single(processIDs); } testSite.AspNetCoreApp.RestoreFile("web.config"); @@ -662,9 +690,9 @@ namespace AspNetCoreModule.Test { int startupDelay = 3; //3 seconds iisConfig.SetANCMConfig( - testSite.SiteName, - testSite.AspNetCoreApp.Name, - "environmentVariable", + testSite.SiteName, + testSite.AspNetCoreApp.Name, + "environmentVariable", new string[] { "ANCMTestStartUpDelay", (startupDelay * 1000).ToString() } ); @@ -676,7 +704,7 @@ namespace AspNetCoreModule.Test { await VerifyResponseStatus(testSite.AspNetCoreApp.GetUri("DoSleep3000"), HttpStatusCode.BadGateway); } - else + else { await VerifyResponseBody(testSite.AspNetCoreApp.GetUri("DoSleep3000"), "Running", HttpStatusCode.OK); } @@ -691,12 +719,12 @@ namespace AspNetCoreModule.Test { using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) { - iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "requestTimeout", TimeSpan.Parse(requestTimeout)); + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "requestTimeout", TimeSpan.Parse(requestTimeout)); Thread.Sleep(500); if (requestTimeout.ToString() == "00:02:00") { - await VerifyResponseBody(testSite.AspNetCoreApp.GetUri("DoSleep65000"), "Running", HttpStatusCode.OK, timeout:70); + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri("DoSleep65000"), "Running", HttpStatusCode.OK, timeout: 70); } else if (requestTimeout.ToString() == "00:01:00") { @@ -732,7 +760,7 @@ namespace AspNetCoreModule.Test string response = await GetResponse(testSite.AspNetCoreApp.GetUri(""), HttpStatusCode.OK); Assert.True(response == "Running"); - + string backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); var backendProcess = Process.GetProcessById(Convert.ToInt32(backendProcessId)); @@ -749,7 +777,7 @@ namespace AspNetCoreModule.Test await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); // if expectedClosing time is less than the shutdownDelay time, gracefulshutdown is supposed to fail and failure event is expected - if (expectedClosingTime*1000 + 1000 == shutdownDelayTime) + if (expectedClosingTime * 1000 + 1000 == shutdownDelayTime) { Assert.True(TestUtility.RetryHelper((arg1, arg2) => VerifyANCMGracefulShutdownEvent(arg1, arg2), startTime, backendProcessId)); Assert.True(TestUtility.RetryHelper((arg1, arg2) => VerifyANCMGracefulShutdownEvent(arg1, arg2), startTime, expectedGracefulShutdownResponseStatusCode)); @@ -814,7 +842,7 @@ namespace AspNetCoreModule.Test public static async Task DoProcessPathAndArgumentsTest(IISConfigUtility.AppPoolBitness appPoolBitness, string processPath, string argumentsPrefix) { - using (var testSite = new TestWebSite(appPoolBitness, "DoProcessPathAndArgumentsTest", copyAllPublishedFiles:true)) + using (var testSite = new TestWebSite(appPoolBitness, "DoProcessPathAndArgumentsTest", copyAllPublishedFiles: true)) { using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) { @@ -848,7 +876,7 @@ namespace AspNetCoreModule.Test testSite.AspNetCoreApp.RestoreFile("web.config"); } } - + public static async Task DoForwardWindowsAuthTokenTest(IISConfigUtility.AppPoolBitness appPoolBitness, bool enabledForwardWindowsAuthToken) { using (var testSite = new TestWebSite(appPoolBitness, "DoForwardWindowsAuthTokenTest")) @@ -858,9 +886,9 @@ namespace AspNetCoreModule.Test string result = string.Empty; iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "forwardWindowsAuthToken", enabledForwardWindowsAuthToken); string requestHeaders = await GetResponse(testSite.AspNetCoreApp.GetUri("DumpRequestHeaders"), HttpStatusCode.OK); - Assert.False(requestHeaders.ToUpper().Contains("MS-ASPNETCORE-WINAUTHTOKEN")); + Assert.DoesNotContain("MS-ASPNETCORE-WINAUTHTOKEN", requestHeaders.ToUpper()); - iisConfig.EnableIISAuthentication(testSite.SiteName, windows:true, basic:false, anonymous:false); + iisConfig.EnableIISAuthentication(testSite.SiteName, windows: true, basic: false, anonymous: false); Thread.Sleep(500); // check JitDebugger before continuing @@ -871,7 +899,7 @@ namespace AspNetCoreModule.Test if (enabledForwardWindowsAuthToken) { string expectedHeaderName = "MS-ASPNETCORE-WINAUTHTOKEN"; - Assert.True(requestHeaders.ToUpper().Contains(expectedHeaderName)); + Assert.Contains(expectedHeaderName, requestHeaders.ToUpper()); result = await GetResponse(testSite.AspNetCoreApp.GetUri("ImpersonateMiddleware"), HttpStatusCode.OK); bool compare = false; @@ -892,10 +920,10 @@ namespace AspNetCoreModule.Test } else { - Assert.False(requestHeaders.ToUpper().Contains("MS-ASPNETCORE-WINAUTHTOKEN")); + Assert.DoesNotContain("MS-ASPNETCORE-WINAUTHTOKEN", requestHeaders.ToUpper()); result = await GetResponse(testSite.AspNetCoreApp.GetUri("ImpersonateMiddleware"), HttpStatusCode.OK); - Assert.True(result.Contains("ImpersonateMiddleware-UserName = NoAuthentication")); + Assert.Contains("ImpersonateMiddleware-UserName = NoAuthentication", result); } } @@ -915,13 +943,13 @@ namespace AspNetCoreModule.Test using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) { - + // allocating 1024,000 KB await VerifyResponseStatus(testSite.AspNetCoreApp.GetUri("MemoryLeak1024000"), HttpStatusCode.OK); - + // get backend process id string pocessIdBackendProcess = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); - + // get process id of IIS worker process (w3wp.exe) string userName = testSite.SiteName; int processIdOfWorkerProcess = Convert.ToInt32(TestUtility.GetProcessWMIAttributeValue("w3wp.exe", "Handle", userName)); @@ -948,7 +976,7 @@ namespace AspNetCoreModule.Test TestUtility.ResetHelper(ResetHelperMode.KillVSJitDebugger); iisConfig.SetAppPoolSetting(testSite.AspNetCoreApp.AppPoolName, "privateMemory", totalPrivateMemoryKB); - + // set 100 for rapidFailProtection counter for both IIS worker process and aspnetcore backend process iisConfig.SetAppPoolSetting(testSite.AspNetCoreApp.AppPoolName, "rapidFailProtectionMaxCrashes", 100); iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "rapidFailsPerMinute", 100); @@ -1029,12 +1057,12 @@ namespace AspNetCoreModule.Test { startupClass = "StartupNoCompressionCaching"; } - + // set startup class iisConfig.SetANCMConfig( - testSite.SiteName, - testSite.AspNetCoreApp.Name, - "environmentVariable", + testSite.SiteName, + testSite.AspNetCoreApp.Name, + "environmentVariable", new string[] { "ANCMTestStartupClassName", startupClass } ); @@ -1091,7 +1119,7 @@ namespace AspNetCoreModule.Test using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) { string startupClass = "StartupCompressionCaching"; - + // set startup class iisConfig.SetANCMConfig( testSite.SiteName, @@ -1103,7 +1131,7 @@ namespace AspNetCoreModule.Test // enable IIS compression // Note: IIS compression, however, will be ignored if AspnetCore compression middleware is enabled. iisConfig.SetCompression(testSite.SiteName, true); - + // prepare static contents testSite.AspNetCoreApp.CreateDirectory("wwwroot"); testSite.AspNetCoreApp.CreateDirectory(@"wwwroot\pdir"); @@ -1149,7 +1177,7 @@ namespace AspNetCoreModule.Test public static async Task DoSendHTTPSRequestTest(IISConfigUtility.AppPoolBitness appPoolBitness) { - using (var testSite = new TestWebSite(appPoolBitness, "DoSendHTTPSRequestTest", startIISExpress:false)) + using (var testSite = new TestWebSite(appPoolBitness, "DoSendHTTPSRequestTest", startIISExpress: false)) { using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) { @@ -1161,12 +1189,12 @@ namespace AspNetCoreModule.Test // Add https binding and get https uri information iisConfig.AddBindingToSite(testSite.SiteName, ipAddress, sslPort, hostName, "https"); - + // Create a self signed certificate string thumbPrint = iisConfig.CreateSelfSignedCertificate(subjectName); // Export the self signed certificate to rootCA - iisConfig.ExportCertificateTo(thumbPrint, sslStoreTo:@"Cert:\LocalMachine\Root"); + iisConfig.ExportCertificateTo(thumbPrint, sslStoreTo: @"Cert:\LocalMachine\Root"); // Configure http.sys ssl certificate mapping to IP:Port endpoint with the newly created self signed certificage iisConfig.SetSSLCertificate(sslPort, hexIPAddress, thumbPrint); @@ -1228,13 +1256,13 @@ namespace AspNetCoreModule.Test string requestHeaders = string.Empty; requestHeaders = await GetResponseAndHeaders(testSite.AspNetCoreApp.GetUri("DumpRequestHeaders"), new string[] { "Accept-Encoding", "gzip", requestHeader, requestHeaderValue }, HttpStatusCode.OK); requestHeaders = requestHeaders.Replace(" ", ""); - Assert.False(requestHeaders.ToUpper().Contains(requestHeader.ToLower()+":")); + Assert.DoesNotContain(requestHeader.ToLower() + ":", requestHeaders.ToUpper()); // Verify https request Uri targetHttpsUri = testSite.AspNetCoreApp.GetUri(null, sslPort, protocol: "https"); requestHeader = await GetResponseAndHeaders(targetHttpsUri, new string[] { "Accept-Encoding", "gzip", requestHeader, requestHeaderValue }, HttpStatusCode.OK); requestHeaders = requestHeaders.Replace(" ", ""); - Assert.False(requestHeaders.ToUpper().Contains(requestHeader.ToLower() + ":")); + Assert.DoesNotContain(requestHeader.ToLower() + ":", requestHeaders.ToUpper()); // Remove the SSL Certificate mapping iisConfig.RemoveSSLCertificate(sslPort, hexIPAddress); @@ -1273,11 +1301,11 @@ namespace AspNetCoreModule.Test // Create a certificate for web server setting its issuer with the root certificate subject name string thumbPrintForWebServer = iisConfig.CreateSelfSignedCertificateWithMakeCert(webServerCN, rootCN, extendedKeyUsage: "1.3.6.1.5.5.7.3.1"); - string thumbPrintForKestrel = null; + string thumbPrintForKestrel = null; // Create a certificate for client authentication setting its issuer with the root certificate subject name string thumbPrintForClientAuthentication = iisConfig.CreateSelfSignedCertificateWithMakeCert(clientCN, rootCN, extendedKeyUsage: "1.3.6.1.5.5.7.3.2"); - + // Configure http.sys ssl certificate mapping to IP:Port endpoint with the newly created self signed certificage iisConfig.SetSSLCertificate(sslPort, hexIPAddress, thumbPrintForWebServer); @@ -1337,7 +1365,8 @@ namespace AspNetCoreModule.Test // starting IISExpress was deffered after creating test applications and now it is ready to start it Uri rootHttpsUri = testSite.RootAppContext.GetUri(null, sslPort, protocol: "https"); - testSite.StartIISExpress("( invoke-webrequest " + rootHttpsUri.OriginalString + " -CertificateThumbprint " + thumbPrintForClientAuthentication + ").StatusCode"); + testSite.StartIISExpress(); + TestUtility.RunPowershellScript("( invoke-webrequest " + rootHttpsUri.OriginalString + " -CertificateThumbprint " + thumbPrintForClientAuthentication + ").StatusCode", "200"); // Verify http request with using client certificate Uri targetHttpsUri = testSite.AspNetCoreApp.GetUri(null, sslPort, protocol: "https"); @@ -1348,21 +1377,21 @@ namespace AspNetCoreModule.Test Uri targetHttpsUriForDumpRequestHeaders = testSite.AspNetCoreApp.GetUri("DumpRequestHeaders", sslPort, protocol: "https"); string outputRawContent = TestUtility.RunPowershellScript("( invoke-webrequest " + targetHttpsUriForDumpRequestHeaders.OriginalString + " -CertificateThumbprint " + thumbPrintForClientAuthentication + ").RawContent.ToString()"); string expectedHeaderName = "MS-ASPNETCORE-CLIENTCERT"; - Assert.True(outputRawContent.Contains(expectedHeaderName)); + Assert.Contains(expectedHeaderName, outputRawContent); // Get the value of MS-ASPNETCORE-CLIENTCERT request header again and verify it is matched to its configured public key Uri targetHttpsUriForCLIENTCERTRequestHeader = testSite.AspNetCoreApp.GetUri("GetRequestHeaderValueMS-ASPNETCORE-CLIENTCERT", sslPort, protocol: "https"); outputRawContent = TestUtility.RunPowershellScript("( invoke-webrequest " + targetHttpsUriForCLIENTCERTRequestHeader.OriginalString + " -CertificateThumbprint " + thumbPrintForClientAuthentication + ").RawContent.ToString()"); - Assert.True(outputRawContent.Contains(publicKey)); + Assert.Contains(publicKey, outputRawContent); // Verify non-https request returns 403.4 error string result = string.Empty; result = await GetResponseAndHeaders(testSite.AspNetCoreApp.GetUri(), new string[] { "Accept-Encoding", "gzip" }, HttpStatusCode.Forbidden); - Assert.True(result.Contains("403.4")); + Assert.Contains("403.4", result); // Verify https request without using client certificate returns 403.7 result = await GetResponseAndHeaders(targetHttpsUri, new string[] { "Accept-Encoding", "gzip" }, HttpStatusCode.Forbidden); - Assert.True(result.Contains("403.7")); + Assert.Contains("403.7", result); // Clean up user temp = TestUtility.RunPowershellScript("net localgroup IIS_IUSRS /Delete " + userName); @@ -1376,7 +1405,7 @@ namespace AspNetCoreModule.Test iisConfig.DeleteCertificate(thumbPrintForWebServer, @"Cert:\LocalMachine\My"); if (useHTTPSMiddleWare) { - iisConfig.DeleteCertificate(thumbPrintForKestrel, @"Cert:\LocalMachine\My"); + iisConfig.DeleteCertificate(thumbPrintForKestrel, @"Cert:\LocalMachine\My"); } iisConfig.DeleteCertificate(thumbPrintForClientAuthentication, @"Cert:\CurrentUser\My"); } @@ -1388,25 +1417,32 @@ namespace AspNetCoreModule.Test { using (var testSite = new TestWebSite(appPoolBitness, "DoWebSocketTest")) { + string appDllFileName = testSite.AspNetCoreApp.GetArgumentFileName(); + + using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) + { + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "shutdownTimeLimit", 10); + } + DateTime startTime = DateTime.Now; await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); // Get Process ID - string backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); - + string backendProcessId_old = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + // Verify WebSocket without setting subprotocol await VerifyResponseBodyContain(testSite.WebSocketApp.GetUri("echo.aspx"), new string[] { "Socket Open" }, HttpStatusCode.OK); // echo.aspx has hard coded path for the websocket server // Verify WebSocket subprotocol await VerifyResponseBodyContain(testSite.WebSocketApp.GetUri("echoSubProtocol.aspx"), new string[] { "Socket Open", "mywebsocketsubprotocol" }, HttpStatusCode.OK); // echoSubProtocol.aspx has hard coded path for the websocket server - + // Verify websocket using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) { var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); - Assert.True(frameReturned.Content.Contains("Connection: Upgrade")); - Assert.True(frameReturned.Content.Contains("HTTP/1.1 101 Switching Protocols")); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); Thread.Sleep(500); VerifySendingWebSocketData(websocketClient, testData); @@ -1418,8 +1454,489 @@ namespace AspNetCoreModule.Test Assert.True(frameReturned.FrameType == FrameType.Close, "Closing Handshake"); } - // send a simple request again and verify the response body + // send a simple request and verify the response body await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + Thread.Sleep(500); + string backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + Assert.Equal(backendProcessId_old, backendProcessId); + + // Verify server side websocket disconnection + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + for (int jj = 0; jj < 3; jj++) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + Assert.True(websocketClient.IsOpened, "Check active connection before starting"); + + // Send a special string to initiate the server side connection closing + websocketClient.SendTextData("CloseFromServer"); + bool connectionClosedFromServer = websocketClient.WaitForWebSocketState(WebSocketState.ConnectionClosed); + + // Verify server side connection closing is done successfully + Assert.True(connectionClosedFromServer, "Closing Handshake initiated from Server"); + + // extract text data from the last frame, which is the close frame + int lastIndex = websocketClient.Connection.DataReceived.Count - 1; + + // Verify text data is matched to the string sent by server + Assert.Equal("ClosingFromServer", websocketClient.Connection.DataReceived[lastIndex].TextData); + } + } + + // send a simple request and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + Thread.Sleep(500); + backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + Assert.Equal(backendProcessId_old, backendProcessId); + + // Verify websocket with app_offline.htm + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + for (int jj = 0; jj < 3; jj++) + { + testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); + Thread.Sleep(1000); + + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + VerifySendingWebSocketData(websocketClient, testData); + Thread.Sleep(500); + + // put app_offline + testSite.AspNetCoreApp.CreateFile(new string[] { "test" }, "App_Offline.Htm"); + Thread.Sleep(1000); + + // ToDo: This should be replaced when the server can handle this automaticially + // send a websocket data to invoke the server side websocket disconnection after the app_offline + websocketClient.SendTextData("test"); + bool connectionClosedFromServer = websocketClient.WaitForWebSocketState(WebSocketState.ConnectionClosed); + + // Verify server side connection closing is done successfully + Assert.True(connectionClosedFromServer, "Closing Handshake initiated from Server"); + + // extract text data from the last frame, which is the close frame + int lastIndex = websocketClient.Connection.DataReceived.Count - 1; + + // Verify text data is matched to the string sent by server + Assert.Equal("ClosingFromServer", websocketClient.Connection.DataReceived[lastIndex].TextData); + + // Verify the application file can be removed under app_offline mode + testSite.AspNetCoreApp.BackupFile(appDllFileName); + testSite.AspNetCoreApp.DeleteFile(appDllFileName); + testSite.AspNetCoreApp.RestoreFile(appDllFileName); + } + } + + // remove app_offline.htm + testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); + Thread.Sleep(1000); + + /* + BugBug!!! configuration change does not invoke the shutdown message + because IIS does not trigger the change notification event until all websocket connection is gone. + This scenario should be added back when the issue is resolved. + + // Verify websocket with configuration change notification + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + for (int jj = 0; jj < 10; jj++) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + VerifySendingWebSocketData(websocketClient, testData); + Thread.Sleep(500); + + using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) + { + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "startupTimeLimit", 11 + jj); + Thread.Sleep(1000); + } + + // ToDo: This should be replaced when the server can handle this automaticially + // send a websocket data to invoke the server side websocket disconnection after the app_offline + websocketClient.SendTextData("test"); + bool connectionClosedFromServer = websocketClient.WaitForWebSocketState(WebSocketState.ConnectionClosed); + + // Verify server side connection closing is done successfully + Assert.True(connectionClosedFromServer, "Closing Handshake initiated from Server"); + + // extract text data from the last frame, which is the close frame + int lastIndex = websocketClient.Connection.DataReceived.Count - 1; + + // Verify text data is matched to the string sent by server + Assert.Equal("ClosingFromServer", websocketClient.Connection.DataReceived[lastIndex].TextData); + } + } + */ + + // send a simple request and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + } + } + + public static async Task DoWebSocketErrorhandlingTest(IISConfigUtility.AppPoolBitness appPoolBitness) + { + try + { + using (var testSite = new TestWebSite(appPoolBitness, "DoWebSocketErrorhandlingTest")) + { + // Verify websocket returns 404 when websocket module is not registered + using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) + { + // Remove websocketModule + IISConfigUtility.BackupAppHostConfig("DoWebSocketErrorhandlingTest", true); + iisConfig.RemoveModule("WebSocketModule"); + + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.DoesNotContain("Connection: Upgrade", frameReturned.Content); + + //BugBug: Currently we returns 101 here. + //Assert.DoesNotContain("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + } + } + + // send a simple request again and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + // roback configuration + IISConfigUtility.RestoreAppHostConfig("DoWebSocketErrorhandlingTest", true); + } + } + catch + { + // roback configuration + IISConfigUtility.RestoreAppHostConfig("DoWebSocketErrorhandlingTest", true); + throw; + } + } + + public enum DoAppVerifierTest_ShutDownMode + { + RecycleAppPool, + CreateAppOfflineHtm, + StopAndStartAppPool, + RestartW3SVC, + ConfigurationChangeNotification + } + + public enum DoAppVerifierTest_StartUpMode + { + UseGracefulShutdown, + DontUseGracefulShutdown + } + + public static async Task DoAppVerifierTest(IISConfigUtility.AppPoolBitness appPoolBitness, bool verifyTimeout, DoAppVerifierTest_StartUpMode startUpMode, DoAppVerifierTest_ShutDownMode shutDownMode, int repeatCount = 2) + { + TestWebSite testSite = null; + bool testResult = false; + + testSite = new TestWebSite(appPoolBitness, "DoAppVerifierTest", startIISExpress: false); + if (testSite.IisServerType == ServerType.IISExpress) + { + TestUtility.LogInformation("This test is not valid for IISExpress server type because of IISExpress bug; Once it is resolved, we should activate this test for IISExpress as well"); + return; + } + + // enable AppVerifier + testSite.AttachAppverifier(); + + using (var iisConfig = new IISConfigUtility(testSite.IisServerType, testSite.IisExpressConfigPath)) + { + // Prepare https binding + string hostName = ""; + string subjectName = "localhost"; + string ipAddress = "*"; + string hexIPAddress = "0x00"; + int sslPort = InitializeTestMachine.SiteId + 6300; + + // Add https binding and get https uri information + iisConfig.AddBindingToSite(testSite.SiteName, ipAddress, sslPort, hostName, "https"); + + // Create a self signed certificate + string thumbPrint = iisConfig.CreateSelfSignedCertificate(subjectName); + + // Export the self signed certificate to rootCA + iisConfig.ExportCertificateTo(thumbPrint, sslStoreTo: @"Cert:\LocalMachine\Root"); + + // Configure http.sys ssl certificate mapping to IP:Port endpoint with the newly created self signed certificage + iisConfig.SetSSLCertificate(sslPort, hexIPAddress, thumbPrint); + + // Set shutdownTimeLimit with 3 seconds and use 5 seconds for delay time to make the shutdownTimeout happen + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "shutdownTimeLimit", 3); + + int timeoutValue = 3; + if (verifyTimeout) + { + // set requestTimeout + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "requestTimeout", TimeSpan.Parse("00:01:00")); // 1 minute + + // set startupTimeout + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "startupTimeLimit", timeoutValue); + + // Set shutdownTimeLimit with 3 seconds and use 5 seconds for delay time to make the shutdownTimeout happen + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "shutdownTimeLimit", timeoutValue); + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "environmentVariable", new string[] { "ANCMTestShutdownDelay", "10" }); + } + + // starting IISExpress was deffered after creating test applications and now it is ready to start it + testSite.StartIISExpress(); + + if (verifyTimeout) + { + Thread.Sleep(500); + + // initial request which requires more than startup timeout should fails + await VerifyResponseStatus(testSite.AspNetCoreApp.GetUri("DoSleep5000"), HttpStatusCode.BadGateway, timeout: 10); + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri("DoSleep5000"), "Running", HttpStatusCode.OK, timeout: 10); + + // request which requires more than request timeout should fails + await VerifyResponseStatus(testSite.AspNetCoreApp.GetUri("DoSleep65000"), HttpStatusCode.BadGateway, timeout: 70); + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri("DoSleep50000"), "Running", HttpStatusCode.OK, timeout: 70); + } + + /////////////////////////////////// + // Start test sceanrio + /////////////////////////////////// + if (startUpMode == DoAppVerifierTest_StartUpMode.DontUseGracefulShutdown) + { + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "environmentVariable", new string[] { "GracefulShutdown", "disabled" }); + } + + // reset existing worker process process + TestUtility.ResetHelper(ResetHelperMode.KillWorkerProcess); + + for (int i = 0; i < repeatCount; i++) + { + // reset worker process id to refresh + testSite.WorkerProcessID = 0; + + // send a startup request to start a new worker process + TestUtility.RunPowershellScript("( invoke-webrequest http://localhost:" + testSite.TcpPort + " ).StatusCode", "200", retryCount: 5); + + // attach debugger to the worker process + testSite.AttachWinDbg(testSite.WorkerProcessID); + TestUtility.RunPowershellScript("( invoke-webrequest http://localhost:" + testSite.TcpPort + " ).StatusCode", "200", retryCount: 30); + + // verify windbg process is started + TestUtility.RunPowershellScript("(get-process -name windbg 2> $null).count", "1", retryCount: 5); + + DateTime startTime = DateTime.Now; + + // Verify http request + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + // Get Process ID + string backendProcessId_old = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + + // Verify WebSocket without setting subprotocol + await VerifyResponseBodyContain(testSite.WebSocketApp.GetUri("echo.aspx"), new string[] { "Socket Open" }, HttpStatusCode.OK); // echo.aspx has hard coded path for the websocket server + + // Verify WebSocket subprotocol + await VerifyResponseBodyContain(testSite.WebSocketApp.GetUri("echoSubProtocol.aspx"), new string[] { "Socket Open", "mywebsocketsubprotocol" }, HttpStatusCode.OK); // echoSubProtocol.aspx has hard coded path for the websocket server + + string testData = "test"; + + // Verify websocket + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + VerifySendingWebSocketData(websocketClient, testData); + Thread.Sleep(500); + + frameReturned = websocketClient.Close(); + Thread.Sleep(500); + + Assert.True(frameReturned.FrameType == FrameType.Close, "Closing Handshake"); + } + + // send a simple request and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + Thread.Sleep(500); + string backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + Assert.Equal(backendProcessId_old, backendProcessId); + + // Verify server side websocket disconnection + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + Assert.True(websocketClient.IsOpened, "Check active connection before starting"); + + // Send a special string to initiate the server side connection closing + websocketClient.SendTextData("CloseFromServer"); + bool connectionClosedFromServer = websocketClient.WaitForWebSocketState(WebSocketState.ConnectionClosed); + + // Verify server side connection closing is done successfully + Assert.True(connectionClosedFromServer, "Closing Handshake initiated from Server"); + + // extract text data from the last frame, which is the close frame + int lastIndex = websocketClient.Connection.DataReceived.Count - 1; + + // Verify text data is matched to the string sent by server + Assert.Equal("ClosingFromServer", websocketClient.Connection.DataReceived[lastIndex].TextData); + } + + // send a simple request and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + Thread.Sleep(500); + backendProcessId = await GetResponse(testSite.AspNetCoreApp.GetUri("GetProcessId"), HttpStatusCode.OK); + Assert.Equal(backendProcessId_old, backendProcessId); + + if (startUpMode != DoAppVerifierTest_StartUpMode.DontUseGracefulShutdown) + { + // Verify websocket with app_offline.htm + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + for (int jj = 0; jj < 10; jj++) + { + testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); + Thread.Sleep(1000); + + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + VerifySendingWebSocketData(websocketClient, testData); + Thread.Sleep(500); + + // put app_offline + testSite.AspNetCoreApp.CreateFile(new string[] { "test" }, "App_Offline.Htm"); + Thread.Sleep(500); + + // ToDo: remove this when server can handle this automatically + // send a websocket data to invoke the server side websocket disconnection after the app_offline + websocketClient.SendTextData("test"); + bool connectionClosedFromServer = websocketClient.WaitForWebSocketState(WebSocketState.ConnectionClosed); + + // Verify server side connection closing is done successfully + Assert.True(connectionClosedFromServer, "Closing Handshake initiated from Server"); + } + } + + // remove app_offline.htm + testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); + Thread.Sleep(500); + } + + // Verify websocket again + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + var frameReturned = websocketClient.Connect(testSite.AspNetCoreApp.GetUri("websocket"), true, true); + Assert.Contains("Connection: Upgrade", frameReturned.Content); + Assert.Contains("HTTP/1.1 101 Switching Protocols", frameReturned.Content); + Thread.Sleep(500); + + VerifySendingWebSocketData(websocketClient, testData); + Thread.Sleep(500); + + frameReturned = websocketClient.Close(); + Thread.Sleep(500); + + Assert.True(frameReturned.FrameType == FrameType.Close, "Closing Handshake"); + } + + // send a simple request and verify the response body + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "Running", HttpStatusCode.OK); + + // Verify https request + Uri targetHttpsUri = testSite.AspNetCoreApp.GetUri(null, sslPort, protocol: "https"); + var result = await GetResponseAndHeaders(targetHttpsUri, new string[] { "Accept-Encoding", "gzip" }, HttpStatusCode.OK); + Assert.True(result.Contains("Running"), "verify response body"); + + switch (shutDownMode) + { + case DoAppVerifierTest_ShutDownMode.StopAndStartAppPool: + iisConfig.StopAppPool(testSite.AspNetCoreApp.AppPoolName); + Thread.Sleep(5000); + iisConfig.StartAppPool(testSite.AspNetCoreApp.AppPoolName); + break; + case DoAppVerifierTest_ShutDownMode.RestartW3SVC: + TestUtility.ResetHelper(ResetHelperMode.StopWasStartW3svc); + break; + case DoAppVerifierTest_ShutDownMode.CreateAppOfflineHtm: + testSite.AspNetCoreApp.DeleteFile("App_Offline.Htm"); + testSite.AspNetCoreApp.CreateFile(new string[] { "test" }, "App_Offline.Htm"); + break; + case DoAppVerifierTest_ShutDownMode.ConfigurationChangeNotification: + iisConfig.SetANCMConfig(testSite.SiteName, testSite.AspNetCoreApp.Name, "startupTimeLimit", timeoutValue + 1); + iisConfig.RecycleAppPool(testSite.AspNetCoreApp.AppPoolName); + break; + case DoAppVerifierTest_ShutDownMode.RecycleAppPool: + iisConfig.RecycleAppPool(testSite.AspNetCoreApp.AppPoolName); + break; + } + Thread.Sleep(2000); + + if (verifyTimeout) + { + // Wait for shutdown delay additionally + Thread.Sleep(timeoutValue * 1000); + } + + switch (shutDownMode) + { + case DoAppVerifierTest_ShutDownMode.CreateAppOfflineHtm: + // verify app_offline.htm file works + await VerifyResponseBody(testSite.AspNetCoreApp.GetUri(), "test" + "\r\n", HttpStatusCode.ServiceUnavailable); + + // remove app_offline.htm file and then recycle apppool + testSite.AspNetCoreApp.MoveFile("App_Offline.Htm", "_App_Offline.Htm"); + iisConfig.RecycleAppPool(testSite.AspNetCoreApp.AppPoolName); + Thread.Sleep(2000); + break; + } + + // verify windbg process is gone, which means there was no unexpected error + TestUtility.RunPowershellScript("(get-process -name windbg 2> $null).count", "0", retryCount: 5); + } + + // clean up https test environment + + // Remove the SSL Certificate mapping + iisConfig.RemoveSSLCertificate(sslPort, hexIPAddress); + + // Remove the newly created self signed certificate + iisConfig.DeleteCertificate(thumbPrint); + + // Remove the exported self signed certificate on rootCA + iisConfig.DeleteCertificate(thumbPrint, @"Cert:\LocalMachine\Root"); + } + + // cleanup + if (testSite != null) + { + testSite.DetachAppverifier(); + } + TestUtility.ResetHelper(ResetHelperMode.KillWorkerProcess); + + // cleanup windbg process incase it is still running + if (testResult == false) + { + TestUtility.RunPowershellScript("stop-process -Name windbg -Force -Confirm:$false 2> $null"); } } @@ -1561,11 +2078,11 @@ namespace AspNetCoreModule.Test Assert.Null(response.Headers.ConnectionClose); Assert.Null(GetContentLength(response)); } - catch (XunitException) + catch (XunitException ex) { TestUtility.LogInformation(response.ToString()); TestUtility.LogInformation(responseText); - throw; + throw ex; } } @@ -1786,7 +2303,7 @@ namespace AspNetCoreModule.Test } foreach (string item in expectedStringsInResponseBody) { - Assert.True(responseText.Contains(item)); + Assert.Contains(item, responseText); } } Assert.Equal(expectedResponseStatus, response.StatusCode); diff --git a/test/AspNetCoreModule.Test/WebSocketClientHelper/Frame.cs b/test/AspNetCoreModule.Test/WebSocketClientHelper/Frame.cs index d6c987fa4b..3f0fa9b755 100644 --- a/test/AspNetCoreModule.Test/WebSocketClientHelper/Frame.cs +++ b/test/AspNetCoreModule.Test/WebSocketClientHelper/Frame.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Text; + namespace AspNetCoreModule.Test.WebSocketClient { public class Frame { private int startingIndex; // This will be initialized as output parameter of GetFrameString() - public int DataLength; // This will be initialized as output parameter of GetFrameString() + public int DataLength = 0; // This will be initialized as output parameter of GetFrameString() public Frame(byte[] data) { @@ -18,6 +20,18 @@ namespace AspNetCoreModule.Test.WebSocketClient public FrameType FrameType { get; set; } public byte[] Data { get; private set; } + + public string TextData { + get + { + if (DataLength == 0) + { + throw new System.Exception("DataLength is zero"); + } + return Encoding.ASCII.GetString(Data, startingIndex, DataLength); + } + } + public string Content { get; private set; } public bool IsMasked { get; private set; } diff --git a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketClientHelper.cs b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketClientHelper.cs index 401818ce99..ebd32f5ff0 100644 --- a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketClientHelper.cs +++ b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketClientHelper.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Text; using System.Collections; using System.Threading; -using Xunit; namespace AspNetCoreModule.Test.WebSocketClient { @@ -33,6 +32,34 @@ namespace AspNetCoreModule.Test.WebSocketClient } } + public bool WaitForWebSocketState(WebSocketState expectedState, int timeout = 3000) + { + bool result = false; + int RETRYMAX = 300; + int INTERVAL = 100; // ms + if (timeout > RETRYMAX * INTERVAL) + { + throw new Exception("timeout should be less than " + 100 * 300); + } + for (int i=0; i timeout) + { + break; + } + if (this.WebSocketState == expectedState) + { + result = true; + break; + } + else + { + Thread.Sleep(INTERVAL); + } + } + return result; + } + public Frame Connect(Uri address, bool storeData, bool isAlwaysReading) { Address = address; @@ -44,7 +71,11 @@ namespace AspNetCoreModule.Test.WebSocketClient InitiateWithAlwaysReading(); } SendWebSocketRequest(WebSocketClientUtility.WebSocketVersion); - Thread.Sleep(3000); + + if (!WaitForWebSocketState(WebSocketState.ConnectionOpen)) + { + throw new Exception("Failed to open a connection"); + } Frame openingFrame = null; @@ -53,23 +84,21 @@ namespace AspNetCoreModule.Test.WebSocketClient else openingFrame = Connection.DataReceived[0]; - Thread.Sleep(1000); - IsOpened = true; return openingFrame; } - + public Frame Close() { CloseConnection(); - Thread.Sleep(1000); - + Frame closeFrame = null; if (!IsAlwaysReading) closeFrame = ReadData(); else closeFrame = Connection.DataReceived[Connection.DataReceived.Count - 1]; + IsOpened = false; return closeFrame; } @@ -137,7 +166,7 @@ namespace AspNetCoreModule.Test.WebSocketClient Encoding.UTF8.GetString(outputData, 0, outputData.Length)); } } - + public void ReadDataCallback(IAsyncResult result) { WebSocketConnect client = (WebSocketConnect) result.AsyncState; @@ -190,28 +219,17 @@ namespace AspNetCoreModule.Test.WebSocketClient // Create frame with the tempBuffer Frame frame = new Frame(tempBuffer); - int nextFrameIndex = 0; + ProcessReceivedData(frame); + int nextFrameIndex = frame.IndexOfNextFrame; + while (nextFrameIndex != -1) { - TestUtility.LogInformation("ReadDataCallback: Client {0:D3}: Read Type {1} : {2} ", Connection.Id, frame.FrameType, bytesReadIntotal); + tempBuffer = tempBuffer.SubArray(frame.IndexOfNextFrame, tempBuffer.Length - frame.IndexOfNextFrame); + frame = new Frame(tempBuffer); ProcessReceivedData(frame); - - // Send Pong if the frame was Ping - if (frame.FrameType == FrameType.Ping) - SendPong(frame); - nextFrameIndex = frame.IndexOfNextFrame; - if (nextFrameIndex != -1) - { - tempBuffer = tempBuffer.SubArray(frame.IndexOfNextFrame, tempBuffer.Length - frame.IndexOfNextFrame); - frame = new Frame(tempBuffer); - } } - // Send Pong if the frame was Ping - if (frame.FrameType == FrameType.Ping) - SendPong(frame); - if (client.IsDisposed) return; @@ -271,6 +289,10 @@ namespace AspNetCoreModule.Test.WebSocketClient { Send(Frames.PONG); } + public void SendClose() + { + Send(Frames.CLOSE_FRAME); + } public void SendPong(Frame receivedPing) { @@ -291,8 +313,13 @@ namespace AspNetCoreModule.Test.WebSocketClient public void CloseConnection() { - Connection.Done = true; + this.WebSocketState = WebSocketState.ClosingFromClientStarted; Send(Frames.CLOSE_FRAME); + + if (!WaitForWebSocketState(WebSocketState.ConnectionClosed)) + { + throw new Exception("Failed to close a connection"); + } } public static void WriteCallback(IAsyncResult result) @@ -336,12 +363,45 @@ namespace AspNetCoreModule.Test.WebSocketClient private void ProcessReceivedData(Frame frame) { - if (frame.Content.Contains("Connection: Upgrade") - && frame.Content.Contains("Upgrade: Websocket") - && frame.Content.Contains("HTTP/1.1 101 Switching Protocols")) - WebSocketState = WebSocketState.ConnectionOpen; - if (frame.FrameType == FrameType.Close) - WebSocketState = WebSocketState.ConnectionClosed; + TestUtility.LogInformation("ReadDataCallback: Client {0:D3}: Read Type {1} : {2} ", Connection.Id, frame.FrameType, frame.DataLength); + if (frame.FrameType == FrameType.NonControlFrame) + { + string content = frame.Content.ToLower(); + if (content.Contains("connection: upgrade") + && content.Contains("upgrade: websocket") + && content.Contains("http/1.1 101 switching protocols")) + { + TestUtility.LogInformation("Connection opened..."); + TestUtility.LogInformation(frame.Content); + WebSocketState = WebSocketState.ConnectionOpen; + } + } + else + { + // Send Pong if the frame was Ping + if (frame.FrameType == FrameType.Ping) + SendPong(frame); + + // Send Close if the frame was Close + if (frame.FrameType == FrameType.Close) + { + if (WebSocketState == WebSocketState.ConnectionClosed) + { + throw new Exception("Connection was already closed"); + } + else + { + if (WebSocketState != WebSocketState.ClosingFromClientStarted) + { + TestUtility.LogInformation("Send back Close frame to responsd server closing..."); + SendClose(); + } + TestUtility.LogInformation(frame.Content); + WebSocketState = WebSocketState.ConnectionClosed; + IsOpened = false; + } + } + } ProcessData(frame, false); } diff --git a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketConnect.cs b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketConnect.cs index 69cd3fd5a6..284b65e32a 100644 --- a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketConnect.cs +++ b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketConnect.cs @@ -35,9 +35,7 @@ namespace AspNetCoreModule.Test.WebSocketClient public byte[] InputData { get; set; } public bool IsDisposed { get; set; } - public bool Done { get; set; } - - + public int Id { get; set; } public MyTcpClient TcpClient { get; set; } diff --git a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketState.cs b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketState.cs index b9a4d8c5db..0c25d95f7b 100644 --- a/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketState.cs +++ b/test/AspNetCoreModule.Test/WebSocketClientHelper/WebSocketState.cs @@ -7,6 +7,7 @@ namespace AspNetCoreModule.Test.WebSocketClient { NonWebSocket, ConnectionOpen, + ClosingFromClientStarted, ConnectionClosed } } diff --git a/test/AspNetCoreModule.TestSites.Standard/Program.cs b/test/AspNetCoreModule.TestSites.Standard/Program.cs index 35e934da14..ca854a8e2b 100644 --- a/test/AspNetCoreModule.TestSites.Standard/Program.cs +++ b/test/AspNetCoreModule.TestSites.Standard/Program.cs @@ -15,6 +15,7 @@ namespace AspnetCoreModule.TestSites.Standard public static class Program { public static IApplicationLifetime AappLifetime; + public static bool AappLifetimeStopping = false; public static int GracefulShutdownDelayTime = 0; private static X509Certificate2 _x509Certificate2; @@ -153,16 +154,21 @@ namespace AspnetCoreModule.TestSites.Standard } AappLifetime.ApplicationStarted.Register( - () => Thread.Sleep(1000) + () => { + Thread.Sleep(1000); + } ); AappLifetime.ApplicationStopping.Register( - () => Thread.Sleep(Startup.SleeptimeWhileClosing / 2) + () => { + AappLifetimeStopping = true; + Thread.Sleep(Startup.SleeptimeWhileClosing / 2); + } ); AappLifetime.ApplicationStopped.Register( () => { Thread.Sleep(Startup.SleeptimeWhileClosing / 2); Startup.SleeptimeWhileClosing = 0; // All of SleeptimeWhileClosing is used now - } + } ); try { diff --git a/test/AspNetCoreModule.TestSites.Standard/Startup.cs b/test/AspNetCoreModule.TestSites.Standard/Startup.cs index 4a69fc9fe9..18981450c2 100644 --- a/test/AspNetCoreModule.TestSites.Standard/Startup.cs +++ b/test/AspNetCoreModule.TestSites.Standard/Startup.cs @@ -32,17 +32,36 @@ namespace AspnetCoreModule.TestSites.Standard // the below line is not required at present, however keeping in case the default value is changed later. options.ForwardWindowsAuthentication = true; }); - } + } private async Task Echo(WebSocket webSocket) { var buffer = new byte[1024 * 4]; var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + bool closeFromServer = false; + while (!result.CloseStatus.HasValue) { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + if ((result.Count == "CloseFromServer".Length && System.Text.Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == "CloseFromServer") + || Program.AappLifetimeStopping == true) + { + // start closing handshake from backend process + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "ClosingFromServer", CancellationToken.None); + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + } + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); } + + if (closeFromServer) + { + return; + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); } diff --git a/test/WebRoot/WebSocket/chatplus.html b/test/WebRoot/WebSocket/chatplus.html new file mode 100644 index 0000000000..ca903f4e63 --- /dev/null +++ b/test/WebRoot/WebSocket/chatplus.html @@ -0,0 +1,366 @@ + + + + + + + + + + + +

WebSocket Sample Application

+

Ready to connect...

+
+ + + +
+

+
+ + + + + +
+

Communication Log

+ + + + + + + + + +
FromToData
+ + + + + + \ No newline at end of file diff --git a/test/WebRoot/WebSocket/images/select_arrow.gif b/test/WebRoot/WebSocket/images/select_arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..781c8bc4333fa6ab61b68d7155d224d3bace2155 GIT binary patch literal 636 zcmd6kO>fd*0DxaUq_CBYFwJlqfyq?1sSDG?mK_{n>eP7k>`vBwd%8D^xk*i8s9Eh@Pq zH@+}!$MLD55r@OvhYdKV@6QjtQK@HU`m~sB^tDk%jB7O!4K}!i2ZG^f z%sO>%#6TM~CX-tM-sBm4?8dY4iN^ZRr{=S-9&WgkOLj2-7gZe*Q~&?~ literal 0 HcmV?d00001 diff --git a/test/WebRoot/WebSocket/images/select_arrow_down.gif b/test/WebRoot/WebSocket/images/select_arrow_down.gif new file mode 100644 index 0000000000000000000000000000000000000000..0be8124c2004e7b8b4ae267af4feb9a6a9a3fe44 GIT binary patch literal 650 zcmZ?wbhEHb^?u*^w_6UqTe;^`Uf;*c=^xwXewelX zSsh!H3d0A9tSqT;Fx8b<)l1j$6AA-D#NiamtGKm+rmZe(YoEjF0QL z-7ast$v|JA_>%=}p$>=y#R&uZ^M+tmWkoqTu|Q*K>42c9NL4qr2oVuYUptmr9wB_n zYJOZ67Q9Xp%R|}ibQMjlxp;ZSZP>ONakC4oUB7YjR<`Xs<=nZr?Cr%pC3(*9a0`fu zSjsZ6UXzky;JAI^62txL_iu3s7;DO2dGN?VNl90l(bksr(R~9M8D@Vg#=n0baSHj& z_;7&DzMhedfkR`$0$w#yEtU=q2DS-2k`^MBiVxWs85LM%IE)rNY37i3sNm6@w8V3Y zLWqFHh6WZ+7A3z8363coTvA5j1`!({9cdO93|O?`QPRn&5`2rSIusXbaY*S+VPRsh F1^_uT4paaD literal 0 HcmV?d00001 diff --git a/test/WebRoot/WebSocket/images/select_arrow_over.gif b/test/WebRoot/WebSocket/images/select_arrow_over.gif new file mode 100644 index 0000000000000000000000000000000000000000..0620571c8179977789c9b41fb9055fcdb34f4f3c GIT binary patch literal 639 zcmZ?wbhEHbV|w|}p^{&V5!pHq+i zo_qE8z>`16UjM)G_Rq^-|F69KGkg7m>1!TLT>bFT=f9U;{X6vN&(6EQ=WKd<|I@$6 zU;g*5e0cip|95}>KmGQ9>e{DEcRs)H;!oGI#}igPnX&HSkr)4NfBZjf&HcTP|K54? zw`cji8?XLsIsSUVmZy`}K6?1+-_=)t?|=9+echvDkN$r9|NqjPe}`WEU%l_;-Uq*X zS3WuM`1g#BPxhUD_UYgM$;%Z*_?j61R z=g|3AJ8%6yeD?YI+i&J>2Ksd{0>z&!U}x!oNKl+Gu-|F$(J@!h)s?U|l9zW;R`S$w zGLhxymy?oIQ{xk06EZik<(Hbl#Hg(;z{#efFhzuEqb4WoR#rB_b^IGPZP~WNS58in z{U|#t7Z>wMK?xCkCWeio%*+hNSI?ejxWjbi4#Q0&CPTvqq7PVZsK_6C@qmHl6N`#6 z3#e0>Fs0p1Mz$ud_qGTJ`ul?o4Mo~ H85pbqDA^8^ literal 0 HcmV?d00001 diff --git a/test/WebSocketClientEXE/App.config b/test/WebSocketClientEXE/App.config new file mode 100644 index 0000000000..8324aa6ff1 --- /dev/null +++ b/test/WebSocketClientEXE/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/WebSocketClientEXE/Program.cs b/test/WebSocketClientEXE/Program.cs new file mode 100644 index 0000000000..ea45603a33 --- /dev/null +++ b/test/WebSocketClientEXE/Program.cs @@ -0,0 +1,53 @@ +using AspNetCoreModule.Test.Framework; +using AspNetCoreModule.Test.WebSocketClient; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace WebSocketClientEXE +{ + class Program + { + static void Main(string[] args) + { + using (WebSocketClientHelper websocketClient = new WebSocketClientHelper()) + { + if (args.Length == 0) + { + TestUtility.LogInformation("Usage: WebSocketClientEXE http://localhost:40000/aspnetcoreapp/websocket"); + return; + } + string url = "http://localhost:40000/aspnetcoreapp/websocket"; + if (args[0].Contains("http:")) + { + url = args[0]; + } + var frameReturned = websocketClient.Connect(new Uri(url), true, true); + TestUtility.LogInformation(frameReturned.Content); + TestUtility.LogInformation("Type any data and Enter key ('Q' to quit): "); + + while (true) + { + Thread.Sleep(500); + if (!websocketClient.IsOpened) + { + TestUtility.LogInformation("Connection closed..."); + break; + } + + string data = Console.ReadLine(); + if (data.Trim().ToLower() == "q") + { + frameReturned = websocketClient.Close(); + TestUtility.LogInformation(frameReturned.Content); + break; + } + websocketClient.SendTextData(data); + } + } + } + } +} diff --git a/test/WebSocketClientEXE/Properties/AssemblyInfo.cs b/test/WebSocketClientEXE/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ed897b706e --- /dev/null +++ b/test/WebSocketClientEXE/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WebSocketClientEXE")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WebSocketClientEXE")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4062ea94-75f5-4691-86dc-c8594ba896de")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/WebSocketClientEXE/TestUtility.cs b/test/WebSocketClientEXE/TestUtility.cs new file mode 100644 index 0000000000..852bda62bc --- /dev/null +++ b/test/WebSocketClientEXE/TestUtility.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace AspNetCoreModule.Test.Framework +{ + public class TestUtility + { + public static void LogInformation(string format, params object[] parameters) + { + Console.WriteLine(format, parameters); + } + } +} \ No newline at end of file diff --git a/test/WebSocketClientEXE/WebSocketClientEXE.csproj b/test/WebSocketClientEXE/WebSocketClientEXE.csproj new file mode 100644 index 0000000000..67a5fa5efe --- /dev/null +++ b/test/WebSocketClientEXE/WebSocketClientEXE.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {4062EA94-75F5-4691-86DC-C8594BA896DE} + Exe + WebSocketClientEXE + WebSocketClientEXE + v4.6 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + Frame.cs + + + Frames.cs + + + FrameType.cs + + + WebSocketClientHelper.cs + + + WebSocketClientUtility.cs + + + WebSocketConnect.cs + + + WebSocketConstants.cs + + + WebSocketState.cs + + + + + + + + + + \ No newline at end of file