From 4017ae267ce6b9915ee1e162b79c4fcbaa78da34 Mon Sep 17 00:00:00 2001 From: Andrew Tsekhansky Date: Fri, 23 Jul 2021 09:13:34 +1200 Subject: [PATCH 1/2] Add ability to upload supporting document content --- src/Twilio/Http/Request.cs | 18 +++++++ src/Twilio/Http/SystemNetHttpClient.cs | 26 ++++++++- src/Twilio/Rest/Domain.cs | 1 + .../SupportingDocumentOptions.cs | 5 ++ .../SupportingDocumentResource.cs | 54 ++++++++++++------- src/Twilio/Types/UploadFile.cs | 24 +++++++++ 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 src/Twilio/Types/UploadFile.cs diff --git a/src/Twilio/Http/Request.cs b/src/Twilio/Http/Request.cs index 148572885..8c4c7975a 100644 --- a/src/Twilio/Http/Request.cs +++ b/src/Twilio/Http/Request.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Twilio.Rest; +using Twilio.Types; #if !NET35 using System.Net; @@ -61,6 +62,13 @@ public class Request /// public List> HeaderParams { get; private set; } +#if !NET35 + /// + /// Files to upload + /// + public List> Files { get; private set; } +#endif + /// /// Create a new Twilio request /// @@ -73,6 +81,9 @@ public Request(HttpMethod method, string url) QueryParams = new List>(); PostParams = new List>(); HeaderParams = new List>(); +#if !NET35 + Files = new List>(); +#endif } /// @@ -86,6 +97,7 @@ public Request(HttpMethod method, string url) /// Post data /// Twilio edge /// Custom header data + /// List of files to upload public Request( HttpMethod method, Domain domain, @@ -95,6 +107,9 @@ public Request( List> postParams = null, string edge = null, List> headerParams = null +#if !NET35 + , List> files = null +#endif ) { Method = method; @@ -105,6 +120,9 @@ public Request( QueryParams = queryParams ?? new List>(); PostParams = postParams ?? new List>(); HeaderParams = headerParams ?? new List>(); +#if !NET35 + Files = files ?? new List>(); +#endif } /// diff --git a/src/Twilio/Http/SystemNetHttpClient.cs b/src/Twilio/Http/SystemNetHttpClient.cs index 724d4fb62..6b78247c0 100644 --- a/src/Twilio/Http/SystemNetHttpClient.cs +++ b/src/Twilio/Http/SystemNetHttpClient.cs @@ -61,7 +61,31 @@ public override async Task MakeRequestAsync(Request request) var httpRequest = BuildHttpRequest(request); if (!Equals(request.Method, HttpMethod.Get)) { - httpRequest.Content = new FormUrlEncodedContent(request.PostParams); + if (request.Files.Count > 0) + { + var boundary = "---------" + Guid.NewGuid().ToString().ToLower(); + var multiPartContent = new MultipartFormDataContent(boundary); + multiPartContent.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data") + { + Parameters = { new NameValueHeaderValue("boundary", boundary) } + }; + + foreach (var postParam in request.PostParams) + { + multiPartContent.Add(new StringContent(postParam.Value), postParam.Key); + } + + foreach (var file in request.Files) + { + multiPartContent.Add(new StreamContent(file.Value.Stream), file.Key, file.Value.FileName); + } + + httpRequest.Content = multiPartContent; + } + else + { + httpRequest.Content = new FormUrlEncodedContent(request.PostParams); + } } this.LastRequest = request; diff --git a/src/Twilio/Rest/Domain.cs b/src/Twilio/Rest/Domain.cs index e9af23310..fa649d9ad 100644 --- a/src/Twilio/Rest/Domain.cs +++ b/src/Twilio/Rest/Domain.cs @@ -33,6 +33,7 @@ public static implicit operator Domain(string value) public static readonly Domain Monitor = new Domain("monitor"); public static readonly Domain Notify = new Domain("notify"); public static readonly Domain Numbers = new Domain("numbers"); + public static readonly Domain NumbersUpload = new Domain("numbers-upload"); public static readonly Domain Preview = new Domain("preview"); public static readonly Domain Pricing = new Domain("pricing"); public static readonly Domain Proxy = new Domain("proxy"); diff --git a/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentOptions.cs b/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentOptions.cs index 826278492..f421cbc56 100644 --- a/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentOptions.cs +++ b/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentOptions.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using Twilio.Base; +using Twilio.Types; using Twilio.Converters; namespace Twilio.Rest.Numbers.V2.RegulatoryCompliance @@ -29,6 +30,10 @@ public class CreateSupportingDocumentOptions : IOptions public object Attributes { get; set; } +#if !NET35 + public IUploadFile File { get; set; } +#endif + /// /// Construct a new CreateSupportingDocumentOptions /// diff --git a/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentResource.cs b/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentResource.cs index 13dbddb1e..c3501094f 100644 --- a/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentResource.cs +++ b/src/Twilio/Rest/Numbers/V2/RegulatoryCompliance/SupportingDocumentResource.cs @@ -40,6 +40,20 @@ public static implicit operator StatusEnum(string value) private static Request BuildCreateRequest(CreateSupportingDocumentOptions options, ITwilioRestClient client) { +#if !NET35 + if (options.File != null) + { + return new Request( + HttpMethod.Post, + Rest.Domain.NumbersUpload, + "/v2/RegulatoryCompliance/SupportingDocuments", + postParams: options.GetParams(), + headerParams: null, + files: new List> { new KeyValuePair("File", options.File) } + ); + } +#endif + return new Request( HttpMethod.Post, Rest.Domain.Numbers, @@ -63,7 +77,7 @@ public static SupportingDocumentResource Create(CreateSupportingDocumentOptions return FromJson(response.Content); } - #if !NET35 +#if !NET35 /// /// Create a new Supporting Document. /// @@ -77,7 +91,7 @@ public static async System.Threading.Tasks.Task Crea var response = await client.RequestAsync(BuildCreateRequest(options, client)); return FromJson(response.Content); } - #endif +#endif /// /// Create a new Supporting Document. @@ -96,7 +110,7 @@ public static SupportingDocumentResource Create(string friendlyName, return Create(options, client); } - #if !NET35 +#if !NET35 /// /// Create a new Supporting Document. /// @@ -113,7 +127,7 @@ public static async System.Threading.Tasks.Task Crea var options = new CreateSupportingDocumentOptions(friendlyName, type){Attributes = attributes}; return await CreateAsync(options, client); } - #endif +#endif private static Request BuildReadRequest(ReadSupportingDocumentOptions options, ITwilioRestClient client) { @@ -142,7 +156,7 @@ public static ResourceSet Read(ReadSupportingDocumen return new ResourceSet(page, options, client); } - #if !NET35 +#if !NET35 /// /// Retrieve a list of all Supporting Document for an account. /// @@ -158,7 +172,7 @@ public static async System.Threading.Tasks.Task.FromJson("results", response.Content); return new ResourceSet(page, options, client); } - #endif +#endif /// /// Retrieve a list of all Supporting Document for an account. @@ -175,7 +189,7 @@ public static ResourceSet Read(int? pageSize = null, return Read(options, client); } - #if !NET35 +#if !NET35 /// /// Retrieve a list of all Supporting Document for an account. /// @@ -190,7 +204,7 @@ public static async System.Threading.Tasks.Task /// Fetch the target page of records @@ -272,7 +286,7 @@ public static SupportingDocumentResource Fetch(FetchSupportingDocumentOptions op return FromJson(response.Content); } - #if !NET35 +#if !NET35 /// /// Fetch specific Supporting Document Instance. /// @@ -286,7 +300,7 @@ public static async System.Threading.Tasks.Task Fetc var response = await client.RequestAsync(BuildFetchRequest(options, client)); return FromJson(response.Content); } - #endif +#endif /// /// Fetch specific Supporting Document Instance. @@ -300,7 +314,7 @@ public static SupportingDocumentResource Fetch(string pathSid, ITwilioRestClient return Fetch(options, client); } - #if !NET35 +#if !NET35 /// /// Fetch specific Supporting Document Instance. /// @@ -313,7 +327,7 @@ public static async System.Threading.Tasks.Task Fetc var options = new FetchSupportingDocumentOptions(pathSid); return await FetchAsync(options, client); } - #endif +#endif private static Request BuildUpdateRequest(UpdateSupportingDocumentOptions options, ITwilioRestClient client) { @@ -340,7 +354,7 @@ public static SupportingDocumentResource Update(UpdateSupportingDocumentOptions return FromJson(response.Content); } - #if !NET35 +#if !NET35 /// /// Update an existing Supporting Document. /// @@ -354,7 +368,7 @@ public static async System.Threading.Tasks.Task Upda var response = await client.RequestAsync(BuildUpdateRequest(options, client)); return FromJson(response.Content); } - #endif +#endif /// /// Update an existing Supporting Document. @@ -373,7 +387,7 @@ public static SupportingDocumentResource Update(string pathSid, return Update(options, client); } - #if !NET35 +#if !NET35 /// /// Update an existing Supporting Document. /// @@ -390,7 +404,7 @@ public static async System.Threading.Tasks.Task Upda var options = new UpdateSupportingDocumentOptions(pathSid){FriendlyName = friendlyName, Attributes = attributes}; return await UpdateAsync(options, client); } - #endif +#endif private static Request BuildDeleteRequest(DeleteSupportingDocumentOptions options, ITwilioRestClient client) { @@ -416,7 +430,7 @@ public static bool Delete(DeleteSupportingDocumentOptions options, ITwilioRestCl return response.StatusCode == System.Net.HttpStatusCode.NoContent; } - #if !NET35 +#if !NET35 /// /// Delete a specific Supporting Document. /// @@ -430,7 +444,7 @@ public static async System.Threading.Tasks.Task DeleteAsync(DeleteSupporti var response = await client.RequestAsync(BuildDeleteRequest(options, client)); return response.StatusCode == System.Net.HttpStatusCode.NoContent; } - #endif +#endif /// /// Delete a specific Supporting Document. @@ -444,7 +458,7 @@ public static bool Delete(string pathSid, ITwilioRestClient client = null) return Delete(options, client); } - #if !NET35 +#if !NET35 /// /// Delete a specific Supporting Document. /// @@ -456,7 +470,7 @@ public static async System.Threading.Tasks.Task DeleteAsync(string pathSid var options = new DeleteSupportingDocumentOptions(pathSid); return await DeleteAsync(options, client); } - #endif +#endif /// /// Converts a JSON string into a SupportingDocumentResource object diff --git a/src/Twilio/Types/UploadFile.cs b/src/Twilio/Types/UploadFile.cs new file mode 100644 index 000000000..6f9d0faff --- /dev/null +++ b/src/Twilio/Types/UploadFile.cs @@ -0,0 +1,24 @@ +#if !NET35 +using System.IO; + +namespace Twilio.Types +{ + public interface IUploadFile + { + string FileName { get; } + Stream Stream { get; } + } + + public class UploadFile: IUploadFile + { + public UploadFile(string fileName, Stream stream) + { + FileName = fileName; + Stream = stream; + } + + public string FileName { get; private set; } + public Stream Stream { get; private set; } + } +} +#endif \ No newline at end of file From 1ed5e0db1b92806f4270291f0da46b0775315256 Mon Sep 17 00:00:00 2001 From: Andrew Tsekhansky Date: Fri, 23 Jul 2021 10:25:41 +1200 Subject: [PATCH 2/2] Add unit test --- .../Http/SystemNetHttpClientTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/Twilio.Test/Http/SystemNetHttpClientTest.cs b/test/Twilio.Test/Http/SystemNetHttpClientTest.cs index a18f3ae23..74f2e2dfd 100644 --- a/test/Twilio.Test/Http/SystemNetHttpClientTest.cs +++ b/test/Twilio.Test/Http/SystemNetHttpClientTest.cs @@ -1,12 +1,14 @@ #if !NET35 using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Twilio.Http; +using Twilio.Types; using HttpMethod = Twilio.Http.HttpMethod; namespace Twilio.Tests.Http @@ -23,6 +25,7 @@ public class MockResponse private Dictionary responseMap = new Dictionary(); public HttpRequestMessage InternalRequest { get; private set; } + public List InternalRequestNestedContent { get; private set; } private Random random = new Random(); @@ -59,6 +62,11 @@ protected override Task SendAsync(HttpRequestMessage reques throw response.error; } this.InternalRequest = request; + + if (request.Content is IEnumerable nestedContent) + { + this.InternalRequestNestedContent = nestedContent.ToList(); + } // Inject some randomness for multi-threading scenarios. And for fun! Thread.Sleep(random.Next(100)); @@ -217,6 +225,36 @@ public void TestMakeRequestWithParams() Assert.IsInstanceOf(internalRequest.Content); } + [Test] + public void TestMakeRequestWithParamsAndFile() + { + this._mockHttp.Respond("https://api.twilio.com/v1/Resource.json", HttpStatusCode.OK); + + Request testRequest = new Request(HttpMethod.Post, "https://api.twilio.com/v1/Resource.json"); + testRequest.AddPostParam("post_param", "post_value"); + testRequest.AddQueryParam("query_param", "query_value"); + + UploadFile file = new UploadFile("FileName.txt", new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes("File content"))); + testRequest.Files.Add(new KeyValuePair("File", file)); + + this.TwilioHttpClient.MakeRequest(testRequest); + + HttpRequestMessage internalRequest = this._mockHttp.InternalRequest; + List nestedRequestContent = this._mockHttp.InternalRequestNestedContent; + + Assert.AreEqual("https://api.twilio.com/v1/Resource.json?query_param=query_value", + internalRequest.RequestUri.ToString()); + + Assert.IsNotNull(internalRequest.Content); + Assert.IsInstanceOf(internalRequest.Content); + + Assert.IsNotNull(nestedRequestContent); + Assert.AreEqual(2, nestedRequestContent.Count); + + Assert.IsInstanceOf(nestedRequestContent[0]); + Assert.IsInstanceOf(nestedRequestContent[1]); + } + [Test] public void TestMakeRequestAddsHeadersAndUserAgent() {