我们如何使用 C# .net 核心使用 MoveIt RestFul API

问题描述

我正在研究 MoveIt API 以及如何使用 C# 代码使用它。 试图找到一些基于的示例代码 https://docs.ipswitch.com/MOVEit/Transfer2020/API/rest/#operation/POSTapi%2Fv1%2Ffolders%2F%7BId%7D%2Fsubfolders-1.0 要么 https://docs.ipswitch.com/MOVEit/Transfer2019_1/API/Rest/ MoveIt 还有一个“REST API Swagger 用户界面”

另外,我想使用 .NET 核心。我环顾四周找不到太多,终于开始构建一个,现在我已经分享了。

解决方法

我做了大量的研究和试验基础,最后我能够编写一个我认为可以分享的可测试代码。这是一些示例代码,我将模型类放在这里,这些类基于 https://docs.ipswitch.com/MOVEit/Transfer2019_1/API/Rest/ 和 MoveIt 'REST API Swagger User Interface' 它使用 HttpClient 和 HttpClient 工厂。 还添加了两部分的自动化单元测试代码。

//You need Following package
Microsoft.Extension.http
System.Net.Http
System.Net.Http.Json
///////////////////FTPException class
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;

namespace FtpUtilityLib.Exceptions
{

    [ExcludeFromCodeCoverage]
    [Serializable]
    public class FtpException : Exception
    {
        public FtpException()
        {
        }

        public FtpException(string message) : base(message)
        {
        }

        public FtpException(string message,Exception inner) : base(message,inner)
        {
        }
        
        protected FtpException(
            SerializationInfo info,StreamingContext context) : base(info,context)
        {
        }
    }
}
//////////////// Models
using System.Text.Json.Serialization;

namespace FtpUtilityLib.Models
{
    public class AuthError
    {
        [JsonPropertyName("error")]
        public string Error { get; set; }

        [JsonPropertyName("error_description")]
        public string ErrorDescription { get; set; }
    }
    
    public class Error
    {

        [JsonPropertyName("detail")]
        public string Detail { get; set; }

        [JsonPropertyName("errorCode")]
        public int ErrorCode { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }
    }
    
    public class FileItem
    {
        [JsonPropertyName("uploadUsername")]
        public string UploadUsername { get; set; }

        [JsonPropertyName("uploadAgentVersion")]
        public string UploadAgentVersion { get; set; }

        [JsonPropertyName("currentFileType")]
        public string CurrentFileType { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("dlpMetaData")]
        public int DlpMetaData { get; set; }

        [JsonPropertyName("dlpChecked")]
        public bool DlpChecked { get; set; }

        [JsonPropertyName("id")]
        public string Id { get; set; }

        [JsonPropertyName("dlpBlocked")]
        public bool DlpBlocked { get; set; }

        [JsonPropertyName("originalFileType")]
        public string OriginalFileType { get; set; }

        [JsonPropertyName("folderID")]
        public string FolderID { get; set; }

        [JsonPropertyName("uploadComment")]
        public string UploadComment { get; set; }

        [JsonPropertyName("uploadStamp")]
        public string UploadStamp { get; set; }

        [JsonPropertyName("uploadIntegrity")]
        public int UploadIntegrity { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("size")]
        public int Size { get; set; }

        [JsonPropertyName("isNew")]
        public bool IsNew { get; set; }

        [JsonPropertyName("uploadIP")]
        public string UploadIP { get; set; }

        [JsonPropertyName("path")]
        public string Path { get; set; }

        [JsonPropertyName("downloadCount")]
        public int DownloadCount { get; set; }

        [JsonPropertyName("originalFilename")]
        public string OriginalFilename { get; set; }

        [JsonPropertyName("orgID")]
        public string OrgID { get; set; }

        [JsonPropertyName("dlpViolation")]
        public string DlpViolation { get; set; }

        [JsonPropertyName("uploadAgentBrand")]
        public string UploadAgentBrand { get; set; }

        [JsonPropertyName("uploadUserFullName")]
        public string UploadUserFullName { get; set; }
    }
    
     public class FileList
    {
        [JsonPropertyName("items")]
        public List<FileItem> Items { get; set; }

        [JsonPropertyName("sorting")]
        public List<Sorting> Sorting { get; set; }

        [JsonPropertyName("paging")]
        public Paging Paging { get; set; }
    }
      public class FolderItem
    {
        [JsonPropertyName("subfolderCount")]
        public int SubfolderCount { get; set; }

        [JsonPropertyName("sharedWithGroupsCount")]
        public int SharedWithGroupsCount { get; set; }

        [JsonPropertyName("sharedWithUsersCount")]
        public int SharedWithUsersCount { get; set; }

        [JsonPropertyName("isShared")]
        public bool IsShared { get; set; }

        [JsonPropertyName("id")]
        public string Id { get; set; }

        [JsonPropertyName("parentId")]
        public string ParentId { get; set; }

        [JsonPropertyName("path")]
        public string Path { get; set; }

        [JsonPropertyName("lastContentChangeTime")]
        public string LastContentChangeTime { get; set; }

        [JsonPropertyName("permission")]
        public Permission Permission { get; set; }

        [JsonPropertyName("folderType")]
        public string FolderType { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("totalFileCount")]
        public int TotalFileCount { get; set; }
    }

    public class Permission
    {
        [JsonPropertyName("canListSubfolders")]
        public bool CanListSubfolders { get; set; }

