Skip to content

Commit 0f7395f

Browse files
authored
Add support for OAuth in Api calls
1 parent fc556bc commit 0f7395f

File tree

9 files changed

+279
-8
lines changed

9 files changed

+279
-8
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using NUnit.Framework;
3+
4+
namespace CloudinaryDotNet.IntegrationTests.AdminApi
5+
{
6+
public class OAuthTest: IntegrationTestBase
7+
{
8+
private const string FAKE_OAUTH_TOKEN = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4";
9+
private static string m_uniqueImagePublicId;
10+
11+
public override void Initialize()
12+
{
13+
base.Initialize();
14+
15+
m_uniqueImagePublicId = $"asset_image_{m_uniqueTestId}";
16+
}
17+
18+
[Test]
19+
public void TestOAuthToken()
20+
{
21+
var result = m_cloudinary.GetResource(m_uniqueImagePublicId);
22+
Assert.IsNotNull(result.Error);
23+
Assert.IsTrue(result.Error.Message.Contains("Invalid token"));
24+
}
25+
26+
protected override Account GetAccountInstance()
27+
{
28+
Account account = new Account(m_cloudName, FAKE_OAUTH_TOKEN);
29+
30+
Assert.IsNotEmpty(account.Cloud, $"Cloud name must be specified in {CONFIG_PLACE}");
31+
Assert.IsNotEmpty(account.OAuthToken);
32+
33+
return account;
34+
}
35+
}
36+
}

