Skip to content

Commit

Permalink
Add support for record/playback on tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adstep committed Dec 17, 2022
1 parent f0fd024 commit 16ca9af
Show file tree
Hide file tree
Showing 74 changed files with 6,236 additions and 441 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ dotnet_analyzer_diagnostic.category-CodeQuality.severity = warning
# IDE0011: Add braces to 'if' statement
dotnet_diagnostic.IDE0011.severity = silent

# IDE0045: 'if' statement can be simplified
dotnet_diagnostic.IDE0045.severity = silent

# IDE0046: 'if' statement can be simplified
dotnet_diagnostic.IDE0046.severity = silent

Expand All @@ -233,6 +236,9 @@ dotnet_diagnostic.IDE0058.severity = silent
# IDE0065: Using directives must be placed outside of a namespace declaration
dotnet_diagnostic.IDE0065.severity = silent

# IDE0078: Use pattern matching
dotnet_diagnostic.IDE0078.severity = silent

# C++ Files
[*.{cpp,h,in}]
curly_bracket_next_line = true
Expand Down
6 changes: 4 additions & 2 deletions scripts/Generate-Coverage.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
Push-Location

dotnet test "$PSScriptRoot/../src/WinRMSharp.Tests/" --framework net7.0 --collect:"XPlat Code Coverage"
dotnet test "$PSScriptRoot/../src/WinRMSharp.Tests/" --framework net7.0 --filter "Category!=Integration" --collect:"XPlat Code Coverage"

$coverageFiles = Get-ChildItem -Path "$PSScriptRoot/../**/coverage.cobertura.xml" -Recurse
$coverageFiles = $coverageFiles | Sort-Object -Property LastWriteTime -Descending

$latestCoverageFile = $coverageFiles[0]

reportgenerator.exe -reports:$latestCoverageFile -targetdir:"$PSScriptRoot/../coveragereport" -reporttypes:Html
reportgenerator.exe -reports:$latestCoverageFile -targetdir:"$PSScriptRoot/../coveragereport" -reporttypes:Html

Invoke-Expression "$PSScriptRoot/../coveragereport\index.html"
1 change: 1 addition & 0 deletions scripts/Run-Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dotnet test "$PSScriptRoot/../src/WinRMSharp.Tests/" --framework net7.0 --filter "Category!=Integration"
105 changes: 105 additions & 0 deletions src/WinRMSharp.IntegrationTests/BaseProtocolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using Xunit;

namespace WinRMSharp.IntegrationTests
{
public abstract class BaseProtocolTests
{
[Fact]
public async Task OpenCloseShell()
{
Protocol protocol = GenerateProtocol(nameof(OpenCloseShell));

string shellId = await protocol.OpenShell();
await protocol.CloseShell(shellId);

Assert.Matches(@"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", shellId);
}

[Fact]
public async Task RunCommandWithArgsAndCleanup()
{
Protocol protocol = GenerateProtocol(nameof(RunCommandWithArgsAndCleanup));

string shellId = await protocol.OpenShell();
string commandId = await protocol.RunCommand(shellId, "ipconfig", new string[] { "/all" });

await protocol.CloseCommand(shellId, commandId);
await protocol.CloseShell(shellId);

Assert.Matches(@"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", commandId);
}

[Fact]
public async Task RunCommandWithoutArgsAndCleanup()
{
Protocol protocol = GenerateProtocol(nameof(RunCommandWithoutArgsAndCleanup));

string shellId = await protocol.OpenShell();
string commandId = await protocol.RunCommand(shellId, "hostname");

await protocol.CloseCommand(shellId, commandId);
await protocol.CloseShell(shellId);

Assert.Matches(@"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", commandId);
}

[Fact]
public async Task RunCommandWithEnv()
{
Protocol protocol = GenerateProtocol(nameof(RunCommandWithEnv));

Dictionary<string, string> envVars = new Dictionary<string, string>()
{
{ "TESTENV1", "hi mom" },
{ "TESTENV2", "another var" }
};

string shellId = await protocol.OpenShell(envVars: envVars);
string commandId = await protocol.RunCommand(shellId, "echo", new string[] { "%TESTENV1%", "%TESTENV2%" });

CommandState state = await protocol.PollCommandState(shellId, commandId);

await protocol.CloseCommand(shellId, commandId);
await protocol.CloseShell(shellId);

Assert.Matches(@"hi mom another var", state.Stdout);
}

[Fact]
public async Task GetCommandState()
{
Protocol protocol = GenerateProtocol(nameof(GetCommandState));

string shellId = await protocol.OpenShell();
string commandId = await protocol.RunCommand(shellId, "ipconfig", new string[] { "/all" });

CommandState state = await protocol.PollCommandState(shellId, commandId);

await protocol.CloseCommand(shellId, commandId);
await protocol.CloseShell(shellId);

Assert.Equal(0, state.StatusCode);
Assert.Contains(@"Windows IP Configuration", state.Stdout);
Assert.Equal(0, state.Stderr.Length);
}

[Fact]
public async Task RunCommandExceedingOperationTimeout()
{
Protocol protocol = GenerateProtocol(nameof(RunCommandExceedingOperationTimeout));

string shellId = await protocol.OpenShell();
string commandId = await protocol.RunCommand(shellId, $"powershell -Command Start-Sleep -s {protocol.OperationTimeout.TotalSeconds * 2}");

CommandState state = await protocol.PollCommandState(shellId, commandId);

await protocol.CloseCommand(shellId, commandId);
await protocol.CloseShell(shellId);

Assert.Equal(0, state.StatusCode);
Assert.Equal(0, state.Stderr.Length);
}

public abstract Protocol GenerateProtocol(string sessionName);
}
}
39 changes: 39 additions & 0 deletions src/WinRMSharp.IntegrationTests/IntegrationProtocolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Net;
using WinRMSharp.IntegrationTests.Recording;