        [JsonPropertyName("canListFiles")]
        public bool CanListFiles { get; set; }

        [JsonPropertyName("canChangeSettings")]
        public bool CanChangeSettings { get; set; }

        [JsonPropertyName("canWriteFiles")]
        public bool CanWriteFiles { get; set; }

        [JsonPropertyName("canAddSubfolders")]
        public bool CanAddSubfolders { get; set; }

        [JsonPropertyName("canDelete")]
        public bool CanDelete { get; set; }

        [JsonPropertyName("canReadFiles")]
        public bool CanReadFiles { get; set; }

        [JsonPropertyName("canDeleteFiles")]
        public bool CanDeleteFiles { get; set; }

        [JsonPropertyName("canShare")]
        public bool CanShare { get; set; }
    }
     public class FolderList
    {
        [JsonPropertyName("paging")]
        public Paging Paging { get; set; }

        [JsonPropertyName("sorting")]
        public List<Sorting> Sorting { get; set; }

        [JsonPropertyName("items")]
        public List<FolderItem> Items { get; set; }
    }
    
    public class Paging
    {
        [JsonPropertyName("totalPages")]
        public int TotalPages { get; set; }

        [JsonPropertyName("page")]
        public int Page { get; set; }

        [JsonPropertyName("totalItems")]
        public int TotalItems { get; set; }

        [JsonPropertyName("perPage")]
        public int PerPage { get; set; }
    }
    
     public class Sorting
    {
        [JsonPropertyName("sortField")]
        public string SortField { get; set; }

        [JsonPropertyName("sortDirection")]
        public string SortDirection { get; set; }
    }
    public class Token
    {
        [JsonPropertyName("token_type")]
        public string TokenType { get; set; }

        [JsonPropertyName("access_token")]
        public string AccessToken { get; set; }

        [JsonPropertyName("refresh_token")]
        public string RefreshToken { get; set; }

        [JsonPropertyName("expires_in")]
        public int ExpiresIn { get; set; }
    }
    
     public class UnprocessableEntityError
    {
        [JsonPropertyName("detail")]
        public string Detail { get; set; }

        [JsonPropertyName("errors")]
        public List<SemanticError> SemanticErrors { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("errorCode")]
        public int ErrorCode { get; set; }
    }

    public class SemanticError
    {
        [JsonPropertyName("field")]
        public string Field { get; set; }

        [JsonPropertyName("message")]
        public string Message { get; set; }

        [JsonPropertyName("rejected")]
        public string Rejected { get; set; }
    }   
}

/////////////////////// FtpServiceGateway class

using FtpUtilityLib.Exceptions;
using FtpUtilityLib.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

[assembly: InternalsVisibleTo("MoveItRestApiUtilityLib.Tests")]
namespace FtpUtilityLib
{
    internal sealed class FtpApiServiceGateway : IDisposable
    {
        private const string FtpApiVersionPath = @"api/v1/";
        private readonly IHttpClientFactory httpClientFactory;
        private bool disposed = false;

        public FtpApiServiceGateway(FtpConfiguration ftpConfiguration,IHttpClientFactory httpClientFactory)
        {
            FtpConfiguration = ftpConfiguration;
            this.httpClientFactory = httpClientFactory;
        }

        public FtpConfiguration FtpConfiguration { get; }

        public string AccessToken { get; private set; }


        public async Task SignOnToHostAsync()
        {
            try
            {
                var httpClient = CreateHttpClient();

                var formContent = new FormUrlEncodedContent(new[]
                {
                        new KeyValuePair<string,string>("grant_type","password"),new KeyValuePair<string,string>("username",FtpConfiguration.Username),string>("password",FtpConfiguration.Password),});

                var httpResponse = await httpClient.PostAsync("token",formContent);
                await EnsureSignOnSuccessAsync(httpResponse);
                var token = await httpResponse.Content.ReadFromJsonAsync<Token>();
                AccessToken = token.AccessToken;
            }
            catch (Exception e)
            {

                throw new FtpException($"Error in logging to FTP server {FtpConfiguration.Host}. " + e.Message,e);
            }
        }

         public async Task<string> DownloadFileAsync(
            string nameOfFileToBeDownloaded,string localFolderPath,string localFileName)
        {
            var downloadedFileId = string.Empty;

            try
            {
                using var httpClient = CreateHttpClient();
                var files = await httpClient.GetFromJsonAsync<FileList>("files?perpage=100");
                FileItem file = null;
                for (int i = 1; i <= files.Paging.TotalPages; i++)
                {
                    files = await httpClient.GetFromJsonAsync<FileList>($"files?perpage=100&page={i}");
                    file = files.Items.FirstOrDefault(x =>
                        x.Name.ToLower().Equals(nameOfFileToBeDownloaded.ToLower(),StringComparison.CurrentCulture));

                    if (file != null)
                    {
                        break;
                    }
                }

                downloadedFileId = await SendFileDownloadRequestAsync(nameOfFileToBeDownloaded,localFolderPath,localFileName,file,httpClient,string.Empty);
            }
            catch (Exception e)
            {
                throw new FtpException($"Error in downloading the file {nameOfFileToBeDownloaded} from FTP server. " + e.Message,e);
            }

            return downloadedFileId;
        }