CloudinaryDotNet.IntegrationTests/IntegrationTestBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ private void SaveEmbeddedToDisk(Assembly assembly, string sourcePath, string des
200200
stream.CopyTo(fileStream);
201201
}
202202
}
203-
catch (IOException)
203+
catch
204204
{
205205

206206
}
@@ -349,7 +349,7 @@ private void PopulateMissingRawUploadParams(RawUploadParams uploadParams, bool i
349349
/// A convenient method for initialization of new Account instance with necessary checks
350350
/// </summary>
351351
/// <returns>New Account instance</returns>
352-
private Account GetAccountInstance()
352+
protected virtual Account GetAccountInstance()
353353
{
354354
Account account = new Account(m_cloudName, m_apiKey, m_apiSecret);
355355

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Net.Http.Headers;
5+
using System.Threading.Tasks;
6+
using CloudinaryDotNet.Actions;
7+
using NUnit.Framework;
8+
9+
namespace CloudinaryDotNet.Tests
10+
{
11+
public class ApiAuthorizationTest
12+
{
13+
private const string m_oauthToken = "NTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZj17";
14+
private const string m_cloudName = "test123";
15+
private const string m_apiKey = "key";
16+
private const string m_apiSecret = "secret";
17+
private MockedCloudinary m_mockedCloudinary;
18+
19+
[Test]
20+
public async Task TestOAuthTokenAdminApi()
21+
{
22+
InitCloudinaryApi();
23+
24+
await m_mockedCloudinary.PingAsync();
25+
26+
AssertHasBearerAuthorization(m_mockedCloudinary, m_oauthToken);
27+
}
28+
29+
[Test]
30+
public async Task TestKeyAndSecretAdminApi()
31+
{
32+
InitCloudinaryApi(m_apiKey, m_apiSecret);
33+
34+
await m_mockedCloudinary.PingAsync();
35+
36+
AssertHasBasicAuthorization(m_mockedCloudinary, "a2V5OnNlY3JldA==");
37+
}
38+
39+
[Test]
40+
public async Task TestOAuthTokenUploadApi()
41+
{
42+
InitCloudinaryApi();
43+
44+
var uploadParams = new ImageUploadParams()
45+
{
46+
File = GetFileDescription()
47+
};
48+
49+
await m_mockedCloudinary.UploadAsync(uploadParams);
50+
51+
AssertHasBearerAuthorization(m_mockedCloudinary, m_oauthToken);
52+
Assert.IsFalse(m_mockedCloudinary.HttpRequestContent.Contains("signature"));
53+
}
54+
55+
[Test]
56+
public async Task TestKeyAndSecretUploadApi()
57+
{
58+
InitCloudinaryApi(m_apiKey, m_apiSecret);
59+
60+
var uploadParams = new ImageUploadParams()
61+
{
62+
File = GetFileDescription()
63+
};
64+
await m_mockedCloudinary.UploadAsync(uploadParams);
65+
66+
AssertUploadSignature();
67+
}
68+
69+
[TestCaseSource(typeof(UploadApiProvider), nameof(UploadApiProvider.UploadApis))]
70+
public async Task TestUploadAuthorization(Func<MockedCloudinary, Task> func)
71+
{
72+
InitCloudinaryApi(m_apiKey, m_apiSecret);
73+
74+
await func(m_mockedCloudinary);
75+
76+
AssertUploadSignature();
77+
}
78+
79+
private static FileDescription GetFileDescription()
80+
=> new FileDescription("foo", new MemoryStream(new byte[5]));
81+
82+
private void AssertUploadSignature()
83+
{
84+
var httpRequestContent = m_mockedCloudinary.HttpRequestContent;
85+
Assert.IsTrue(httpRequestContent.Contains("signature"));
86+
Assert.IsTrue(httpRequestContent.Contains("api_key"));
87+
}
88+
89+
[Test]
90+
public async Task TestMissingCredentialsUploadApi()
91+
{
92+
InitCloudinaryApi(null, null);
93+
94+
var uploadParams = new ImageUploadParams()
95+
{
96+
File = new FileDescription(Path.GetTempFileName()),
97+
Unsigned = true,
98+
UploadPreset = "api_test_upload_preset"
99+
};
100+
101+
await m_mockedCloudinary.UploadAsync(uploadParams);
102+
103+
Assert.IsTrue(m_mockedCloudinary.HttpRequestContent.Contains("upload_preset"));
104+
}
105+
106+
private void InitCloudinaryApi()
107+
{
108+
m_mockedCloudinary = new MockedCloudinary(account: new Account(m_cloudName, m_oauthToken));
109+
}
110+
111+
private void InitCloudinaryApi(string apiKey, string apiSecret)
112+
{
113+
m_mockedCloudinary = new MockedCloudinary(account: new Account(m_cloudName, apiKey, apiSecret));
114+
}
115+
116+
private void AssertHasAuthorization(MockedCloudinary cloudinary, string scheme, string value) =>
117+
Assert.AreEqual(cloudinary.HttpRequestHeaders.Authorization, new AuthenticationHeaderValue(scheme, value));
118+
119+
private void AssertHasBearerAuthorization(MockedCloudinary cloudinary, string value) =>
120+
AssertHasAuthorization(cloudinary, "Bearer", value);
121+
122+
private void AssertHasBasicAuthorization(MockedCloudinary cloudinary, string value) =>
123+
AssertHasAuthorization(cloudinary, "Basic", value);
124+
125+
private static class UploadApiProvider
126+
{
127+
public static IEnumerable<object> UploadApis()
128+
{
129+
yield return new Func<MockedCloudinary, Task>[]
130+
{ m => m.UploadAsync(new VideoUploadParams { File = GetFileDescription() }) };
131+
132+
yield return new Func<MockedCloudinary, Task>[]
133+
{ m => m.UploadAsync(new ImageUploadParams { File = GetFileDescription() }) };
134+
135+
yield return new Func<MockedCloudinary, Task>[]
136+
{ m => m.UploadAsync(new RawUploadParams { File = GetFileDescription() }) };
137+
138+
yield return new Func<MockedCloudinary, Task>[]
139+
{ m => m.UploadLargeAsync(new RawUploadParams { File = GetFileDescription() }) };
140+
141+
yield return new Func<MockedCloudinary, Task>[]
142+
{ m => m.UploadLargeRawAsync(new RawUploadParams { File = GetFileDescription() }) };
143+
144+
yield return new Func<MockedCloudinary, Task>[]
145+
{ m => m.TagAsync(new TagParams()) };
146+
147+
yield return new Func<MockedCloudinary, Task>[]
148+
{ m => m.ContextAsync(new ContextParams()) };
149+
150+
yield return new Func<MockedCloudinary, Task>[]
151+
{ m => m.ExplicitAsync(new ExplicitParams("id")) };
152+
153+
yield return new Func<MockedCloudinary, Task>[]
154+
{ m => m.ExplodeAsync(new ExplodeParams("id", new Transformation())) };
155+
156+
yield return new Func<MockedCloudinary, Task>[]
157+
{ m => m.CreateZipAsync(new ArchiveParams().PublicIds(new List<string> { "id" })) };
158+
159+
yield return new Func<MockedCloudinary, Task>[]
160+
{ m => m.CreateArchiveAsync(new ArchiveParams().PublicIds(new List<string> { "id" })) };
161+
162+
yield return new Func<MockedCloudinary, Task>[]
163+
{ m => m.MakeSpriteAsync(new SpriteParams("tag")) };
164+
165+
yield return new Func<MockedCloudinary, Task>[]
166+
{ m => m.MultiAsync(new MultiParams("tag")) };
167+
168+
yield return new Func<MockedCloudinary, Task>[]
169+
{ m => m.TextAsync(new TextParams("text")) };
170+
171+
yield return new Func<MockedCloudinary, Task>[]
172+
{ m => m.CreateSlideshowAsync(
173+
new CreateSlideshowParams { ManifestTransformation = new Transformation() }) };
174+
}
175+
}
176+
}
177+
}

CloudinaryDotNet.Tests/MockedCloudinary.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Net;
1+
using System;
2+
using System.Net;
23
using System.Net.Http;
34
using System.Net.Http.Headers;
45
using System.Threading;
@@ -14,8 +15,10 @@ public class MockedCloudinary : Cloudinary
1415
public Mock<HttpMessageHandler> HandlerMock;
1516
public string HttpRequestContent;
1617
private const string CloudName = "test123";
18+
public HttpRequestHeaders HttpRequestHeaders;
1719

18-
public MockedCloudinary(string responseStr = "{}", HttpResponseHeaders httpResponseHeaders = null) : base("cloudinary://key:secret@test123")
20+
public MockedCloudinary(string responseStr = "{}", HttpResponseHeaders httpResponseHeaders = null, Account account = null)
21+
: base(account ?? new Account(CloudName, "key", "secret"))
1922
{
2023
HandlerMock = new Mock<HttpMessageHandler>();
2124

@@ -45,6 +48,7 @@ public MockedCloudinary(string responseStr = "{}", HttpResponseHeaders httpRespo
4548
.ReadAsStringAsync()
4649
.GetAwaiter()
4750
.GetResult();
51+
HttpRequestHeaders = httpRequestMessage.Headers;
4852
})
4953
.ReturnsAsync(httpResponseMessage);
5054
Api.Client = new HttpClient(HandlerMock.Object);
@@ -70,7 +74,6 @@ public void AssertHttpCall(System.Net.Http.HttpMethod httpMethod, string localPa
7074
);
7175
}
7276

73-
7477
public JToken RequestJson()
7578
{
7679
return JToken.Parse(HttpRequestContent);

CloudinaryDotNet/Account.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ public Account(string cloud, string apiKey, string apiSecret)
3030
ApiSecret = apiSecret;
3131
}
3232

33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="Account"/> class.
35+
/// Parameterized constructor.
36+
/// </summary>
37+
/// <param name="cloud">Cloud name.</param>
38+
/// <param name="oauthToken">OAuth token.</param>
39+
public Account(string cloud, string oauthToken)
40+
: this(cloud)
41+
{
42+
OAuthToken = oauthToken;
43+
}
44+
3345
/// <summary>
3446
/// Initializes a new instance of the <see cref="Account"/> class.
3547
/// Parameterized constructor.
@@ -54,5 +66,10 @@ public Account(string cloud)
5466
/// Gets or sets API secret.
5567
/// </summary>
5668
public string ApiSecret { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets oauth token.
72+
/// </summary>
73+
public string OAuthToken { get; set; }
5774
}
5875
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace CloudinaryDotNet.Actions
2+
{
3+
using System.Runtime.Serialization;
4+
5+
/// <summary>
6+
/// Parsed details of a single ping request.
7+
/// </summary>
8+
[DataContract]
9+
public class PingResult : BaseResult
10+
{
11+
}
12+
}

CloudinaryDotNet/ApiShared.Internal.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ internal void FinalizeUploadParameters(IDictionary<string, object> parameters)
187187
parameters.Add("timestamp", Utils.UnixTimeNowSeconds());
188188
}
189189

