Skip to content
Merged
29 changes: 29 additions & 0 deletions APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using APIMatic.Core.Http.Abstractions;

namespace APIMatic.Core.Test.MockTypes.Http.Request
{
public class HttpRequestData : IHttpRequestData
{
public string Method { get; }
public Uri Url { get; }
public IReadOnlyDictionary<string, string[]> Headers { get; }
public Stream Body { get; set; }
public IReadOnlyDictionary<string, string[]> Query { get; }
public IReadOnlyDictionary<string, string> Cookies { get; }
public string Protocol { get; }
public string ContentType { get; }
public long? ContentLength { get; }

public HttpRequestData(
IDictionary<string, string[]> headers,
Stream body)
{
Headers = new ReadOnlyDictionary<string, string[]>(headers);
Body = body;
}
}
}
48 changes: 48 additions & 0 deletions APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using APIMatic.Core.Security.Cryptography;
using APIMatic.Core.Types;
using NUnit.Framework;

namespace APIMatic.Core.Test.Security.Cryptography
{
public class DigestCodecTests
{
[TestCase(EncodingType.Hex, "4A6F686E", new byte[] { 0x4A, 0x6F, 0x68, 0x6E })]
[TestCase(EncodingType.Base64, "SGVsbG8=", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })]
[TestCase(EncodingType.Base64, " ", new byte[]{})]
[TestCase(EncodingType.Base64Url, "SGVsbG8", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })]
[TestCase(EncodingType.Base64Url, "SG", new byte[] { 0x48 })]
public void DigestCodec_Decode_Success(EncodingType encodingType, string input, byte[] expected)
{
var codec = DigestCodec.Create(encodingType);
var result = codec.Decode(input);
Assert.AreEqual(expected, result);
}

[TestCase(EncodingType.Hex, "")]
[TestCase(EncodingType.Hex, null)]
[TestCase(EncodingType.Base64, "")]
[TestCase(EncodingType.Base64, null)]
[TestCase(EncodingType.Base64Url, "")]
[TestCase(EncodingType.Base64Url, null)]
public void DigestCodecIncorrectInput_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input)
{
var codec = DigestCodec.Create(encodingType);
Assert.Throws<ArgumentException>(() => codec.Decode(input));
}

[TestCase(EncodingType.Hex, "ABC")]
public void DigestCodecIncorrectFormat_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input)
{
var codec = DigestCodec.Create(encodingType);
Assert.Throws<FormatException>(() => codec.Decode(input));
}

[TestCase(-1)]
public void DigestCodec_Create_Exception(int invalidValue)
{
var encodingType = (EncodingType)invalidValue;
Assert.Throws<NotSupportedException>(() => DigestCodec.Create(encodingType));
}
}
}
35 changes: 35 additions & 0 deletions APIMatic.Core.Test/Security/HmacFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Security.Cryptography;
using APIMatic.Core.Types.Sdk;
using NUnit.Framework;

namespace APIMatic.Core.Test.Security
{
[TestFixture]
public class HmacFactoryTests
{
[Test]
public void HmacAlgorithmSha256_HmacFactoryCreate_ReturnsHMACSHA256()
{
var key = new byte[] { 1, 2, 3 };
var hmac = HmacFactory.Create(HmacAlgorithm.Sha256, key);
Assert.IsInstanceOf<HMACSHA256>(hmac);
}

[Test]
public void HmacAlgorithmSha512_HmacFactoryCreate_ReturnsHMACSHA512()
{
var key = new byte[] { 4, 5, 6 };
var hmac = HmacFactory.Create(HmacAlgorithm.Sha512, key);
Assert.IsInstanceOf<HMACSHA512>(hmac);
}

[Test]
public void UnsupportedAlgorithm_HmacFactoryCreate_ThrowsNotSupportedException()
{
var key = new byte[] { 7, 8, 9 };
const HmacAlgorithm invalidAlgorithm = (HmacAlgorithm)999;
Assert.Throws<NotSupportedException>(() => HmacFactory.Create(invalidAlgorithm, key));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using APIMatic.Core.Security.SignatureVerifier;
using APIMatic.Core.Test.MockTypes.Http.Request;
using APIMatic.Core.Types;
using NUnit.Framework;

namespace APIMatic.Core.Test.Security.SignatureVerifier;

[TestFixture]
public class HmacSignatureVerifierTests
{
private const string SecretKey = "test_secret";
private const string HeaderName = "X-Signature";
private const string Payload = "hello world";

private static HttpRequestData CreateRequest(string headerValue, string headerName = HeaderName, string payload = Payload)
{
var headers = headerValue == null
? new Dictionary<string, string[]>()
: new Dictionary<string, string[]> { { headerName, new[] { headerValue } } };
return new HttpRequestData(headers, new MemoryStream(System.Text.Encoding.UTF8.GetBytes(payload)));
}

private static HmacSignatureVerifier CreateVerifier(
EncodingType encodingType,
string headerName = HeaderName,
string secretKey = SecretKey,
string signatureValueTemplate = "{digest}")
{
return new HmacSignatureVerifier(secretKey, headerName, encodingType, signatureValueTemplate: signatureValueTemplate);
}

[Test]
public void Constructor_ThrowsOnNullOrEmptySecretKey_OnCreate_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, secretKey: null));
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, secretKey: ""));
}