        public async Task<string> DownloadFileAsync(
             string nameOfFileToBeDownloaded,string downloadFilePath,string localFileName)
        {
            var downloadedFileId = string.Empty;
            try
            {
                using var httpClient = CreateHttpClient();
                var folders = await httpClient.GetFromJsonAsync<FolderList>(@$"folders?path={downloadFilePath}");
                var folder = folders.Items.SingleOrDefault(x => x.Path.ToLower().Equals(downloadFilePath.ToLower()));
                if (folder != null)
                {
                    var files = await httpClient.GetFromJsonAsync<FileList>($"folders/{folder.Id}/files?perPage=100");
                    var file = files.Items.SingleOrDefault(x =>
                        x.Name.ToLower().Equals(nameOfFileToBeDownloaded.ToLower(),StringComparison.CurrentCulture));
                    downloadedFileId = await SendFileDownloadRequestAsync(nameOfFileToBeDownloaded,downloadFilePath);
                }
                else
                {
                    throw new FtpException($"Folder {downloadFilePath} not found on FTP server");
                }
            }
            catch (Exception e)
            {
                throw new FtpException($"Error in downloading the file {nameOfFileToBeDownloaded} from FTP server. " + e.Message,e);
            }

            return downloadedFileId;
        }

        public async Task<string> UploadFile(string sourceFilePathName,string destinationPath)
        {
            return await UploadFileAsync(sourceFilePathName,destinationPath,"Comments are not provided");
        }

        public async Task<string> UploadFileAsync(string sourceFilePathName,string destinationPath,string comments)
        {
            string uploadedFileId;
            try
            {
                using var multipartFormContent = new MultipartFormDataContent();
                using (var sha256 = SHA256.Create())
                {
                    await using var fileStream = File.Open(sourceFilePathName,FileMode.Open);

                    // Be sure it's positioned to the beginning of the stream.
                    fileStream.Position = 0;

                    // Compute the hash of the fileStream.
                    byte[] hashValue = sha256.ComputeHash(fileStream);

                    var hash = System.Text.Encoding.UTF8.GetString(hashValue,hashValue.Length);
                    multipartFormContent.Add(new StringContent("hash"),hash);
                    multipartFormContent.Add(new StringContent("hashtype"),"sha-256");
                    multipartFormContent.Add(new StringContent("comments"),comments);

                    // Close the file.
                    fileStream.Close();
                }

                await using (var fileStream = File.OpenRead(sourceFilePathName))
                {
                    using var httpClient = CreateHttpClient();

                    var folders = await httpClient.GetFromJsonAsync<FolderList>(@$"folders?path={destinationPath}");
                    var folder = folders.Items.SingleOrDefault(x => x.Path.ToLower().Equals(destinationPath.ToLower()));
                    if (folder != null)
                    {
                        multipartFormContent.Add(new StreamContent(fileStream),"file",Path.GetFileName(sourceFilePathName));
                        var response = await httpClient.PostAsync($"folders/{folder.Id}/files",multipartFormContent);
                        await EnsureSuccessAsync(response);
                        var file = await response.Content.ReadFromJsonAsync<FileItem>();
                        uploadedFileId = file.Id;
                    }
                    else
                    {
                        throw new FtpException($"Folder {destinationPath} not found on FTP server");
                    }

                    fileStream.Close();
                }
            }
            catch (Exception e)
            {
                throw new FtpException($"Error in Uploading the file {sourceFilePathName} to the folder {destinationPath} on FTP server. " + e.Message,e);
            }

            return uploadedFileId;
        }
        
        public async Task<string> UploadFileFromBufferAsync(byte[] buffer,string fileName,string comments)
        {
            string uploadedFileId;
            try
            {
                using var multipartFormContent = new MultipartFormDataContent();
                using var sha256 = SHA256.Create();

                // Compute the hash of the fileStream.
                byte[] hashValue = sha256.ComputeHash(buffer);

                var hash = Encoding.UTF8.GetString(hashValue,hashValue.Length);
                multipartFormContent.Add(new StringContent("hash"),hash);
                multipartFormContent.Add(new StringContent("hashtype"),"sha-256");
                multipartFormContent.Add(new StringContent("comments"),comments);

                using var httpClient = CreateHttpClient();

                var folders = await httpClient.GetFromJsonAsync<FolderList>(@$"folders?path={destinationPath}");
                var folder = folders.Items.SingleOrDefault(x => x.Path.ToLower().Equals(destinationPath.ToLower()));

                if (folder != null)
                {
                    await using (var memoryStreamToUpload = new MemoryStream(buffer))
                    {

                        multipartFormContent.Add(new StreamContent(memoryStreamToUpload),fileName);
                        var response = await httpClient.PostAsync($"folders/{folder.Id}/files",multipartFormContent);
                        await EnsureSuccessAsync(response);
                        var file = await response.Content.ReadFromJsonAsync<FileItem>();
                        uploadedFileId = file.Id;
                        memoryStreamToUpload.Close();
                    }
                }
                else
                {
                    throw new FtpException($"Folder {destinationPath} not found on FTP server");
                }
            }
            catch (Exception e)
            {
                throw new FtpException($"Error in Uploading to the folder {destinationPath} on FTP server. " + e.Message,e);
            }

            return uploadedFileId;
        }