namespace WinRMSharp.IntegrationTests
{
public class IntegrationProtocolTests : BaseProtocolTests, IDisposable
{
private SessionManager _sessionManager = new SessionManager();

public IntegrationProtocolTests()
{

}

public void Dispose()
{
_sessionManager.Dispose();
}

public override Protocol GenerateProtocol(string sessionName)
{
string baseUrl = Environment.GetEnvironmentVariable("WINRM_BASE_URL") ?? throw new ArgumentNullException("WINRM_BASE_URL");
string username = Environment.GetEnvironmentVariable("WINRM_USERNAME") ?? throw new ArgumentNullException("WINRM_USERNAME");
string password = Environment.GetEnvironmentVariable("WINRM_PASSWORD") ?? throw new ArgumentNullException("WINRM_PASSWORD");

SessionHandler handler = _sessionManager.GenerateSessionHandler(State.Recording, sessionName);

ICredentials credentials = new NetworkCredential(username, password);
ITransport transport = new Transport(baseUrl, handler, credentials);

ProtocolOptions protocolOptions = new ProtocolOptions()
{
OperationTimeout = TimeSpan.FromSeconds(5)
};

return new Protocol(transport, new IncrementingGuidProvider(), protocolOptions);
}
}
}
18 changes: 15 additions & 3 deletions src/WinRMSharp.IntegrationTests/ProtocolTests.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
using System.Net;
using WinRMSharp.IntegrationTests.Recording;
using Xunit;
using Xunit.Abstractions;

namespace WinRMSharp.IntegrationTests
{
public class ProtocolTests
{
private readonly RecordingManager _recordingManager;
private readonly Protocol _protocol;

public ProtocolTests()
public ProtocolTests(ITestOutputHelper outputHelper)
{
string baseUrl = Environment.GetEnvironmentVariable("WINRM_BASE_URL") ?? throw new ArgumentNullException("WINRM_BASE_URL");
string username = Environment.GetEnvironmentVariable("WINRM_USERNAME") ?? throw new ArgumentNullException("WINRM_USERNAME");
string password = Environment.GetEnvironmentVariable("WINRM_PASSWORD") ?? throw new ArgumentNullException("WINRM_PASSWORD");

_recordingManager = new RecordingManager();
_recordingManager.Load("example");

ICredentials credentials = new NetworkCredential(username, password);
ITransport transport = new Transport(baseUrl, credentials);
ITransport transport = new Transport(baseUrl, _recordingManager, credentials);

ProtocolOptions protocolOptions = new ProtocolOptions()
{
OperationTimeout = TimeSpan.FromSeconds(5)
};

_protocol = new Protocol(transport, protocolOptions);
_protocol = new Protocol(transport, new IncrementingGuidProvider(), protocolOptions);
_protocol.Transport.OnMessage += (string message) =>
{
outputHelper.WriteLine(message);
};
}


Expand All @@ -31,6 +41,8 @@ public async Task OpenCloseShell()
string shellId = await _protocol.OpenShell();
await _protocol.CloseShell(shellId);

_recordingManager.Save();

Assert.Matches(@"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", shellId);
}

Expand Down
10 changes: 10 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/PassThruHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace WinRMSharp.IntegrationTests.Recording
{
internal class PassThruHandler : SessionHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken);
}
}
}
64 changes: 64 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/PlaybackHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Net;
using System.Xml.Linq;
using WinRMSharp.Utils;
using YamlDotNet.Serialization;

