Bladeren bron

Merge pull request #1148 from WojciechNagorski/integration-tests-mstests

MSTest Integration tests
Wojciech Nagórski 2 jaren geleden
bovenliggende
commit
1b46264bd3

+ 32 - 0
src/Renci.SshNet.IntegrationTests/.editorconfig

@@ -0,0 +1,32 @@
+[*.cs]
+
+#### SYSLIB diagnostics ####
+
+# SYSLIB1045: Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time
+#
+# TODO: Remove this when https://github.com/sshnet/SSH.NET/issues/1131 is implemented.
+dotnet_diagnostic.SYSLIB1045.severity = none
+
+### StyleCop Analyzers rules ###
+
+#### .NET Compiler Platform analysers rules ####
+
+# IDE0007: Use var instead of explicit type
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0007
+dotnet_diagnostic.IDE0007.severity = suggestion
+
+# IDE0028: Use collection initializers
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0028
+dotnet_diagnostic.IDE0028.severity = suggestion
+
+# IDE0058: Remove unnecessary expression value
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0058
+dotnet_diagnostic.IDE0058.severity = suggestion
+
+# IDE0059: Remove unnecessary value assignment
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0059
+dotnet_diagnostic.IDE0059.severity = suggestion
+
+# IDE0230: Use UTF-8 string literal
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0230
+dotnet_diagnostic.IDE0230.severity = suggestion

+ 49 - 0
src/Renci.SshNet.IntegrationTests/Dockerfile