      private static async Task EnsureSuccessAsync(HttpResponseMessage httpResponseMessage)
        {
            if (httpResponseMessage.IsSuccessStatusCode)
            {
                return;
            }

            var reasonPhrase = httpResponseMessage.ReasonPhrase;

            switch (httpResponseMessage.StatusCode)
            {
                case HttpStatusCode.UnprocessableEntity:
                    var unprocessableEntityError = await httpResponseMessage.Content.ReadFromJsonAsync<UnprocessableEntityError>();
                    var semanticErrors = new StringBuilder();
                    foreach (var detail in unprocessableEntityError.SemanticErrors.Select(
                        semanticError => $"Field: {semanticError.Field},Rejected: {semanticError.Rejected},Message: {semanticError.Message}"))
                    {
                        semanticErrors.AppendLine(detail);
                    }
                    throw new HttpRequestException(
                        "Calling the FTP Service resulting in an error with  HttpStatusCode:" +
                        $" {httpResponseMessage.StatusCode},ErrorCode:{unprocessableEntityError.ErrorCode} | ErrorDetail: {unprocessableEntityError.Detail} | {semanticErrors}");
                default:
                    var error = await httpResponseMessage.Content.ReadFromJsonAsync<Error>();
                    throw new HttpRequestException(
                        $"Calling the FTP Service resulting in an error with unexpected  HttpStatusCode: " +
                        $"{httpResponseMessage.StatusCode},ErrorCode:{error.ErrorCode} | Detail: {error.Detail} | HttpReason: {reasonPhrase}");
            }
        }


        private static async Task EnsureSignOnSuccessAsync(HttpResponseMessage httpResponseMessage)
        {
            if (httpResponseMessage.IsSuccessStatusCode)
            {
                return;
            }

            var reasonPhrase = httpResponseMessage.ReasonPhrase;
            var authErrorContent = await httpResponseMessage.Content.ReadAsStringAsync();
            var authError = JsonSerializer.Deserialize<AuthError>(authErrorContent);
            throw new HttpRequestException(
                        $"Calling the FTP Service resulting in an error with HttpStatusCode:" +
                        $" {httpResponseMessage.StatusCode},ErrorCode:{authError.Error} | Description: {authError.ErrorDescription} | HttpReason: {reasonPhrase}");
        }

        private HttpClient CreateHttpClient()
        {
            HttpClient httpClient = httpClientFactory.CreateClient("FtpAPIClient");
            httpClient.BaseAddress = new Uri($@"{FtpConfiguration.Host}/{FtpApiVersionPath}");

            httpClient.DefaultRequestHeaders.Add("accept","application/json");
            httpClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("Bearer",AccessToken);
            return httpClient;
        }

        private async Task<string> SendFileDownloadRequestAsync(
            string nameOfFileToBeDownloaded,string localFileName,FileItem file,HttpClient httpClient,string downloadFilePath)
        {
            string downloadedFileId;
            if (file != null)
            {
                httpClient.DefaultRequestHeaders.Clear();
                httpClient.DefaultRequestHeaders.Add("accept","application/octet-stream");
                httpClient.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer",AccessToken);
                var downloadResponse = await httpClient.GetAsync($"files/{file.Id}/download");
                await EnsureSuccessAsync(downloadResponse);
                await using var streamToReadFrom = await downloadResponse.Content.ReadAsStreamAsync();
                await using var fs = new FileStream(@$"{localFolderPath}\{localFileName}",FileMode.OpenOrCreate);
                await streamToReadFrom.CopyToAsync(fs);
                fs.Close();
                downloadedFileId = file.Id;
            }
            else
            {
                throw new FtpException(
                    $"File {nameOfFileToBeDownloaded} under folder {downloadFilePath} not found on FTP server");
            }

            return downloadedFileId;
        }