[Test]
public void Constructor_ThrowsOnNullOrEmptyHeader_OnCreate_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, headerName: null));
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, headerName: ""));
}

[Test]
public async Task NullHeader_OnVerifyAsync_ReturnsFailure()
{
var request = CreateRequest(null);
var verifier = CreateVerifier(EncodingType.Hex);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
Assert.AreEqual($"Signature header '{HeaderName}' is missing.", result.Errors.First());
}

[Test]
public async Task MissingHeader_OnVerifyAsync_ReturnsFailure()
{
var request = CreateRequest(string.Empty);
var verifier = CreateVerifier(EncodingType.Hex);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
Assert.AreEqual($"Malformed signature header '{HeaderName}' value.", result.Errors.First());
}

[Test]
public async Task MalformedHeader_OnVerifyAsync_ReturnsFailure()
{
var request = CreateRequest("");
var verifier = CreateVerifier(EncodingType.Hex);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
StringAssert.Contains("Malformed", result.Errors.First());
}

[Test]
public async Task SignatureDecodingFails_OnVerifyAsync_ReturnsFailure()
{
var request = CreateRequest("not-a-valid-hex");
var verifier = CreateVerifier(EncodingType.Hex);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
}

[TestCase(EncodingType.Hex)]
[TestCase(EncodingType.Base64)]
[TestCase(EncodingType.Base64Url)]
public async Task CorrectSignature_OnVerifyAsync_ReturnsSuccess(EncodingType encodingType)
{
string encodedDigest = GetDigest(encodingType, SecretKey, Payload);
var request = CreateRequest(encodedDigest);
var verifier = CreateVerifier(encodingType);
var result = await verifier.VerifyAsync(request);
Assert.IsTrue(result.IsSuccess);
}

[TestCase(EncodingType.Hex, "deadbeef")]
[TestCase(EncodingType.Base64, "Zm9vYmFyYmF6")]
[TestCase(EncodingType.Base64Url, "Zm9vYmFyYmF6")]
public async Task IncorrectSignature_OnVerifyAsync_ReturnsFailure(EncodingType encodingType, string badDigest)
{
var request = CreateRequest(badDigest);
var verifier = CreateVerifier(encodingType);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
StringAssert.Contains("failed", result.Errors.First());
}

[Test]
public async Task TemplateExtractsDigest_OnVerifyAsync_ReturnsSuccess()
{
var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey));
var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload));
var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
const string template = "prefix-{digest}-suffix";
var signatureValue = $"prefix-{digest}-suffix";
var request = CreateRequest(signatureValue);
var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template);
var result = await verifier.VerifyAsync(request);
Assert.IsTrue(result.IsSuccess);
}

[Test]
public async Task TemplateDoesNotMatch_OnVerifyAsync_ReturnsFailure()
{
const string template = "prefix-{digest}-suffix";
const string signatureValue = $"wrongprefix-deadbeef-wrongsuffix";
var request = CreateRequest(signatureValue);
var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
}

[Test]
public async Task TemplateDoesNotContainDigest_OnVerifyAsync_ReturnsFailure()
{
const string template = "prefix-{wrong}-suffix";
const string signatureValue = $"wrongprefix-deadbeef-wrongsuffix";
var request = CreateRequest(signatureValue);
var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
}