@@ -0,0 +1,49 @@
+FROM alpine:latest
+
+COPY --chown=root:root server/ssh /etc/ssh/
+COPY --chown=root:root server/script /opt/sshnet
+COPY user/sshnet /home/sshnet/.ssh
+
+RUN apk update && apk upgrade --no-cache && \
+    apk add --no-cache syslog-ng && \
+    # install and configure sshd
+    apk add --no-cache openssh && \
+    # install openssh-server-pam to allow for keyboard-interactive authentication
+    apk add --no-cache openssh-server-pam && \
+    dos2unix /etc/ssh/* && \
+    chmod 400 /etc/ssh/ssh*key && \
+    sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
+    sed -i 's/#LogLevel\s*INFO/LogLevel DEBUG3/' /etc/ssh/sshd_config && \
+    echo 'PubkeyAcceptedAlgorithms ssh-rsa' >> /etc/ssh/sshd_config && \
+    chmod 646 /etc/ssh/sshd_config && \
+    # install and configure sudo
+    apk add --no-cache sudo && \
+    addgroup sudo && \
+    # allow root to run any command
+    echo 'root ALL=(ALL) ALL' > /etc/sudoers && \
+    # allow everyone in the 'sudo' group to run any command without a password
+    echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
+    # add user to run most of the integration tests
+    adduser -D sshnet && \
+    passwd -u sshnet && \
+    echo 'sshnet:ssh4ever' | chpasswd && \
+    dos2unix /home/sshnet/.ssh/* && \
+    chown -R sshnet:sshnet /home/sshnet && \
+    chmod -R 700 /home/sshnet/.ssh && \
+    chmod -R 644 /home/sshnet/.ssh/authorized_keys && \
+    # add user to administer container (update configs, restart sshd)
+    adduser -D sshnetadm && \
+    passwd -u sshnetadm && \
+    echo 'sshnetadm:ssh4ever' | chpasswd && \
+    addgroup sshnetadm sudo && \
+    dos2unix /opt/sshnet/* && \
+    # install shadow package; we use chage command in this package to expire/unexpire password of the sshnet user
+    apk add --no-cache shadow && \
+    # allow us to use telnet command; we use this in the remote port forwarding tests
+    apk --no-cache add busybox-extras && \
+    # install full-fledged ps command
+    apk add --no-cache procps
+
+EXPOSE 22 22
+
+ENTRYPOINT ["/opt/sshnet/start.sh"]

+ 47 - 0
src/Renci.SshNet.IntegrationTests/IntegrationTests.csproj

@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+
+    <IsPackable>false</IsPackable>
+    <IsTestProject>true</IsTestProject>
+    <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
+    <!--
+          Even though we're not interested in producing XML docs for test projects, we have to enable this in order to have the .NET Compiler
+          Platform analyzers produce the IDE0005 (Remove unnecessary import) diagnostic.
+            
+          To avoid warnings for missing XML docs, we add CS1591 (Missing XML comment for publicly visible type or member) to the NoWarn property.
+
+          We can stop producing XML docs for test projects (and remove the NoWarn for CS1591) once the following issue is fixed:
+          https://github.com/dotnet/roslyn/issues/41640.
+      -->
+    <NoWarn>$(NoWarn);CS1591</NoWarn>
+      
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
+    <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
+    <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
+    <PackageReference Include="Testcontainers" Version="3.4.0" />
+    <PackageReference Include="coverlet.collector" Version="6.0.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Renci.SshNet\Renci.SshNet.csproj">
+        <Aliases>LocalSshNet</Aliases>
+    </ProjectReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="app.config">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 41 - 0
src/Renci.SshNet.IntegrationTests/ScpClientTests.cs

@@ -0,0 +1,41 @@
+namespace IntegrationTests
+{
+    /// <summary>
+    /// The SCP client integration tests
+    /// </summary>
+    [TestClass]
+    public class ScpClientTests : IntegrationTestBase, IDisposable
+    {
+        private readonly ScpClient _scpClient;
+
+        public ScpClientTests()
+        {
+            _scpClient = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
+            _scpClient.Connect();
+        }
+
+        [TestMethod]
+
+        public void Upload_And_Download_FileStream()
+        {
+            var file = $"/tmp/{Guid.NewGuid()}.txt";
+            var fileContent = "File content !@#$%^&*()_+{}:,./<>[];'\\|";
+        
+            using var uploadStream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent));
+            _scpClient.Upload(uploadStream, file);
+
+            using var downloadStream = new MemoryStream();
+            _scpClient.Download(file, downloadStream);
+
+            var result = Encoding.UTF8.GetString(downloadStream.ToArray());
+
+            Assert.AreEqual(fileContent, result);
+        }
+
+        public void Dispose()
+        {
+            _scpClient.Disconnect();
+            _scpClient.Dispose();
+        }
+    }
+}

+ 100 - 0
src/Renci.SshNet.IntegrationTests/SftpClientTests.cs

@@ -0,0 +1,100 @@
+namespace IntegrationTests
+{
+    /// <summary>
+    /// The SFTP client integration tests
+    /// </summary>
+    [TestClass]
+    public class SftpClientTests : IntegrationTestBase, IDisposable
+    {
+        private readonly SftpClient _sftpClient;
+
+        public SftpClientTests()
+        {
+            _sftpClient = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
+            _sftpClient.Connect();
+        }
+
+        [TestMethod]
+        public void Create_directory_with_contents_and_list_it()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+            var testFileName =  "test-file.txt";
+            var testFilePath = $"{testDirectory}/{testFileName}";
+            var testContent = "file content";
+
+            // Create new directory and check if it exists
+            _sftpClient.CreateDirectory(testDirectory);
+            Assert.IsTrue(_sftpClient.Exists(testDirectory));
+
+            // Upload file and check if it exists
+            using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
+            _sftpClient.UploadFile(fileStream, testFilePath);
+            Assert.IsTrue(_sftpClient.Exists(testFilePath));
+
+            // Check if ListDirectory works
+            var files = _sftpClient.ListDirectory(testDirectory);
+
+            _sftpClient.DeleteFile(testFilePath);
+            _sftpClient.DeleteDirectory(testDirectory);
+
+            var builder = new StringBuilder();
+            foreach (var file in files)
+            {
+                builder.AppendLine($"{file.FullName} {file.IsRegularFile} {file.IsDirectory}");
+            }
+
+            Assert.AreEqual(@"/home/sshnet/sshnet-test/. False True
+/home/sshnet/sshnet-test/.. False True
+/home/sshnet/sshnet-test/test-file.txt True False
+", builder.ToString());
+        }
+
+        [TestMethod]
+        public async Task Create_directory_with_contents_and_list_it_async()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+            var testFileName = "test-file.txt";
+            var testFilePath = $"{testDirectory}/{testFileName}";
+            var testContent = "file content";
+
+            // Create new directory and check if it exists
+            _sftpClient.CreateDirectory(testDirectory);
+            Assert.IsTrue(_sftpClient.Exists(testDirectory));
+
+            // Upload file and check if it exists
+            using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
+            _sftpClient.UploadFile(fileStream, testFilePath);
+            Assert.IsTrue(_sftpClient.Exists(testFilePath));
+
+            // Check if ListDirectory works
+            var files = await _sftpClient.ListDirectoryAsync(testDirectory, CancellationToken.None);
+
+            _sftpClient.DeleteFile(testFilePath);
+            _sftpClient.DeleteDirectory(testDirectory);
+
+            var builder = new StringBuilder();
+            foreach (var file in files)
+            {
+                builder.AppendLine($"{file.FullName} {file.IsRegularFile} {file.IsDirectory}");
+            }
+
+            Assert.AreEqual(@"/home/sshnet/sshnet-test/. False True
+/home/sshnet/sshnet-test/.. False True
+/home/sshnet/sshnet-test/test-file.txt True False
+", builder.ToString());
+        }
+
+        [TestMethod]
+        [ExpectedException(typeof(SftpPermissionDeniedException), "Permission denied")]
+        public void Test_Sftp_ListDirectory_Permission_Denied()
+        {
+            _sftpClient.ListDirectory("/root");
+        }
+
+        public void Dispose()
+        {
+            _sftpClient.Disconnect();
+            _sftpClient.Dispose();
+        }
+    }
+}

+ 32 - 0
src/Renci.SshNet.IntegrationTests/SshClientTests.cs

@@ -0,0 +1,32 @@
+namespace IntegrationTests
+{
+    /// <summary>
+    /// The SSH client integration tests
+    /// </summary>
+    [TestClass]
+    public class SshClientTests : IntegrationTestBase, IDisposable
+    {
+        private readonly SshClient _sshClient;
+
+        public SshClientTests()
+        {
+            _sshClient = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
+            _sshClient.Connect();
+        }
+
+        [TestMethod]
+        public void Echo_Command_with_all_characters()
+        {
+            var builder = new StringBuilder();
+            var response = _sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'");
+
+            Assert.AreEqual("test !@#$%^&*()_+{}:,./<>[];\\|\n", response.Result);
+        }
+    
+        public void Dispose()
+        {
+            _sshClient.Disconnect();
+            _sshClient.Dispose();
+        }
+    }
+}

+ 19 - 0
src/Renci.SshNet.IntegrationTests/TestInitializer.cs

@@ -0,0 +1,19 @@
+namespace IntegrationTests
+{
+    [TestClass]
+    public class TestInitializer
+    {
+        [AssemblyInitialize]
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "MSTests requires context parameter")]
+        public static async Task Initialize(TestContext context)
+        {
+            await InfrastructureFixture.Instance.InitializeAsync();
+        }
+
+        [AssemblyCleanup]
+        public static async Task Cleanup()
+        {
+            await InfrastructureFixture.Instance.DisposeAsync();
+        }
+    }
+}

+ 76 - 0
src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs

@@ -0,0 +1,76 @@
+using DotNet.Testcontainers.Images;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace IntegrationTests.TestsFixtures
+{
+    public sealed class InfrastructureFixture : IDisposable
+    {
+        private InfrastructureFixture()
+        {
+        }
+
+        private static readonly Lazy<InfrastructureFixture> InstanceLazy = new Lazy<InfrastructureFixture>(() => new InfrastructureFixture());
+
+        public static InfrastructureFixture Instance
+        {
+            get
+            {
+                return InstanceLazy.Value;
+            }
+        }
+
+        private IContainer? _sshServer;
+
+        private IFutureDockerImage? _sshServerImage;
+
+        public string? SshServerHostName { get; set; }
+
+        public ushort SshServerPort { get; set; }
+
+        public SshUser AdminUser = new SshUser("sshnetadm", "ssh4ever");
+
+        public SshUser User = new SshUser("sshnet", "ssh4ever");
+
+        public async Task InitializeAsync()
+        {
+            _sshServerImage = new ImageFromDockerfileBuilder()
+                .WithName("renci-ssh-tests-server-image")
+                .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), "Renci.SshNet.IntegrationTests")
+                .WithDockerfile("Dockerfile")
+                .WithDeleteIfExists(true)
+                
+                .Build();
+
+            await _sshServerImage.CreateAsync();
+
+            _sshServer = new ContainerBuilder()
+                .WithHostname("renci-ssh-tests-server")
+                .WithImage(_sshServerImage)
+                .WithPortBinding(22, true)
+                .Build();
+
+            await _sshServer.StartAsync();
+
+            SshServerPort = _sshServer.GetMappedPublicPort(22);
+            SshServerHostName = _sshServer.Hostname;
+        }
+
+        public async Task DisposeAsync()
+        {
+            if (_sshServer != null)
+            {
+                await _sshServer.DisposeAsync();
+            }
+
+            if (_sshServerImage != null)
+            {
+                await _sshServerImage.DisposeAsync();
+            }
+        }
+
+        public void Dispose()
+        {
+        }
+    }
+}

+ 66 - 0
src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs

@@ -0,0 +1,66 @@
+namespace IntegrationTests.TestsFixtures
+{
+    /// <summary>
+    /// The base class for integration tests
+    /// </summary>
+    public abstract class IntegrationTestBase
+    {
+        private readonly InfrastructureFixture _infrastructureFixture;
+
+        /// <summary>
+        /// The SSH Server host name.
+        /// </summary>
+        public string? SshServerHostName
+        {
+            get
+            {
+                return _infrastructureFixture.SshServerHostName;
+            }
+        }
+
+        /// <summary>
+        /// The SSH Server host name
+        /// </summary>
+        public ushort SshServerPort
+        {
+            get
+            {
+                return _infrastructureFixture.SshServerPort;
+            }
+        }
+
+        /// <summary>
+        /// The admin user that can use SSH Server.
+        /// </summary>
+        public SshUser AdminUser
+        {
+            get
+            {
+                return _infrastructureFixture.AdminUser;
+            }
+        }
+
+        /// <summary>
+        /// The normal user that can use SSH Server.
+        /// </summary>
+        public SshUser User
+        {
+            get
+            {
+                return _infrastructureFixture.User;
+            }
+        }
+
+        protected IntegrationTestBase()
+        {
+            _infrastructureFixture = InfrastructureFixture.Instance;
+            ShowInfrastructureInformation();
+        }
+
+        private void ShowInfrastructureInformation()
+        {
+            Console.WriteLine($"SSH Server host name: {_infrastructureFixture.SshServerHostName}");
+            Console.WriteLine($"SSH Server port: {_infrastructureFixture.SshServerPort}");
+        }
+    }
+}

+ 16 - 0
src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs

@@ -0,0 +1,16 @@
+namespace IntegrationTests.TestsFixtures
+{
+    public class SshUser
+    {
+        public string UserName { get; }
+
+        public string Password { get; }
+
+        public SshUser(string userName, string password)
+        {
+            UserName = userName;
+            Password = password;
+        }
+    }
+}
+

+ 36 - 0
src/Renci.SshNet.IntegrationTests/Usings.cs

@@ -0,0 +1,36 @@
+#pragma warning disable IDE0005
+
+extern alias LocalSshNet;
+
+global using System.Text;
+
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+global using IntegrationTests.TestsFixtures;
+
+// The testcontainers library uses SSH.NET, so we have two versions of SSH.NET in the project.
+// We need to explicitly choose which version we want to test.
+// To avoid problems, we import all namespaces.
+global using LocalSshNet::Renci.SshNet;
+global using LocalSshNet::Renci.SshNet.Abstractions;
+global using LocalSshNet::Renci.SshNet.Channels;
+global using LocalSshNet::Renci.SshNet.Common;
+global using LocalSshNet::Renci.SshNet.Compression;
+global using LocalSshNet::Renci.SshNet.Connection;
+global using LocalSshNet::Renci.SshNet.Messages;
+global using LocalSshNet::Renci.SshNet.Messages.Authentication;
+global using LocalSshNet::Renci.SshNet.Messages.Connection;
+global using LocalSshNet::Renci.SshNet.Messages.Transport;
+global using LocalSshNet::Renci.SshNet.NetConf;
+global using LocalSshNet::Renci.SshNet.Security;
+global using LocalSshNet::Renci.SshNet.Security.Chaos;
+global using LocalSshNet::Renci.SshNet.Security.Chaos.NaCl;
+global using LocalSshNet::Renci.SshNet.Security.Chaos.NaCl.Internal;
+global using LocalSshNet::Renci.SshNet.Security.Cryptography;
+global using LocalSshNet::Renci.SshNet.Security.Cryptography.Ciphers;
+global using LocalSshNet::Renci.SshNet.Security.Org;
+global using LocalSshNet::Renci.SshNet.Security.Org.BouncyCastle;
+
+global using LocalSshNet::Renci.SshNet.Sftp;
+
+

+ 15 - 0
src/Renci.SshNet.IntegrationTests/app.config

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <runtime>
+        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+            <dependentAssembly>
+                <assemblyIdentity name="Renci.SshNet"
+                                  publicKeyToken="1cee9f8bde3db106"
+                                  culture="neutral" />
+                <bindingRedirect oldVersion="2020.0.2.0-2023.0.0.0" newVersion="2023.0.0.0" />
+            </dependentAssembly>
+
+        </assemblyBinding>
+    </runtime>
+
+</configuration>

+ 10 - 0
src/Renci.SshNet.IntegrationTests/server/script/start.sh

@@ -0,0 +1,10 @@
+#!/bin/ash
+/usr/sbin/syslog-ng
+
+# allow us to make changes to /etc/hosts; we need this for the port forwarding tests
+chmod 777 /etc/hosts
+
+# start PAM-enabled ssh daemon as we also want keyboard-interactive authentication to work
+/usr/sbin/sshd.pam
+
+tail -f < /var/log/auth.log

+ 9 - 0
src/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa_key

@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTtDoci0CaEgyR+p+ersiYltKUSqZx/
+MffWpnEPfGgnFI81huQw0D9e/SqABbeHtrzcSWskSZc0f2jjFxyqVkliAAAAsDrEln06xJ
+Z9AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO0OhyLQJoSDJH6n
+56uyJiW0pRKpnH8x99amcQ98aCcUjzWG5DDQP179KoAFt4e2vNxJayRJlzR/aOMXHKpWSW
+IAAAAgYdRMomjDSquRMSYTvEIzX7cReJ2grVIWsxIOLyhJnw0AAAAWcm9vdEBVYnVudHUx
+OTEwRGVza3RvcAEC
+-----END OPENSSH PRIVATE KEY-----

+ 7 - 0
src/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ed25519_key

@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCCMW3VnuKfj6AxBQ7gJ4qAfEgw/YJl9q3AXyelCw+OdwAAAKDKcLC0ynCw
+tAAAAAtzc2gtZWQyNTUxOQAAACCCMW3VnuKfj6AxBQ7gJ4qAfEgw/YJl9q3AXyelCw+Odw
+AAAED9TRnDkG0tzdZv5oPJCXwzqrkxut7y33A8Wi8AzusJL4IxbdWe4p+PoDEFDuAnioB8
+SDD9gmX2rcBfJ6ULD453AAAAFnJvb3RAVWJ1bnR1MTkxMERlc2t0b3ABAgMEBQYH
+-----END OPENSSH PRIVATE KEY-----

+ 38 - 0
src/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_rsa_key

@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+-----END OPENSSH PRIVATE KEY-----

+ 4 - 0
src/Renci.SshNet.IntegrationTests/user/sshnet/authorized_keys

@@ -0,0 +1,4 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4N0T6VopnwPSYQUw0waQNhBjz0DYFQwvkv4OwWYSf//fJF3M6bH42Tn2J+IlQ+4/fCFnE3m99seV5T1myEj7fsupNteY2sKFGXENLGtAD/76FM0iBmXx76xlSTyZSSmNDIRU4xUR23cfc+al84F5mO2lEk+5Zr3Qn5JUpucBfis4vtu0sMDgZ4w1d0tcuXkT/MEJn2iX2cnxbSy5qNAPHu7b+LEfXBv2OrMDqPrx/X6QREgi3w5RxL5kz7bvitWsIwIvb3ST2ARAArBwb8pEyp2A/w5p22rkQtL+3ibZ8fkmpgn33f31AZPgtM++iJPBmPKFjArcWEJ9fIVB/6DAj
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzzrPpItEjNG7tU0DpJJ4pkI01E9d6K61OKTVPdFQSyGCdMj9XdP93lC6sJA+9/ahvf5F3gWEKxUJL2CKUiFWw=
+ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLSsu/HNKiaALhQ26UDv+N0AFdMb26fMVrOKe866CGu6ajSf9HUOhJFdjhseihB2rTalMPr8MrcXNLufii4mL8u4l9fUQXFgwnM/ZpiVPSs6C+8i4u/ZDg7Nx2NXybNIgQ==
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACB4WgRgGBRo6Uk+cRgg8tJPCbEtGURRWlUA7PDDerXR+P9O6mm3L99Etxsyh5XNYqXyaMNtH5c51ooMajrFwcayAHIhPPb8X3CsTwEfIUQ96aDyHQMotbRfnkn6uefeUTRrSNcqeAndUtVyAqBdqbsq2mgJYXHrz2NUKlPFYgauQi+WQ==

+ 3 - 3
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -64,9 +64,9 @@
 
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
-    <PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
-    <PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
+    <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
+    <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
     <PackageReference Include="Moq" Version="4.18.4" />
   </ItemGroup>
   <ItemGroup>

+ 22 - 0
src/Renci.SshNet.sln

@@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D21A4D03-0
 		..\test\Directory.Build.props = ..\test\Directory.Build.props
 	EndProjectSection
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "Renci.SshNet.IntegrationTests\IntegrationTests.csproj", "{EEF98046-729C-419E-932D-4E569073C8CC}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -84,6 +86,26 @@ Global
 		{C45379B9-17B1-4E89-BC2E-6D41726413E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
 		{C45379B9-17B1-4E89-BC2E-6D41726413E8}.Release|x64.ActiveCfg = Release|Any CPU
 		{C45379B9-17B1-4E89-BC2E-6D41726413E8}.Release|x86.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|ARM.Build.0 = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|x64.Build.0 = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Debug|x86.Build.0 = Debug|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|ARM.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|ARM.Build.0 = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|x64.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|x64.Build.0 = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|x86.ActiveCfg = Release|Any CPU
+		{EEF98046-729C-419E-932D-4E569073C8CC}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 5 - 5
src/Renci.SshNet/Properties/CommonAssemblyInfo.cs

@@ -5,13 +5,13 @@ using System.Runtime.InteropServices;
 [assembly: AssemblyDescription("SSH.NET is a Secure Shell (SSH) library for .NET, optimized for parallelism.")]
 [assembly: AssemblyCompany("Renci")]
 [assembly: AssemblyProduct("SSH.NET")]
-[assembly: AssemblyCopyright("Copyright © Renci 2010-2017")]
+[assembly: AssemblyCopyright("Copyright © Renci 2010-2023")]
 [assembly: AssemblyTrademark("")]
 [assembly: AssemblyCulture("")]
 
-[assembly: AssemblyVersion("2017.0.0")]
-[assembly: AssemblyFileVersion("2017.0.0")]
-[assembly: AssemblyInformationalVersion("2017.0.0-beta1")]
+[assembly: AssemblyVersion("2023.0.0")]
+[assembly: AssemblyFileVersion("2023.0.0")]
+[assembly: AssemblyInformationalVersion("2023.0.0")]
 [assembly: CLSCompliant(false)]
 
 // Setting ComVisible to false makes the types in this assembly not visible 
@@ -24,4 +24,4 @@ using System.Runtime.InteropServices;
 [assembly: AssemblyConfiguration("Debug")]
 #else
 [assembly: AssemblyConfiguration("Release")]
-#endif
+#endif