190-
if (!parameters.ContainsKey("signature"))
190+
if (!parameters.ContainsKey("signature") && string.IsNullOrEmpty(Account.OAuthToken))
191191
{
192192
parameters.Add("signature", SignParameters(parameters));
193193
}
@@ -548,6 +548,13 @@ private void SetChunkedEncoding(HttpRequestMessage request)
548548
}
549549
}
550550

551+
private AuthenticationHeaderValue GetAuthorizationHeaderValue()
552+
{
553+
return !string.IsNullOrEmpty(Account.OAuthToken)
554+
? new AuthenticationHeaderValue("Bearer", Account.OAuthToken)
555+
: new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(GetApiCredentials())));
556+
}
557+
551558
private void PrePrepareRequestBody(
552559
HttpRequestMessage request,
553560
HttpMethod method,
@@ -562,8 +569,7 @@ private void PrePrepareRequestBody(
562569
: string.Format(CultureInfo.InvariantCulture, "{0} {1}", UserPlatform, USER_AGENT);
563570
request.Headers.Add("User-Agent", userPlatform);
564571

565-
byte[] authBytes = Encoding.ASCII.GetBytes(GetApiCredentials());
566-
request.Headers.Add("Authorization", string.Format(CultureInfo.InvariantCulture, "Basic {0}", Convert.ToBase64String(authBytes)));
572+
request.Headers.Authorization = GetAuthorizationHeaderValue();
567573

568574
if (extraHeaders != null)
569575
{

CloudinaryDotNet/Cloudinary.AdminApi.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,25 @@ public Task<UsageResult> GetUsageAsync(CancellationToken? cancellationToken = nu
10661066
cancellationToken);
10671067
}
10681068

1069+
/// <summary>
1070+
/// Tests the reachability of the Cloudinary API.
1071+
/// </summary>
1072+
/// <returns>Ping result.</returns>
1073+
public PingResult Ping()
1074+
{
1075+
return PingAsync(null).GetAwaiter().GetResult();
1076+
}
1077+
1078+
/// <summary>
1079+
/// Tests the reachability of the Cloudinary API asynchronously.
1080+
/// </summary>
1081+
/// <param name="cancellationToken">(Optional) Cancellation token.</param>
1082+
/// <returns>Ping result.</returns>
1083+
public Task<PingResult> PingAsync(CancellationToken? cancellationToken = null)
1084+
{
1085+
return CallAdminApiAsync<PingResult>(HttpMethod.GET, GetApiUrlV().BuildUrl("ping"), null, cancellationToken);
1086+
}
1087+
10691088
/// <summary>
10701089
/// Gets a list of tags asynchronously.
10711090
/// </summary>

CloudinaryDotNet/CloudinaryDotNet.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3+
<LangVersion>9.0</LangVersion>
34
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
45
<TargetFrameworks>netstandard1.3;netstandard2.0;net452</TargetFrameworks>
56
<AssemblyName>CloudinaryDotNet</AssemblyName>

0 commit comments

Comments
 (0)