        private void Dispose(bool disposing)
        {
            if (disposing && !disposed)
            {
                try
                {
                    using var httpClient = CreateHttpClient();
                    var httpResponse = httpClient.PostAsync($"{AccessToken}/revoke",null);
                }
                catch (Exception e)
                {
                    // see where can we log error
                }

                disposed = true;
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}
/////////////////////////////FtpConfiguration
using System;

namespace FtpUtilityLib
{
    public sealed class FtpConfiguration
    {
        private int maxNumberOfSignOnAttempts;

        private int waitTimeBeforeNextAttemptInMilliseconds;

        public FtpConfiguration(string host,string username,string password)
        {
            WaitTimeBeforeNextAttemptInMilliseconds = 30000;
            MaxNumberOfSignOnAttempts = 3;
            Host = host;
            Username = username;
            Password = password;
        }

        public FtpConfiguration(
            string host,string password,int waitTimeBeforeNextAttemptInMilliseconds,int maxNumberOfSignOnAttempts)
        {
            WaitTimeBeforeNextAttemptInMilliseconds = waitTimeBeforeNextAttemptInMilliseconds;
            MaxNumberOfSignOnAttempts = maxNumberOfSignOnAttempts;
            Host = host;
            Username = username;
            Password = password;
        }

        public string Username { get; }

        public string Password { get; }

        public string Host { get; }

        public int MaxNumberOfSignOnAttempts
        {
            get => maxNumberOfSignOnAttempts;
            private set
            {
                if (value < 1 || value > 6)
                    throw new ArgumentOutOfRangeException(
                        $"MaxNumberOfSignOnAttempts- Allowed range of value is between 1 and 6. You have provided {value}");

                maxNumberOfSignOnAttempts = value;
            }
        }

        public int WaitTimeBeforeNextAttemptInMilliseconds
        {
            get => waitTimeBeforeNextAttemptInMilliseconds;
            private set
            {
                if (value < 1000 || value > 60000)
                    throw new ArgumentOutOfRangeException(
                        $"WaitTimeBeforeNextAttemptInMilliseconds- Allowed range of value is between 1000 and 60000. You have provided {value}");

                waitTimeBeforeNextAttemptInMilliseconds = value;
            }
        }
    }
}

/////////////////HttpClientFactory
using System.Net.Http;

namespace FtpUtilityLib
{

    internal sealed class HttpClientFactory : IHttpClientFactory
    {
        public HttpClient CreateClient(string name)
        {
            return new HttpClient();
        }
    }
}
,

下面是用于自动化单元测试的测试类。由于我可以发布的文本限制,添加了 2 部分。,这是第 1 部分。您可以使用编译错误来调整引用。 TestTResponses 类在第 -2 部分上传

///Create a TestFiles folder under MSTestProject and then create SampleFile.json file in that folder. Go to properties of file select copyAlways for CopyToOutputDirectory

using FtpUtilityLib.Models;
using System.Collections.Generic;

namespace MoveItRestApiUtilityLib.Tests
{

    //////////////////////FakeHttpMessageHandler
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Http;
    using System.Text;
    using System.Text.Json;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class FakeHttpMessageHandler : DelegatingHandler
    {
        public Dictionary<(string,string),HttpResponseMessage> DesiredHttpResponseMessagesByUriAbsolutePath { get; }

        public FakeHttpMessageHandler()
        {
            DesiredHttpResponseMessagesByUriAbsolutePath = new Dictionary<(string,HttpResponseMessage>();
            //PopulateDesiredResponseList();
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
        {
            if (request.RequestUri.Query.Contains("perpage=100&page="))
            {
                return new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.OK,Content = new StringContent(JsonSerializer.Serialize(TestResponse.FilesResponse),Encoding.UTF8,"application/json")
                };
            }
            var requestUriAbsolutePath = request.RequestUri.AbsolutePath;
            var responseMessage = DesiredHttpResponseMessagesByUriAbsolutePath[(requestUriAbsolutePath,request.Method.Method.ToLower())];

            return await Task.FromResult(responseMessage);

        }
    }
    
    /////////////////////////////
    
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    public static class AssertEx
    {
        public static void AssertExceptionMessageContains(IEnumerable<string> expected,string actual)
        {
            var messages = new List<string>();

            foreach (var item in expected)
            {
                if (actual.IndexOf(item,StringComparison.OrdinalIgnoreCase) < 0)
                {
                    messages.Add("Expected to find the substring: \"" + item + "\",in the Exception Message: " + actual);
                }
            }

            if (messages.Any())
            {
                throw new AssertFailedException(String.Join(",",messages));
            }
        }
    }
    
      //////////////////////////////Partial Test Class in a Test file 
     [ExcludeFromCodeCoverage]
    [TestClass]
    public partial class FtpApiServiceGatewayTests
    {
        public static string HostName => "https://ATestHost.com";

        //private Mock httpClientFactoryMock = new Mock<IHttpClientFactory>();
        FtpConfiguration ftpConfiguration =
            new FtpConfiguration(HostName,"SomeUser","SomePassword");

        [TestMethod]
        public async Task SignOnToHost_WhenValidUserIdAndPassword_ShouldSuccess()
        {
            //ARRANGE
            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/token","post"),new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.OK,Content = new StringContent(JsonSerializer.Serialize(TestResponse.TokenResponse),"application/json")
                });

            var fakeHttpClient = new HttpClient(fakeHttpMessageHandler);
            httpClientFactoryMock.Setup(h => h.CreateClient(It.IsAny<string>())).Returns(fakeHttpClient);
            var ftpApiServiceGateway = new FtpApiServiceGateway(ftpConfiguration,httpClientFactoryMock.Object);

            //ACT
            await ftpApiServiceGateway.SignOnToHostAsync();

            //ASSERT
            Assert.AreEqual(TestResponse.TokenResponse.AccessToken,ftpApiServiceGateway.AccessToken);
        }

        [TestMethod]
        public async Task SignOnToHost_WhenInvalidUserIdOrPassword_ShouldThrowException()
        {
            //ARRANGE
            var r = JsonSerializer.Serialize(TestResponse.TokenErrorResponse);

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/token",new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.BadRequest,Content = new StringContent(
                        JsonSerializer.Serialize(
                            TestResponse.TokenErrorResponse),httpClientFactoryMock.Object);

            //ACT
            try
            {
                await ftpApiServiceGateway.SignOnToHostAsync();
                Assert.Fail("Expected Exception of type: FtpException during this test,but it was not thrown");
            }
            catch (FtpException e)
            {
                //ASSERT
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                        "Error in logging to FTP server",HostName,TestResponse.TokenErrorResponse.ErrorDescription,TestResponse.TokenErrorResponse.Error,"Calling the FTP Service resulting in an error with HttpStatusCode:"
                    },e.Message);
            }
        }