namespace WinRMSharp.IntegrationTests.Recording
{
internal class PlaybackHandler : SessionHandler
{
private int _playbackIndex = 0;
private readonly List<Recording> _recordings = new List<Recording>();

private PlaybackHandler(List<Recording> recordings)
{
_recordings = recordings;
}

public static PlaybackHandler Load(string sessionName)
{
string path = Path.Join("sessions", $"{sessionName}.yml");

if (!File.Exists(path))
{
throw new FileNotFoundException($"Expected recording to exist at '{path}'");
}

using StreamReader fileStream = File.OpenText(path);

IDeserializer deserializer = new DeserializerBuilder().Build();
List<Recording> recordings = deserializer.Deserialize<List<Recording>>(fileStream);

return new PlaybackHandler(recordings);
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Recording recording = _recordings[_playbackIndex++];

XDocument? requestDocument = Xml.Parse(await request.Content!.ReadAsStringAsync(cancellationToken));
XDocument? recordingDocument = Xml.Parse(recording.Request.Body);

if (!XNode.DeepEquals(requestDocument, recordingDocument))
{
throw new InvalidOperationException($"Expected '{requestDocument}' Actual: '{recordingDocument}'");
}

HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)recording.Response.StatusCode)
{
Content = new StringContent(recording.Response.Body!)
};

foreach (KeyValuePair<string, string> kv in recording.Response.Headers)
{
string key = kv.Key;
string[] values = kv.Value.Split(';');

response.Headers.Add(key, values);
}

return response;
}

}
}
44 changes: 44 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/RecordedRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace WinRMSharp.IntegrationTests.Recording
{
public class RecordedRequest
{
public string Method { get; set; }
public string? Url { get; set; }
public Dictionary<string, string> Headers { get; set; }
public string? Body { get; set; }

public RecordedRequest()
{
Method = string.Empty;
Url = string.Empty;
Headers = new Dictionary<string, string>();
Body = string.Empty;
}

public RecordedRequest(HttpRequestMessage requestMessage)
{
Method = requestMessage.Method.Method;
Url = requestMessage.RequestUri?.ToString();
Headers = ApplyFilters(requestMessage.Headers.ToDictionary(h => h.Key, h => string.Join(";", h.Value)));
Body = requestMessage.Content?.ReadAsStringAsync().Result;
}

private static Dictionary<string, string> ApplyFilters(Dictionary<string, string> headers)
{
HashSet<string> headersToFilter = new HashSet<string>()
{
"Authorization"
};

foreach (string headerToFilter in headersToFilter)
{
if (!headers.ContainsKey(headerToFilter))
continue;

headers.Remove(headerToFilter);
}

return headers;
}
}
}
20 changes: 20 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/RecordedResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using WinRMSharp.Utils;

namespace WinRMSharp.IntegrationTests.Recording
{
internal class RecordedResponse
{
public int StatusCode { get; set; }
public Dictionary<string, string> Headers { get; set; }
public string? Body { get; set; }

public RecordedResponse() { }

public RecordedResponse(HttpResponseMessage responseMessage)
{
StatusCode = (int)responseMessage.StatusCode;
Headers = responseMessage.Headers.ToDictionary(h => h.Key, h => string.Join(";", h.Value));
Body = Xml.Format(responseMessage.Content?.ReadAsStringAsync().Result);
}
}
}
8 changes: 8 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/RecordedSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace WinRMSharp.IntegrationTests.Recording
{
internal class RecordedSession
{
public string Name { get; set; }
public List<Recording> Recordings { get; set; }
}
}
8 changes: 8 additions & 0 deletions src/WinRMSharp.IntegrationTests/Recording/Recording.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace WinRMSharp.IntegrationTests.Recording
{
internal class Recording
{
public RecordedRequest Request { get; set; }
public RecordedResponse Response { get; set; }
}
}
Loading

0 comments on commit 16ca9af

Please sign in to comment.