[Test]
public async Task CorrectDigestButIncorrectExpectedTemplate_OnVerifyAsync_ReturnsFailure()
{
var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey));
var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload));
var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
// The expected template doesn't match the signature value template, though digest is correct
const string expectedTemplate = "sha26={digest}";
var signatureValue = $"sha256={digest}";
var request = CreateRequest(signatureValue);
var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: expectedTemplate);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
}

[Test]
public async Task CorrectDigestButIncorrectSignatureValue_OnVerifyAsync_ReturnsFailure()
{
var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey));
var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload));
var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
const string expectedTemplate = "sha256={digest}";
// The signature value does not match the template, though digest is correct
var signatureValue = $"sha25={digest}";
var request = CreateRequest(signatureValue);
var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: expectedTemplate);
var result = await verifier.VerifyAsync(request);
Assert.IsFalse(result.IsSuccess);
}

private static string GetDigest(EncodingType encodingType, string secretKey, string payload)
{
var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secretKey));
var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload));
return encodingType switch
{
EncodingType.Hex => BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(),
EncodingType.Base64 => Convert.ToBase64String(hash),
EncodingType.Base64Url => Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('='),
_ => BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using APIMatic.Core.Security.SignatureVerifier;
using NUnit.Framework;

namespace APIMatic.Core.Test.Security.SignatureVerifier
{
[TestFixture]
public class SignatureVerificationExtensionsTests
{
[TestCase(null, null, true)]
[TestCase(null, new byte[] { 1, 2, 3 }, false)]
[TestCase(new byte[] { 1, 2, 3 }, null, false)]
[TestCase(new byte[] { 1, 2, 3 }, new byte[] { 1, 2 }, false)]
[TestCase(new byte[] { }, new byte[] { }, true)]
[TestCase(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 4 }, true)]
[TestCase(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 5 }, false)]
public void ConstantTimeEquals_VariousInputs_ReturnsExpected(byte[] a, byte[] b, bool expected)
{
Assert.AreEqual(expected, SignatureVerifierExtensions.FixedTimeEquals(a, b));
}
}
}
28 changes: 28 additions & 0 deletions APIMatic.Core.Test/Utilities/IHttpRequestDataExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using APIMatic.Core.Test.MockTypes.Http.Request;
using APIMatic.Core.Utilities;
using NUnit.Framework;

namespace APIMatic.Core.Test.Utilities
{
[TestFixture]
public class IHttpRequestDataExtensionsTests
{

[TestCase(null, new byte[0])]
[TestCase("", new byte[0])]
[TestCase("hello", new byte[] { 104, 101, 108, 108, 111 })]
public async Task ReadBodyStreamToByteArrayAsync_VariousBodies_ReturnsExpected(string body, byte[] expected)
{
var stream = body == null ? null : new MemoryStream(Encoding.UTF8.GetBytes(body));
var request = new HttpRequestData(new Dictionary<string, string[]>(), stream);

var result = await request.ReadBodyStreamToByteArrayAsync();

Assert.AreEqual(expected, result);
}
}
}
13 changes: 13 additions & 0 deletions APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,18 @@ public void ResolveScopedJsonValue_BooleanToken_ReturnsTokenToString()

Assert.AreEqual("True", result);
}

[TestCase("#/name", "{\"name\":\"John\",\"age\":30}", "John")]
[TestCase("#/invalid", "{\"name\":\"John\"}", null)]
[TestCase(null, "{\"name\":\"John\"}", null)]
[TestCase("", "{\"name\":\"John\"}", null)]
[TestCase("/name", "{\"name\":\"John\"}", null)]
[TestCase("#/age", "{\"age\":30}", "30")]
[TestCase("#/name", null, null)]
public void ResolveJsonValue_VariousCases_ReturnsExpected(string jsonPointer, string json, string expected)
{
var result = JsonPointerResolver.ResolveJsonValue(jsonPointer, json);
Assert.AreEqual(expected, result);
}
}
}
3 changes: 3 additions & 0 deletions APIMatic.Core/APIMatic.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@
<ItemGroup>
<InternalsVisibleTo Include="APIMatic.Core.Test" />
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Abstractions\" />
</ItemGroup>

</Project>
Loading
Loading