        [TestMethod]
        public async Task DownloadFileAsync_ByFilesAPI_WhenValidFile_ShouldSuccess()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "SampleFile.json";
            var localFolderPath = @".\TestFiles";
            var localFileName = "ATest2.json";
            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/files","get"),"application/json")
                });

            var filePathName = @".\TestFiles\SampleFile.json";
            using var fileStream = File.OpenRead(filePathName);
            var ms = new MemoryStream();
            fileStream.CopyTo(ms);
            var content = new ByteArrayContent(ms.GetBuffer());
            content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = "SampleFile.json"
            };

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/files/769379085/download",ReasonPhrase = "OK",Content = content
                });

            var fakeHttpClient = new HttpClient(fakeHttpMessageHandler);
            httpClientFactoryMock.Setup(h => h.CreateClient(It.IsAny<string>())).Returns(fakeHttpClient);
            var ftpApiServiceGateway = new FtpApiServiceGateway(ftpConfiguration,httpClientFactoryMock.Object);

            //ACT
            var fileId = await ftpApiServiceGateway.DownloadFileAsync(
                nameOfFileToBeDownloaded,localFileName);

            //ASSERT
            Assert.IsFalse(string.IsNullOrEmpty(fileId));
        }

        [TestMethod]
        public async Task DownloadFileAsync_WhenValidFile_ShouldSuccess()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "SampleFile.json";
            var downloadFilePath = TestResponse.FtpServerTestFolderPath;
            var localFolderPath = @".\TestFiles";
            var localFileName = "ATest2.json";
            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
               ("/api/v1/folders",new HttpResponseMessage()
               {
                   StatusCode = HttpStatusCode.OK,Content = new StringContent(JsonSerializer.Serialize(TestResponse.FoldersResponse),"application/json")
               });

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders/715431145/files",downloadFilePath,localFileName);

            //ASSERT
            Assert.IsFalse(string.IsNullOrEmpty(fileId));
        }

        [TestMethod]
        public async Task DownloadFileAsync_WhenInvalidSourceFolder_ThrowsException()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "SampleFile.json";
            var downloadFilePath = @"/Data/InvalidFolder";
            var localFolderPath = @".\TestFiles";
            var localFileName = "ATest2.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",Content = new StringContent(JsonSerializer.Serialize(TestResponse.FolderNotFoundResponse),"application/json")
                });

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders/715431145/files","application/json")
                });

            var filePathName = @".\TestFiles\SampleFile.json";
            using var fileStream = File.OpenRead(filePathName);
            var ms = new MemoryStream();
            fileStream.CopyTo(ms);
            var content = new ByteArrayContent(ms.GetBuffer());
            content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = nameOfFileToBeDownloaded
            };

            var fakeHttpClient = new HttpClient(fakeHttpMessageHandler);
            httpClientFactoryMock.Setup(h => h.CreateClient(It.IsAny<string>())).Returns(fakeHttpClient);
            var ftpApiServiceGateway = new FtpApiServiceGateway(ftpConfiguration,httpClientFactoryMock.Object);

            try
            {
                //ACT
                var fileId = await ftpApiServiceGateway.DownloadFileAsync(
                    nameOfFileToBeDownloaded,localFileName);

                //ASSERT
                Assert.Fail("Expected Exception of type: FtpException during this test,but it was not thrown");

            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                        $"Folder {downloadFilePath} not found on FTP server"
                    },e.Message);
            }
        }

        [TestMethod]
        public async Task DownloadFileAsync_WhenInvalidSourceFilename_ThrowsException()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "InvalidFile.json";
            var downloadFilePath = TestResponse.FtpServerTestFolderPath;
            var localFolderPath = @"C:\TestFolder\Test";
            var localFileName = "ATest2.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",but it was not thrown");

            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                        $"File {nameOfFileToBeDownloaded} under folder {downloadFilePath} not found on FTP server"
                    },e.Message);
            }
        }

        [TestMethod]
        public async Task DownloadFileAsync_WhenAccessDenied_ThrowsException()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "InvalidFile.json";
            var downloadFilePath = TestResponse.FtpServerTestFolderPath;
            var localFolderPath = @"C:\TestFolder\Test";
            var localFileName = "ATest2.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.Forbidden,Content = new StringContent(JsonSerializer.Serialize(TestResponse.ErrorResponse),but it was not thrown");

            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                        "Error in downloading the file",nameOfFileToBeDownloaded,"from FTP server"
                    },e.Message);
            }
        }


        [TestMethod]
        public async Task DownloadFileAsync_WhenInternalServerError_ThrowsException()
        {
            //ARRANGE
            var nameOfFileToBeDownloaded = "SampleFile.json";
            var downloadFilePath = TestResponse.FtpServerTestFolderPath;
            var localFolderPath = @"C:TestFTP\Download";
            var localFileName = "ATest2.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.InternalServerError,"application/json")
                });

            // var dir = Directory.GetCurrentDirectory();
            var filePathName = @".\TestFiles\SampleFile.json";
            using var fileStream = File.OpenRead(filePathName);
            var ms = new MemoryStream();
            fileStream.CopyTo(ms);
            var content = new ByteArrayContent(ms.GetBuffer());
            content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = nameOfFileToBeDownloaded
            };

            var fakeHttpClient = new HttpClient(fakeHttpMessageHandler);
            httpClientFactoryMock.Setup(h => h.CreateClient(It.IsAny<string>())).Returns(fakeHttpClient);
            var ftpApiServiceGateway = new FtpApiServiceGateway(ftpConfiguration,e.Message);
            }
        }
    }
,

这是自动化测试的第 -2 部分。

