2018-03-20 16:03:28 +11:00
using k8s.Tests.Logging ;
using k8s.Tests.Mock.Server ;
using Microsoft.AspNetCore ;
using Microsoft.AspNetCore.Hosting ;
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
2022-02-25 13:33:23 -08:00
using k8s.Autorest ;
2018-03-20 16:03:28 +11:00
using System ;
using System.IO ;
using System.Net.WebSockets ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using Xunit ;
using Xunit.Abstractions ;
namespace k8s.Tests
{
/// <summary>
/// The base class for Kubernetes WebSocket test suites.
/// </summary>
2018-04-28 05:40:47 +02:00
public abstract class WebSocketTestBase : IDisposable
2018-03-20 16:03:28 +11:00
{
/// <summary>
/// The next server port to use.
/// </summary>
2020-10-23 08:31:57 -07:00
private static int nextPort = 13255 ;
private bool disposedValue ;
2018-04-28 05:40:47 +02:00
private readonly ITestOutputHelper testOutput ;
2018-03-20 16:03:28 +11:00
/// <summary>
2020-11-22 14:52:09 -08:00
/// Initializes a new instance of the <see cref="WebSocketTestBase"/> class.
2018-03-20 16:03:28 +11:00
/// Create a new <see cref="WebSocketTestBase"/>.
/// </summary>
/// <param name="testOutput">
/// Output for the current test.
/// </param>
protected WebSocketTestBase ( ITestOutputHelper testOutput )
{
2018-04-28 05:40:47 +02:00
this . testOutput = testOutput ;
2020-10-23 08:31:57 -07:00
int port = Interlocked . Increment ( ref nextPort ) ;
2018-03-20 16:03:28 +11:00
// Useful to diagnose test timeouts.
TestCancellation . Register (
2020-04-23 11:40:06 -07:00
( ) = > testOutput . WriteLine ( "Test-level cancellation token has been canceled." ) ) ;
2018-03-20 16:03:28 +11:00
ServerBaseAddress = new Uri ( $"http://localhost:{port}" ) ;
WebSocketBaseAddress = new Uri ( $"ws://localhost:{port}" ) ;
Host = WebHost . CreateDefaultBuilder ( )
. UseStartup < Startup > ( )
. ConfigureServices ( ConfigureTestServerServices )
. ConfigureLogging ( ConfigureTestServerLogging )
. UseUrls ( ServerBaseAddress . AbsoluteUri )
. Build ( ) ;
}
/// <summary>
/// The test server's base address (http://).
/// </summary>
protected Uri ServerBaseAddress { get ; }
/// <summary>
/// The test server's base WebSockets address (ws://).
/// </summary>
protected Uri WebSocketBaseAddress { get ; }
/// <summary>
/// The test server's web host.
/// </summary>
protected IWebHost Host { get ; }
/// <summary>
/// Test adapter for accepting web sockets.
/// </summary>
protected WebSocketTestAdapter WebSocketTestAdapter { get ; } = new WebSocketTestAdapter ( ) ;
/// <summary>
/// The source for cancellation tokens used by the test.
/// </summary>
protected CancellationTokenSource CancellationSource { get ; } = new CancellationTokenSource ( ) ;
/// <summary>
2020-11-22 14:52:09 -08:00
/// A <see cref="CancellationToken"/> that can be used to cancel asynchronous operations.
2018-03-20 16:03:28 +11:00
/// </summary>
/// <seealso cref="CancellationSource"/>
protected CancellationToken TestCancellation = > CancellationSource . Token ;
/// <summary>
/// Configure services for the test server.
/// </summary>
/// <param name="services">
/// The service collection to configure.
/// </param>
protected virtual void ConfigureTestServerServices ( IServiceCollection services )
{
if ( services = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( services ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
// Inject WebSocketTestData.
services . AddSingleton ( WebSocketTestAdapter ) ;
}
/// <summary>
/// Configure logging for the test server.
/// </summary>
/// <param name="services">
/// The logger factory to configure.
/// </param>
protected virtual void ConfigureTestServerLogging ( ILoggingBuilder logging )
{
if ( logging = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( logging ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
logging . ClearProviders ( ) ; // Don't log to console.
2020-11-22 14:52:09 -08:00
logging . AddTestOutput ( testOutput , LogLevel . Information ) ;
2018-03-20 16:03:28 +11:00
}
/// <summary>
/// Create a Kubernetes client that uses the test server.
/// </summary>
/// <param name="credentials">
/// Optional <see cref="ServiceClientCredentials"/> to use for authentication (defaults to anonymous, i.e. no credentials).
/// </param>
/// <returns>
/// The configured client.
/// </returns>
protected virtual Kubernetes CreateTestClient ( ServiceClientCredentials credentials = null )
{
2022-02-25 13:33:23 -08:00
return new Kubernetes ( new KubernetesClientConfiguration ( )
{
Host = ServerBaseAddress . ToString ( ) ,
} ) ;
2018-03-20 16:03:28 +11:00
}
/// <summary>
/// Asynchronously disconnect client and server WebSockets using the standard handshake.
/// </summary>
/// <param name="clientSocket">
/// The client-side <see cref="WebSocket"/>.
/// </param>
/// <param name="serverSocket">
/// The server-side <see cref="WebSocket"/>.
/// </param>
/// <param name="closeStatus">
/// An optional <see cref="WebSocketCloseStatus"/> value indicating the reason for disconnection.
///
/// Defaults to <see cref="WebSocketCloseStatus.NormalClosure"/>.
/// </param>
/// <param name="closeStatusDescription">
/// An optional textual description of the reason for disconnection.
///
/// Defaults to "Normal Closure".
/// </param>
/// <returns>
/// A <see cref="Task"/> representing the asynchronous operation.
/// </returns>
2020-04-23 11:40:06 -07:00
protected async Task Disconnect ( WebSocket clientSocket , WebSocket serverSocket ,
WebSocketCloseStatus closeStatus = WebSocketCloseStatus . NormalClosure ,
string closeStatusDescription = "Normal Closure" )
2018-03-20 16:03:28 +11:00
{
if ( clientSocket = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( clientSocket ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
if ( serverSocket = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( serverSocket ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Disconnecting..." ) ;
2018-03-20 16:03:28 +11:00
// Asynchronously perform the server's half of the handshake (the call to clientSocket.CloseAsync will block until it receives the server-side response).
ArraySegment < byte > receiveBuffer = new byte [ 1024 ] ;
Task closeServerSocket = serverSocket . ReceiveAsync ( receiveBuffer , TestCancellation )
. ContinueWith ( async received = >
{
if ( received . IsFaulted )
2020-04-23 11:40:06 -07:00
{
2020-11-01 12:24:51 -08:00
testOutput . WriteLine (
"Server socket operation to receive Close message failed: {0}" ,
2020-04-23 11:40:06 -07:00
received . Exception . Flatten ( ) . InnerExceptions [ 0 ] ) ;
}
2018-03-20 16:03:28 +11:00
else if ( received . IsCanceled )
2020-04-23 11:40:06 -07:00
{
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Server socket operation to receive Close message was canceled." ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
else
{
2020-04-23 11:40:06 -07:00
testOutput . WriteLine (
$"Received {received.Result.MessageType} message from server socket (expecting {WebSocketMessageType.Close})." ) ;
2018-03-20 16:03:28 +11:00
if ( received . Result . MessageType = = WebSocketMessageType . Close )
{
2020-04-23 11:40:06 -07:00
testOutput . WriteLine (
$"Closing server socket (with status {received.Result.CloseStatus})..." ) ;
2018-03-20 16:03:28 +11:00
await serverSocket . CloseAsync (
received . Result . CloseStatus . Value ,
received . Result . CloseStatusDescription ,
2020-10-23 08:31:57 -07:00
TestCancellation ) . ConfigureAwait ( false ) ;
2018-03-20 16:03:28 +11:00
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Server socket closed." ) ;
2018-03-20 16:03:28 +11:00
}
Assert . Equal ( WebSocketMessageType . Close , received . Result . MessageType ) ;
}
} ) ;
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Closing client socket..." ) ;
2018-03-20 16:03:28 +11:00
await clientSocket . CloseAsync ( closeStatus , closeStatusDescription , TestCancellation ) . ConfigureAwait ( false ) ;
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Client socket closed." ) ;
2018-03-20 16:03:28 +11:00
await closeServerSocket . ConfigureAwait ( false ) ;
2018-04-28 05:40:47 +02:00
testOutput . WriteLine ( "Disconnected." ) ;
2018-03-20 16:03:28 +11:00
Assert . Equal ( closeStatus , clientSocket . CloseStatus ) ;
Assert . Equal ( clientSocket . CloseStatus , serverSocket . CloseStatus ) ;
Assert . Equal ( closeStatusDescription , clientSocket . CloseStatusDescription ) ;
Assert . Equal ( clientSocket . CloseStatusDescription , serverSocket . CloseStatusDescription ) ;
}
/// <summary>
/// Send text to a multiplexed substream over the specified WebSocket.
/// </summary>
/// <param name="webSocket">
/// The target <see cref="WebSocket"/>.
/// </param>
/// <param name="streamIndex">
/// The 0-based index of the target substream.
/// </param>
/// <param name="text">
/// The text to send.
/// </param>
/// <returns>
/// The number of bytes sent to the WebSocket.
/// </returns>
protected async Task < int > SendMultiplexed ( WebSocket webSocket , byte streamIndex , string text )
{
if ( webSocket = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( webSocket ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
if ( text = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( text ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
byte [ ] payload = Encoding . ASCII . GetBytes ( text ) ;
byte [ ] sendBuffer = new byte [ payload . Length + 1 ] ;
sendBuffer [ 0 ] = streamIndex ;
Array . Copy ( payload , 0 , sendBuffer , 1 , payload . Length ) ;
await webSocket . SendAsync ( sendBuffer , WebSocketMessageType . Binary ,
2020-11-22 14:52:09 -08:00
true ,
TestCancellation ) . ConfigureAwait ( false ) ;
2018-03-20 16:03:28 +11:00
return sendBuffer . Length ;
}
/// <summary>
/// Receive text from a multiplexed substream over the specified WebSocket.
/// </summary>
/// <param name="webSocket">
/// The target <see cref="WebSocket"/>.
/// </param>
/// <param name="text">
/// The text to send.
/// </param>
/// <returns>
/// A tuple containing the received text, 0-based substream index, and total bytes received.
/// </returns>
2020-04-23 11:40:06 -07:00
protected async Task < ( string text , byte streamIndex , int totalBytes ) > ReceiveTextMultiplexed (
WebSocket webSocket )
2018-03-20 16:03:28 +11:00
{
if ( webSocket = = null )
2020-04-23 11:40:06 -07:00
{
2018-03-20 16:03:28 +11:00
throw new ArgumentNullException ( nameof ( webSocket ) ) ;
2020-04-23 11:40:06 -07:00
}
2018-03-20 16:03:28 +11:00
byte [ ] receivedData ;
using ( MemoryStream buffer = new MemoryStream ( ) )
{
byte [ ] receiveBuffer = new byte [ 1024 ] ;
2020-10-23 08:31:57 -07:00
WebSocketReceiveResult receiveResult = await webSocket . ReceiveAsync ( receiveBuffer , TestCancellation ) . ConfigureAwait ( false ) ;
2018-03-20 16:03:28 +11:00
if ( receiveResult . MessageType ! = WebSocketMessageType . Binary )
2020-04-23 11:40:06 -07:00
{
throw new IOException (
$"Received unexpected WebSocket message of type '{receiveResult.MessageType}'." ) ;
}
2018-03-20 16:03:28 +11:00
buffer . Write ( receiveBuffer , 0 , receiveResult . Count ) ;
while ( ! receiveResult . EndOfMessage )
{
2020-10-23 08:31:57 -07:00
receiveResult = await webSocket . ReceiveAsync ( receiveBuffer , TestCancellation ) . ConfigureAwait ( false ) ;
2018-03-20 16:03:28 +11:00
buffer . Write ( receiveBuffer , 0 , receiveResult . Count ) ;
}
buffer . Flush ( ) ;
receivedData = buffer . ToArray ( ) ;
}
return (
text : Encoding . ASCII . GetString ( receivedData , 1 , receivedData . Length - 1 ) ,
streamIndex : receivedData [ 0 ] ,
2020-04-23 11:40:06 -07:00
totalBytes : receivedData . Length ) ;
2018-03-20 16:03:28 +11:00
}
2020-10-23 08:31:57 -07:00
2018-04-28 05:40:47 +02:00
2018-03-20 16:03:28 +11:00
/// <summary>
/// A <see cref="ServiceClientCredentials"/> implementation representing no credentials (i.e. anonymous).
/// </summary>
protected class AnonymousClientCredentials
: ServiceClientCredentials
{
/// <summary>
/// The singleton instance of <see cref="AnonymousClientCredentials"/>.
/// </summary>
public static readonly AnonymousClientCredentials Instance = new AnonymousClientCredentials ( ) ;
/// <summary>
2020-11-22 14:52:09 -08:00
/// Initializes a new instance of the <see cref="AnonymousClientCredentials"/> class.
2018-03-20 16:03:28 +11:00
/// Create new <see cref="AnonymousClientCredentials"/>.
/// </summary>
2020-10-23 08:31:57 -07:00
private AnonymousClientCredentials ( )
2018-03-20 16:03:28 +11:00
{
}
}
/// <summary>
/// Event Id constants used in WebSocket tests.
/// </summary>
protected static class EventIds
{
/// <summary>
/// An error occurred while closing the server-side socket.
/// </summary>
2020-10-23 08:31:57 -07:00
private static readonly EventId ErrorClosingServerSocket = new EventId ( 1000 , nameof ( ErrorClosingServerSocket ) ) ;
}
protected virtual void Dispose ( bool disposing )
{
if ( ! disposedValue )
{
if ( disposing )
{
CancellationSource . Dispose ( ) ;
Host . Dispose ( ) ;
}
disposedValue = true ;
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~WebSocketTestBase()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose ( )
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
2020-11-22 14:52:09 -08:00
Dispose ( true ) ;
2020-10-23 08:31:57 -07:00
GC . SuppressFinalize ( this ) ;
2018-03-20 16:03:28 +11:00
}
}
}