///////////////TestResponses class
    public static class TestResponse
    {
        public static Token TokenResponse = new Token()
        {
            AccessToken = "TestTokenMwzVguxPqzF6WpIXSDfIinPr2ZgX3zBcCYo-qguVYMHSQxmwNEsdVEblVQdWAxUHriBcPaVIwZMxeNy6b5fPD_cZdtgNMiDjdKClRCM9Ps8yg",ExpiresIn = 1199,RefreshToken = "TestRefreshToken57Bne4Cs8lSXx4OYbwjsAYaJBS4uNpAN_mIrawiyC1KmGBc6YCIxe7nEQXhcCPHN3mi8nby3JkIPX190g1Ducjm5hxRHBGOmjMbDeZ35mEjvbUSFbXHrl7xzWPidkcvAg2bomkKDpwojoKxW1RkfR2UG_Qb_o7hrYS4JdQOUm3_uQcvvW9q5uR3snOEiM3Ge7U39HUBOSqOhQ",TokenType = "Bearer"
        };

        public static string FtpServerTestFolderPath = @"/Data/CSS_IntTest";

        public static FolderList FoldersResponse =>
            new FolderList()
            {
                Items = new List<FolderItem>()
                {
                    new FolderItem()
                    {
                        FolderType = "Normal",Id = "715431145",IsShared = true,LastContentChangeTime = "2021-05-27T09:58:16",Name = "CSS_IntTest",ParentId = "510768038",Path = FtpServerTestFolderPath
                    },new FolderItem()
                    {
                        FolderType = "Normal",Id = "71556789",LastContentChangeTime = "2020-05-29T09:58:16",Name = "CSS_IntTest2",Path = @"/Data/CSS_IntTest2"
                    }
                },Paging = new Paging()
                {
                    Page = 1,PerPage = 25,TotalItems = 1,TotalPages = 1
                },Sorting = new List<Sorting>()
                {
                    new Sorting()
                    {
                        SortDirection = "asc",SortField = "Path",}
                }
            };
        public static FolderList FolderNotFoundResponse =>
            new FolderList()
            {
                Items = new List<FolderItem>(),TotalItems = 0,}
                }
            };

        public static FileList FilesResponse =>
            new FileList()
            {
                Items = new List<FileItem>()
                {
                    new FileItem()
                    {
                        CurrentFileType = null,DownloadCount = 0,FolderID = null,Hash = null,Id = "769379085",Name = "samplefile.json",//OrgId = null,OriginalFileType = null,OriginalFilename =null,Path =  @$"{FtpServerTestFolderPath}\samplefile.json",Size = 35935783,UploadAgentBrand = null,UploadAgentVersion = null,UploadComment =  null,UploadIP =  null,UploadIntegrity= 0,UploadStamp ="2021-05-21T16:45:13",UploadUserFullName = null,//UploadUserRealname = null,UploadUsername = null
                    },new FileItem()
                    {
                        CurrentFileType = null,Id = "769379088",Name = "TestAssociate.json",Path =  @$"{FtpServerTestFolderPath}/TestAssociate.json",Size = 541,UploadStamp ="2021-05-27T09:58:16 ",UploadUsername = null
                    }
                },PerPage = 100,TotalItems = 2,SortField = "Name",}
                }
            };

        public static FileItem FileResponse =>
            new FileItem()
            {
                CurrentFileType = null,OriginalFilename = null,Path = @$"{FtpServerTestFolderPath }/samplefile.json",UploadComment = null,UploadIP = null,UploadIntegrity = 0,UploadStamp = "2021-05-27T09:58:16 ",//  UploadUserRealname = null,UploadUsername = null
            };

        public static FolderItem FolderResponse =>
            new FolderItem()
            {
                FolderType = "Normal",Path = FtpServerTestFolderPath
            };

        public static AuthError TokenErrorResponse =>
            new AuthError()
            {
                Error = "This is a Test error",ErrorDescription = "This is a Test error Description"
            };

        public static Error ErrorResponse =>
            new Error()
            {
                Detail = "This is a test error detail",ErrorCode = 4567,Title = "This a test error title"
            };

        public static UnprocessableEntityError UnprocessableEntityResponse =>
            new UnprocessableEntityError()
            {
                SemanticErrors = new List<SemanticError>()
                {
                    new SemanticError()
                    {
                        Field = "fileName",Message = "Invalid character in fileName",Rejected = "Yes"
                    },new SemanticError()
                    {
                        Field = "Header",Message = "Invalid Value",Rejected = "Yes"
                    }
                },Detail = "This is a test UnprocessableEntityError detail",ErrorCode = 1234,Title = "This a test error title"
            };

    }
    
    
    ///////////////////////////  Another test file
    
    
    public partial class FtpApiServiceGatewayTests
    {
        [TestMethod]
        public async Task UploadFileFromBufferAsync_WhenValidFile_ShouldSuccess()
        {
            //ARRANGE
            var fileName = "samplefile.json";
            var destinationFolder = TestResponse.FtpServerTestFolderPath;
            var sourceFolder = @".\TestFiles";
            await using var fileStream = File.Open($"{sourceFolder}/{fileName}",FileMode.Open);
            // Be sure it's positioned to the beginning of the stream.
            fileStream.Position = 0;
            var memoryStream = new MemoryStream();
            fileStream.CopyTo(memoryStream);
            var content = memoryStream.GetBuffer();

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.Created,Content = new StringContent(JsonSerializer.Serialize(TestResponse.FileResponse),httpClientFactoryMock.Object);

            //ACT
            var fileId = await ftpApiServiceGateway.UploadFileFromBufferAsync(
                content,destinationFolder,"ATest2.json","This is a test comment");

            //ASSERT
            Assert.IsFalse(string.IsNullOrEmpty(fileId));
        }

        [TestMethod]
        public async Task UploadFileFromBufferAsync_WhenInvalidDestination_ShouldSuccess()
        {
            //ARRANGE
            var fileName = "samplefile.json";
            var destinationFolder = TestResponse.FtpServerTestFolderPath + "/InValidFolder";
            var sourceFolder = @".\TestFiles";
            await using var fileStream = File.Open($"{sourceFolder}/{fileName}",FileMode.Open);
            // Be sure it's positioned to the beginning of the stream.
            fileStream.Position = 0;
            var memoryStream = new MemoryStream();
            fileStream.CopyTo(memoryStream);
            var content = memoryStream.GetBuffer();

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler(); fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
("/api/v1/folders",new HttpResponseMessage()
{
    StatusCode = HttpStatusCode.OK,"application/json")
}); fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders/715431145/files","application/json")
                });
            var fakeHttpClient = new HttpClient(fakeHttpMessageHandler);
            httpClientFactoryMock.Setup(h => h.CreateClient(It.IsAny<string>())).Returns(fakeHttpClient);
            var ftpApiServiceGateway = new FtpApiServiceGateway(ftpConfiguration,httpClientFactoryMock.Object);

            try
            {
                var fileId = await ftpApiServiceGateway.UploadFileFromBufferAsync(
                    content,"This is a test comment");
                //ASSERT
                Assert.Fail("Expected Exception of type: FtpException during this test,but it was not thrown");
            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                            $"Folder {destinationFolder} not found on FTP server","Error in Uploading to the folder","To the folder","on FTP server"
                    },e.Message);
            }
        }

        [TestMethod]
        public async Task UploadFileFromBufferAsync_WhenInternalServerError_ShouldSuccess()
        {
            //ARRANGE
            var fileName = "samplefile.json";
            var destinationFolder = TestResponse.FtpServerTestFolderPath;
            var sourceFolder = @".\TestFiles";
            await using var fileStream = File.Open($"{sourceFolder}/{fileName}","application/json")
                });
            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders/715431145/files",httpClientFactoryMock.Object);

            //ACT
            try
            {
                var fileId = await ftpApiServiceGateway.UploadFileFromBufferAsync(
                    content,"This is a test comment");
                Assert.Fail("Expected Exception of type: FtpException during this test,but it was not thrown");
            }
            catch (FtpException e)
            {
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                            $"Calling the FTP Service resulting in an error with unexpected  HttpStatusCode:",HttpStatusCode.InternalServerError.ToString(),TestResponse.ErrorResponse.Detail,TestResponse.ErrorResponse.ErrorCode.ToString(),e.Message);
            }
        }

        [TestMethod]
        public async Task UploadFileAsync_WhenValidFile_ShouldSuccess()
        {
            //ARRANGE
            var destinationPath = TestResponse.FtpServerTestFolderPath;
            var sourceFilePathName = @".\TestFiles\samplefile.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",httpClientFactoryMock.Object);

            //ACT
            var fileId = await ftpApiServiceGateway.UploadFile(
                sourceFilePathName,destinationPath);
            Assert.IsFalse(string.IsNullOrEmpty(fileId));
        }

        [TestMethod]
        public async Task UploadFileAsync_WhenInValidDestinationFolder_ThrowsException()
        {
            //ARRANGE
            var destinationPath = TestResponse.FtpServerTestFolderPath + "/InValidFolder";
            var sourceFilePathName = @".\TestFiles\samplefile.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",httpClientFactoryMock.Object);

            //ACT
            try
            {
                var fileId = await ftpApiServiceGateway.UploadFile(
                    sourceFilePathName,destinationPath);
                Assert.Fail("Expected Exception of type: FtpException during this test,but it was not thrown");
            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                            $"Folder {destinationPath} not found on FTP server","Error in Uploading the file",sourceFilePathName,e.Message);
            }
        }

        [TestMethod]
        public async Task UploadFileAsync_WhenFailedWithHttp422_ThrowsException()
        {
            //ARRANGE
            var destinationPath = TestResponse.FtpServerTestFolderPath;
            var sourceFilePathName = @".\TestFiles\samplefile.json";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();

            var fakeHttpMessageHandler = new FakeHttpMessageHandler();

            fakeHttpMessageHandler.DesiredHttpResponseMessagesByUriAbsolutePath.Add(
                ("/api/v1/folders",new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.UnprocessableEntity,Content = new StringContent(JsonSerializer.Serialize(TestResponse.UnprocessableEntityResponse),but it was not thrown");
            }
            catch (FtpException e)
            {
                //Assert
                AssertEx.AssertExceptionMessageContains(new string[]
                    {
                            $"Calling the FTP Service resulting in an error with  HttpStatusCode",HttpStatusCode.UnprocessableEntity.ToString(),TestResponse.UnprocessableEntityResponse.ErrorCode.ToString(),TestResponse.UnprocessableEntityResponse.Detail,TestResponse.UnprocessableEntityResponse.SemanticErrors[0].Message,TestResponse.UnprocessableEntityResponse.SemanticErrors[1].Message,$"Error in Uploading the file",e.Message);
            }

        }
    }
}