Browse Source
* Add more logging to PayPal IPN webhook * Add PayPalIPNClient tests * Add PayPalControllerTests --------- Co-authored-by: aelinton <95626935+aelinton@users.noreply.github.com>
19 changed files with 1543 additions and 323 deletions
@ -0,0 +1,110 @@ |
|||||||
|
using System.Globalization; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Web; |
||||||
|
|
||||||
|
namespace Bit.Billing.Models; |
||||||
|
|
||||||
|
public class PayPalIPNTransactionModel |
||||||
|
{ |
||||||
|
public string TransactionId { get; } |
||||||
|
public string TransactionType { get; } |
||||||
|
public string ParentTransactionId { get; } |
||||||
|
public string PaymentStatus { get; } |
||||||
|
public string PaymentType { get; } |
||||||
|
public decimal MerchantGross { get; } |
||||||
|
public string MerchantCurrency { get; } |
||||||
|
public string ReceiverId { get; } |
||||||
|
public DateTime PaymentDate { get; } |
||||||
|
public Guid? UserId { get; } |
||||||
|
public Guid? OrganizationId { get; } |
||||||
|
public bool IsAccountCredit { get; } |
||||||
|
|
||||||
|
public PayPalIPNTransactionModel(string formData) |
||||||
|
{ |
||||||
|
var queryString = HttpUtility.ParseQueryString(formData); |
||||||
|
|
||||||
|
var data = queryString |
||||||
|
.AllKeys |
||||||
|
.ToDictionary(key => key, key => queryString[key]); |
||||||
|
|
||||||
|
TransactionId = Extract(data, "txn_id"); |
||||||
|
TransactionType = Extract(data, "txn_type"); |
||||||
|
ParentTransactionId = Extract(data, "parent_txn_id"); |
||||||
|
PaymentStatus = Extract(data, "payment_status"); |
||||||
|
PaymentType = Extract(data, "payment_type"); |
||||||
|
|
||||||
|
var merchantGross = Extract(data, "mc_gross"); |
||||||
|
if (!string.IsNullOrEmpty(merchantGross)) |
||||||
|
{ |
||||||
|
MerchantGross = decimal.Parse(merchantGross); |
||||||
|
} |
||||||
|
|
||||||
|
MerchantCurrency = Extract(data, "mc_currency"); |
||||||
|
ReceiverId = Extract(data, "receiver_id"); |
||||||
|
|
||||||
|
var paymentDate = Extract(data, "payment_date"); |
||||||
|
PaymentDate = ToUTCDateTime(paymentDate); |
||||||
|
|
||||||
|
var custom = Extract(data, "custom"); |
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(custom)) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var metadata = custom.Split(',') |
||||||
|
.Where(field => !string.IsNullOrEmpty(field) && field.Contains(':')) |
||||||
|
.Select(field => field.Split(':')) |
||||||
|
.ToDictionary(parts => parts[0], parts => parts[1]); |
||||||
|
|
||||||
|
if (metadata.TryGetValue("user_id", out var userIdStr) && |
||||||
|
Guid.TryParse(userIdStr, out var userId)) |
||||||
|
{ |
||||||
|
UserId = userId; |
||||||
|
} |
||||||
|
|
||||||
|
if (metadata.TryGetValue("organization_id", out var organizationIdStr) && |
||||||
|
Guid.TryParse(organizationIdStr, out var organizationId)) |
||||||
|
{ |
||||||
|
OrganizationId = organizationId; |
||||||
|
} |
||||||
|
|
||||||
|
IsAccountCredit = custom.Contains("account_credit:1"); |
||||||
|
} |
||||||
|
|
||||||
|
private static string Extract(IReadOnlyDictionary<string, string> data, string key) |
||||||
|
{ |
||||||
|
var success = data.TryGetValue(key, out var value); |
||||||
|
return success ? value : null; |
||||||
|
} |
||||||
|
|
||||||
|
private static DateTime ToUTCDateTime(string input) |
||||||
|
{ |
||||||
|
if (string.IsNullOrEmpty(input)) |
||||||
|
{ |
||||||
|
return default; |
||||||
|
} |
||||||
|
|
||||||
|
var success = DateTime.TryParseExact(input, |
||||||
|
new[] |
||||||
|
{ |
||||||
|
"HH:mm:ss dd MMM yyyy PDT", |
||||||
|
"HH:mm:ss dd MMM yyyy PST", |
||||||
|
"HH:mm:ss dd MMM, yyyy PST", |
||||||
|
"HH:mm:ss dd MMM, yyyy PDT", |
||||||
|
"HH:mm:ss MMM dd, yyyy PST", |
||||||
|
"HH:mm:ss MMM dd, yyyy PDT" |
||||||
|
}, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime); |
||||||
|
|
||||||
|
if (!success) |
||||||
|
{ |
||||||
|
return default; |
||||||
|
} |
||||||
|
|
||||||
|
var pacificTime = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) |
||||||
|
? TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") |
||||||
|
: TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); |
||||||
|
|
||||||
|
return TimeZoneInfo.ConvertTimeToUtc(dateTime, pacificTime); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace Bit.Billing.Services; |
||||||
|
|
||||||
|
public interface IPayPalIPNClient |
||||||
|
{ |
||||||
|
Task<bool> VerifyIPN(Guid entityId, string formData); |
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
using System.Text; |
||||||
|
using Microsoft.Extensions.Options; |
||||||
|
|
||||||
|
namespace Bit.Billing.Services.Implementations; |
||||||
|
|
||||||
|
public class PayPalIPNClient : IPayPalIPNClient |
||||||
|
{ |
||||||
|
private readonly HttpClient _httpClient; |
||||||
|
private readonly Uri _ipnEndpoint; |
||||||
|
private readonly ILogger<PayPalIPNClient> _logger; |
||||||
|
|
||||||
|
public PayPalIPNClient( |
||||||
|
IOptions<BillingSettings> billingSettings, |
||||||
|
HttpClient httpClient, |
||||||
|
ILogger<PayPalIPNClient> logger) |
||||||
|
{ |
||||||
|
_httpClient = httpClient; |
||||||
|
_ipnEndpoint = new Uri(billingSettings.Value.PayPal.Production |
||||||
|
? "https://www.paypal.com/cgi-bin/webscr" |
||||||
|
: "https://www.sandbox.paypal.com/cgi-bin/webscr"); |
||||||
|
_logger = logger; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<bool> VerifyIPN(Guid entityId, string formData) |
||||||
|
{ |
||||||
|
LogInfo(entityId, $"Verifying IPN against {_ipnEndpoint}"); |
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(formData)) |
||||||
|
{ |
||||||
|
throw new ArgumentNullException(nameof(formData)); |
||||||
|
} |
||||||
|
|
||||||
|
var requestMessage = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = _ipnEndpoint }; |
||||||
|
|
||||||
|
var requestContent = string.Concat("cmd=_notify-validate&", formData); |
||||||
|
|
||||||
|
LogInfo(entityId, $"Request Content: {requestContent}"); |
||||||
|
|
||||||
|
requestMessage.Content = new StringContent(requestContent, Encoding.UTF8, "application/x-www-form-urlencoded"); |
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(requestMessage); |
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(); |
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode) |
||||||
|
{ |
||||||
|
return responseContent switch |
||||||
|
{ |
||||||
|
"VERIFIED" => Verified(), |
||||||
|
"INVALID" => Invalid(), |
||||||
|
_ => Unhandled(responseContent) |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
LogError(entityId, $"Unsuccessful Response | Status Code: {response.StatusCode} | Content: {responseContent}"); |
||||||
|
|
||||||
|
return false; |
||||||
|
|
||||||
|
bool Verified() |
||||||
|
{ |
||||||
|
LogInfo(entityId, "Verified"); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
bool Invalid() |
||||||
|
{ |
||||||
|
LogError(entityId, "Verification Invalid"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
bool Unhandled(string content) |
||||||
|
{ |
||||||
|
LogWarning(entityId, $"Unhandled Response Content: {content}"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void LogInfo(Guid entityId, string message) |
||||||
|
=> _logger.LogInformation("Verify PayPal IPN ({RequestId}) | {Message}", entityId, message); |
||||||
|
|
||||||
|
private void LogWarning(Guid entityId, string message) |
||||||
|
=> _logger.LogWarning("Verify PayPal IPN ({RequestId}) | {Message}", entityId, message); |
||||||
|
|
||||||
|
private void LogError(Guid entityId, string message) |
||||||
|
=> _logger.LogError("Verify PayPal IPN ({RequestId}) | {Message}", entityId, message); |
||||||
|
} |
||||||
@ -1,176 +0,0 @@ |
|||||||
using System.Globalization; |
|
||||||
using System.Runtime.InteropServices; |
|
||||||
using System.Text; |
|
||||||
using System.Web; |
|
||||||
using Microsoft.Extensions.Options; |
|
||||||
|
|
||||||
namespace Bit.Billing.Utilities; |
|
||||||
|
|
||||||
public class PayPalIpnClient |
|
||||||
{ |
|
||||||
private readonly HttpClient _httpClient = new HttpClient(); |
|
||||||
private readonly Uri _ipnUri; |
|
||||||
private readonly ILogger<PayPalIpnClient> _logger; |
|
||||||
|
|
||||||
public PayPalIpnClient(IOptions<BillingSettings> billingSettings, ILogger<PayPalIpnClient> logger) |
|
||||||
{ |
|
||||||
var bSettings = billingSettings?.Value; |
|
||||||
_logger = logger; |
|
||||||
_ipnUri = new Uri(bSettings.PayPal.Production ? "https://www.paypal.com/cgi-bin/webscr" : |
|
||||||
"https://www.sandbox.paypal.com/cgi-bin/webscr"); |
|
||||||
} |
|
||||||
|
|
||||||
public async Task<bool> VerifyIpnAsync(string ipnBody) |
|
||||||
{ |
|
||||||
_logger.LogInformation("Verifying IPN with PayPal at {Timestamp}: {VerificationUri}", DateTime.UtcNow, _ipnUri); |
|
||||||
if (ipnBody == null) |
|
||||||
{ |
|
||||||
_logger.LogError("No IPN body."); |
|
||||||
throw new ArgumentException("No IPN body."); |
|
||||||
} |
|
||||||
|
|
||||||
var request = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = _ipnUri }; |
|
||||||
var cmdIpnBody = string.Concat("cmd=_notify-validate&", ipnBody); |
|
||||||
request.Content = new StringContent(cmdIpnBody, Encoding.UTF8, "application/x-www-form-urlencoded"); |
|
||||||
var response = await _httpClient.SendAsync(request); |
|
||||||
if (!response.IsSuccessStatusCode) |
|
||||||
{ |
|
||||||
_logger.LogError("Failed to receive a successful response from PayPal IPN verification service. Response: {Response}", response); |
|
||||||
throw new Exception("Failed to verify IPN, status: " + response.StatusCode); |
|
||||||
} |
|
||||||
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync(); |
|
||||||
if (responseContent.Equals("VERIFIED")) |
|
||||||
{ |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
if (responseContent.Equals("INVALID")) |
|
||||||
{ |
|
||||||
_logger.LogWarning("Received an INVALID response from PayPal: {ResponseContent}", responseContent); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
_logger.LogError("Failed to verify IPN: {ResponseContent}", responseContent); |
|
||||||
throw new Exception("Failed to verify IPN."); |
|
||||||
} |
|
||||||
|
|
||||||
public class IpnTransaction |
|
||||||
{ |
|
||||||
private string[] _dateFormats = new string[] |
|
||||||
{ |
|
||||||
"HH:mm:ss dd MMM yyyy PDT", "HH:mm:ss dd MMM yyyy PST", "HH:mm:ss dd MMM, yyyy PST", |
|
||||||
"HH:mm:ss dd MMM, yyyy PDT","HH:mm:ss MMM dd, yyyy PST", "HH:mm:ss MMM dd, yyyy PDT" |
|
||||||
}; |
|
||||||
|
|
||||||
public IpnTransaction(string ipnFormData) |
|
||||||
{ |
|
||||||
if (string.IsNullOrWhiteSpace(ipnFormData)) |
|
||||||
{ |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
var qsData = HttpUtility.ParseQueryString(ipnFormData); |
|
||||||
var dataDict = qsData.Keys.Cast<string>().ToDictionary(k => k, v => qsData[v].ToString()); |
|
||||||
|
|
||||||
TxnId = GetDictValue(dataDict, "txn_id"); |
|
||||||
TxnType = GetDictValue(dataDict, "txn_type"); |
|
||||||
ParentTxnId = GetDictValue(dataDict, "parent_txn_id"); |
|
||||||
PaymentStatus = GetDictValue(dataDict, "payment_status"); |
|
||||||
PaymentType = GetDictValue(dataDict, "payment_type"); |
|
||||||
McCurrency = GetDictValue(dataDict, "mc_currency"); |
|
||||||
Custom = GetDictValue(dataDict, "custom"); |
|
||||||
ItemName = GetDictValue(dataDict, "item_name"); |
|
||||||
ItemNumber = GetDictValue(dataDict, "item_number"); |
|
||||||
PayerId = GetDictValue(dataDict, "payer_id"); |
|
||||||
PayerEmail = GetDictValue(dataDict, "payer_email"); |
|
||||||
ReceiverId = GetDictValue(dataDict, "receiver_id"); |
|
||||||
ReceiverEmail = GetDictValue(dataDict, "receiver_email"); |
|
||||||
|
|
||||||
PaymentDate = ConvertDate(GetDictValue(dataDict, "payment_date")); |
|
||||||
|
|
||||||
var mcGrossString = GetDictValue(dataDict, "mc_gross"); |
|
||||||
if (!string.IsNullOrWhiteSpace(mcGrossString) && decimal.TryParse(mcGrossString, out var mcGross)) |
|
||||||
{ |
|
||||||
McGross = mcGross; |
|
||||||
} |
|
||||||
var mcFeeString = GetDictValue(dataDict, "mc_fee"); |
|
||||||
if (!string.IsNullOrWhiteSpace(mcFeeString) && decimal.TryParse(mcFeeString, out var mcFee)) |
|
||||||
{ |
|
||||||
McFee = mcFee; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public string TxnId { get; set; } |
|
||||||
public string TxnType { get; set; } |
|
||||||
public string ParentTxnId { get; set; } |
|
||||||
public string PaymentStatus { get; set; } |
|
||||||
public string PaymentType { get; set; } |
|
||||||
public decimal McGross { get; set; } |
|
||||||
public decimal McFee { get; set; } |
|
||||||
public string McCurrency { get; set; } |
|
||||||
public string Custom { get; set; } |
|
||||||
public string ItemName { get; set; } |
|
||||||
public string ItemNumber { get; set; } |
|
||||||
public string PayerId { get; set; } |
|
||||||
public string PayerEmail { get; set; } |
|
||||||
public string ReceiverId { get; set; } |
|
||||||
public string ReceiverEmail { get; set; } |
|
||||||
public DateTime PaymentDate { get; set; } |
|
||||||
|
|
||||||
public Tuple<Guid?, Guid?> GetIdsFromCustom() |
|
||||||
{ |
|
||||||
Guid? orgId = null; |
|
||||||
Guid? userId = null; |
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Custom) && Custom.Contains(":")) |
|
||||||
{ |
|
||||||
var mainParts = Custom.Split(','); |
|
||||||
foreach (var mainPart in mainParts) |
|
||||||
{ |
|
||||||
var parts = mainPart.Split(':'); |
|
||||||
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id)) |
|
||||||
{ |
|
||||||
if (parts[0] == "user_id") |
|
||||||
{ |
|
||||||
userId = id; |
|
||||||
} |
|
||||||
else if (parts[0] == "organization_id") |
|
||||||
{ |
|
||||||
orgId = id; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return new Tuple<Guid?, Guid?>(orgId, userId); |
|
||||||
} |
|
||||||
|
|
||||||
public bool IsAccountCredit() |
|
||||||
{ |
|
||||||
return !string.IsNullOrWhiteSpace(Custom) && Custom.Contains("account_credit:1"); |
|
||||||
} |
|
||||||
|
|
||||||
private string GetDictValue(IDictionary<string, string> dict, string key) |
|
||||||
{ |
|
||||||
return dict.ContainsKey(key) ? dict[key] : null; |
|
||||||
} |
|
||||||
|
|
||||||
private DateTime ConvertDate(string dateString) |
|
||||||
{ |
|
||||||
if (!string.IsNullOrWhiteSpace(dateString)) |
|
||||||
{ |
|
||||||
var parsed = DateTime.TryParseExact(dateString, _dateFormats, |
|
||||||
CultureInfo.InvariantCulture, DateTimeStyles.None, out var paymentDate); |
|
||||||
if (parsed) |
|
||||||
{ |
|
||||||
var pacificTime = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? |
|
||||||
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") : |
|
||||||
TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); |
|
||||||
return TimeZoneInfo.ConvertTimeToUtc(paymentDate, pacificTime); |
|
||||||
} |
|
||||||
} |
|
||||||
return default(DateTime); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,644 @@ |
|||||||
|
using System.Text; |
||||||
|
using Bit.Billing.Controllers; |
||||||
|
using Bit.Billing.Services; |
||||||
|
using Bit.Billing.Test.Utilities; |
||||||
|
using Bit.Core.AdminConsole.Entities; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Enums; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.Services; |
||||||
|
using Divergic.Logging.Xunit; |
||||||
|
using FluentAssertions; |
||||||
|
using Microsoft.AspNetCore.Http; |
||||||
|
using Microsoft.AspNetCore.Mvc; |
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using Microsoft.Extensions.Options; |
||||||
|
using Microsoft.Extensions.Primitives; |
||||||
|
using NSubstitute; |
||||||
|
using NSubstitute.ReturnsExtensions; |
||||||
|
using Xunit; |
||||||
|
using Xunit.Abstractions; |
||||||
|
using Transaction = Bit.Core.Entities.Transaction; |
||||||
|
|
||||||
|
namespace Bit.Billing.Test.Controllers; |
||||||
|
|
||||||
|
public class PayPalControllerTests |
||||||
|
{ |
||||||
|
private readonly ITestOutputHelper _testOutputHelper; |
||||||
|
|
||||||
|
private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>(); |
||||||
|
private readonly IMailService _mailService = Substitute.For<IMailService>(); |
||||||
|
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>(); |
||||||
|
private readonly IPaymentService _paymentService = Substitute.For<IPaymentService>(); |
||||||
|
private readonly IPayPalIPNClient _payPalIPNClient = Substitute.For<IPayPalIPNClient>(); |
||||||
|
private readonly ITransactionRepository _transactionRepository = Substitute.For<ITransactionRepository>(); |
||||||
|
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>(); |
||||||
|
|
||||||
|
private const string _defaultWebhookKey = "webhook-key"; |
||||||
|
|
||||||
|
public PayPalControllerTests(ITestOutputHelper testOutputHelper) |
||||||
|
{ |
||||||
|
_testOutputHelper = testOutputHelper; |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_NullKey_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, null, null); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN: Key is missing"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_IncorrectKey_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = { WebhookKey = "INCORRECT" } |
||||||
|
}); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, null); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN: Key is incorrect"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_EmptyIPNBody_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = { WebhookKey = _defaultWebhookKey } |
||||||
|
}); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, null); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN: Request body is null or empty"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_IPNHasNoEntityId_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = { WebhookKey = _defaultWebhookKey } |
||||||
|
}); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.TransactionMissingEntityIds); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN (2PK15573S8089712Y): 'custom' did not contain a User ID or Organization ID"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Unverified_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = { WebhookKey = _defaultWebhookKey } |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(false); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN (2PK15573S8089712Y): Verification failed"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_OtherTransactionType_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = { WebhookKey = _defaultWebhookKey } |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.UnsupportedTransactionType); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Transaction type (other) not supported for payments"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_MismatchedReceiverID_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "INCORRECT" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Receiver ID (NHDYKLQ3L4LWL) does not match Bitwarden business ID (INCORRECT)"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_RefundMissingParent_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.RefundMissingParentTransaction); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Parent transaction ID is required for refund"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_eCheckPayment_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.ECheckPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Transaction was an eCheck payment"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_NonUSD_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.NonUSDPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Transaction was not in USD (CAD)"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Completed_ExistingTransaction_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").Returns(new Transaction()); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Already processed this completed transaction"); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Completed_CreatesTransaction_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").ReturnsNull(); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
await _transactionRepository.Received().CreateAsync(Arg.Any<Transaction>()); |
||||||
|
|
||||||
|
await _paymentService.DidNotReceiveWithAnyArgs().CreditAccountAsync(Arg.Any<ISubscriber>(), Arg.Any<decimal>()); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Completed_CreatesTransaction_CreditsOrganizationAccount_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPaymentForOrganizationCredit); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").ReturnsNull(); |
||||||
|
|
||||||
|
const string billingEmail = "billing@organization.com"; |
||||||
|
|
||||||
|
var organization = new Organization { BillingEmail = billingEmail }; |
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization); |
||||||
|
|
||||||
|
_paymentService.CreditAccountAsync(organization, 48M).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction => |
||||||
|
transaction.GatewayId == "2PK15573S8089712Y" && |
||||||
|
transaction.OrganizationId == organizationId && |
||||||
|
transaction.Amount == 48M)); |
||||||
|
|
||||||
|
await _paymentService.Received(1).CreditAccountAsync(organization, 48M); |
||||||
|
|
||||||
|
await _organizationRepository.Received(1).ReplaceAsync(organization); |
||||||
|
|
||||||
|
await _mailService.Received(1).SendAddedCreditAsync(billingEmail, 48M); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Completed_CreatesTransaction_CreditsUserAccount_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var userId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPaymentForUserCredit); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(userId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").ReturnsNull(); |
||||||
|
|
||||||
|
const string billingEmail = "billing@user.com"; |
||||||
|
|
||||||
|
var user = new User { Email = billingEmail }; |
||||||
|
|
||||||
|
_userRepository.GetByIdAsync(userId).Returns(user); |
||||||
|
|
||||||
|
_paymentService.CreditAccountAsync(user, 48M).Returns(true); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction => |
||||||
|
transaction.GatewayId == "2PK15573S8089712Y" && |
||||||
|
transaction.UserId == userId && |
||||||
|
transaction.Amount == 48M)); |
||||||
|
|
||||||
|
await _paymentService.Received(1).CreditAccountAsync(user, 48M); |
||||||
|
|
||||||
|
await _userRepository.Received(1).ReplaceAsync(user); |
||||||
|
|
||||||
|
await _mailService.Received(1).SendAddedCreditAsync(billingEmail, 48M); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Refunded_ExistingTransaction_Unprocessed_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").Returns(new Transaction()); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Already processed this refunded transaction"); |
||||||
|
|
||||||
|
await _transactionRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<Transaction>()); |
||||||
|
|
||||||
|
await _transactionRepository.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<Transaction>()); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Refunded_MissingParentTransaction_BadRequest() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").ReturnsNull(); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"PARENT").ReturnsNull(); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 400); |
||||||
|
|
||||||
|
LoggedError(logger, "PayPal IPN (2PK15573S8089712Y): Could not find parent transaction"); |
||||||
|
|
||||||
|
await _transactionRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<Transaction>()); |
||||||
|
|
||||||
|
await _transactionRepository.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<Transaction>()); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task PostIpn_Refunded_ReplacesParent_CreatesTransaction_Ok() |
||||||
|
{ |
||||||
|
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>(); |
||||||
|
|
||||||
|
_billingSettings.Value.Returns(new BillingSettings |
||||||
|
{ |
||||||
|
PayPal = |
||||||
|
{ |
||||||
|
WebhookKey = _defaultWebhookKey, |
||||||
|
BusinessId = "NHDYKLQ3L4LWL" |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e"); |
||||||
|
|
||||||
|
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund); |
||||||
|
|
||||||
|
_payPalIPNClient.VerifyIPN(organizationId, ipnBody).Returns(true); |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"2PK15573S8089712Y").ReturnsNull(); |
||||||
|
|
||||||
|
var parentTransaction = new Transaction |
||||||
|
{ |
||||||
|
GatewayId = "PARENT", |
||||||
|
Amount = 48M, |
||||||
|
RefundedAmount = 0, |
||||||
|
Refunded = false |
||||||
|
}; |
||||||
|
|
||||||
|
_transactionRepository.GetByGatewayIdAsync( |
||||||
|
GatewayType.PayPal, |
||||||
|
"PARENT").Returns(parentTransaction); |
||||||
|
|
||||||
|
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); |
||||||
|
|
||||||
|
var result = await controller.PostIpn(); |
||||||
|
|
||||||
|
HasStatusCode(result, 200); |
||||||
|
|
||||||
|
await _transactionRepository.Received(1).ReplaceAsync(Arg.Is<Transaction>(transaction => |
||||||
|
transaction.GatewayId == "PARENT" && |
||||||
|
transaction.RefundedAmount == 48M && |
||||||
|
transaction.Refunded == true)); |
||||||
|
|
||||||
|
await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction => |
||||||
|
transaction.GatewayId == "2PK15573S8089712Y" && |
||||||
|
transaction.Amount == 48M && |
||||||
|
transaction.OrganizationId == organizationId && |
||||||
|
transaction.Type == TransactionType.Refund)); |
||||||
|
} |
||||||
|
|
||||||
|
private PayPalController ConfigureControllerContextWith( |
||||||
|
ILogger<PayPalController> logger, |
||||||
|
string webhookKey, |
||||||
|
string ipnBody) |
||||||
|
{ |
||||||
|
var controller = new PayPalController( |
||||||
|
_billingSettings, |
||||||
|
logger, |
||||||
|
_mailService, |
||||||
|
_organizationRepository, |
||||||
|
_paymentService, |
||||||
|
_payPalIPNClient, |
||||||
|
_transactionRepository, |
||||||
|
_userRepository); |
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext(); |
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(webhookKey)) |
||||||
|
{ |
||||||
|
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> |
||||||
|
{ |
||||||
|
{ "key", new StringValues(webhookKey) } |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(ipnBody)) |
||||||
|
{ |
||||||
|
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(ipnBody)); |
||||||
|
|
||||||
|
httpContext.Request.Body = memoryStream; |
||||||
|
httpContext.Request.ContentLength = memoryStream.Length; |
||||||
|
} |
||||||
|
|
||||||
|
controller.ControllerContext = new ControllerContext |
||||||
|
{ |
||||||
|
HttpContext = httpContext |
||||||
|
}; |
||||||
|
|
||||||
|
return controller; |
||||||
|
} |
||||||
|
|
||||||
|
private static void HasStatusCode(IActionResult result, int statusCode) |
||||||
|
{ |
||||||
|
var statusCodeActionResult = (IStatusCodeActionResult)result; |
||||||
|
|
||||||
|
statusCodeActionResult.StatusCode.Should().Be(statusCode); |
||||||
|
} |
||||||
|
|
||||||
|
private static void Logged(ICacheLogger logger, LogLevel logLevel, string message) |
||||||
|
{ |
||||||
|
logger.Last.Should().NotBeNull(); |
||||||
|
logger.Last!.LogLevel.Should().Be(logLevel); |
||||||
|
logger.Last!.Message.Should().Be(message); |
||||||
|
} |
||||||
|
|
||||||
|
private static void LoggedError(ICacheLogger logger, string message) |
||||||
|
=> Logged(logger, LogLevel.Error, message); |
||||||
|
|
||||||
|
private static void LoggedWarning(ICacheLogger logger, string message) |
||||||
|
=> Logged(logger, LogLevel.Warning, message); |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=echeck& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=CAD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=CAD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Refunded& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS%2Caccount_credit%3A1& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=user_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS%2Caccount_credit%3A1& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Refunded& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134& |
||||||
|
parent_txn_id=PARENT |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=region%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=merch_pmt& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
mc_gross=48.00& |
||||||
|
mp_custom=& |
||||||
|
mp_currency=USD& |
||||||
|
protection_eligibility=Eligible& |
||||||
|
payer_id=SVELHYY6G7TJ4& |
||||||
|
payment_date=11%3A07%3A43+Dec+27%2C+2023+PST& |
||||||
|
mp_id=B-4DP02332FD689211K& |
||||||
|
payment_status=Completed& |
||||||
|
charset=UTF-8& |
||||||
|
first_name=John& |
||||||
|
mp_status=0& |
||||||
|
mc_fee=2.17& |
||||||
|
notify_version=3.9& |
||||||
|
custom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS& |
||||||
|
payer_status=verified& |
||||||
|
business=sb-edwkp27927299%40business.example.com& |
||||||
|
quantity=1& |
||||||
|
verify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G& |
||||||
|
payer_email=sb-xuhf727950096%40personal.example.com& |
||||||
|
txn_id=2PK15573S8089712Y& |
||||||
|
payment_type=instant& |
||||||
|
last_name=Doe& |
||||||
|
mp_desc=& |
||||||
|
receiver_email=sb-edwkp27927299%40business.example.com& |
||||||
|
payment_fee=2.17& |
||||||
|
mp_cycle_start=30& |
||||||
|
shipping_discount=0.00& |
||||||
|
insurance_amount=0.00& |
||||||
|
receiver_id=NHDYKLQ3L4LWL& |
||||||
|
txn_type=other& |
||||||
|
item_name=& |
||||||
|
discount=0.00& |
||||||
|
mc_currency=USD& |
||||||
|
item_number=& |
||||||
|
residence_country=US& |
||||||
|
test_ipn=1& |
||||||
|
shipping_method=Default& |
||||||
|
transaction_subject=& |
||||||
|
payment_gross=48.00& |
||||||
|
ipn_track_id=769757969c134 |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
using System.Net; |
||||||
|
using Bit.Billing.Services; |
||||||
|
using Bit.Billing.Services.Implementations; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using Microsoft.Extensions.Options; |
||||||
|
using NSubstitute; |
||||||
|
using RichardSzalay.MockHttp; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Billing.Test.Services; |
||||||
|
|
||||||
|
public class PayPalIPNClientTests |
||||||
|
{ |
||||||
|
private readonly Uri _endpoint = new("https://www.sandbox.paypal.com/cgi-bin/webscr"); |
||||||
|
private readonly MockHttpMessageHandler _mockHttpMessageHandler = new(); |
||||||
|
|
||||||
|
private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>(); |
||||||
|
private readonly ILogger<PayPalIPNClient> _logger = Substitute.For<ILogger<PayPalIPNClient>>(); |
||||||
|
|
||||||
|
private readonly IPayPalIPNClient _payPalIPNClient; |
||||||
|
|
||||||
|
public PayPalIPNClientTests() |
||||||
|
{ |
||||||
|
var httpClient = new HttpClient(_mockHttpMessageHandler) |
||||||
|
{ |
||||||
|
BaseAddress = _endpoint |
||||||
|
}; |
||||||
|
|
||||||
|
_payPalIPNClient = new PayPalIPNClient( |
||||||
|
_billingSettings, |
||||||
|
httpClient, |
||||||
|
_logger); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task VerifyIPN_FormDataNull_ThrowsArgumentNullException() |
||||||
|
=> await Assert.ThrowsAsync<ArgumentNullException>(() => _payPalIPNClient.VerifyIPN(Guid.NewGuid(), null)); |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task VerifyIPN_Unauthorized_ReturnsFalse() |
||||||
|
{ |
||||||
|
const string formData = "form=data"; |
||||||
|
|
||||||
|
var request = _mockHttpMessageHandler |
||||||
|
.Expect(HttpMethod.Post, _endpoint.ToString()) |
||||||
|
.WithFormData(new Dictionary<string, string> { { "cmd", "_notify-validate" }, { "form", "data" } }) |
||||||
|
.Respond(HttpStatusCode.Unauthorized); |
||||||
|
|
||||||
|
var verified = await _payPalIPNClient.VerifyIPN(Guid.NewGuid(), formData); |
||||||
|
|
||||||
|
Assert.False(verified); |
||||||
|
Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request)); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task VerifyIPN_OK_Invalid_ReturnsFalse() |
||||||
|
{ |
||||||
|
const string formData = "form=data"; |
||||||
|
|
||||||
|
var request = _mockHttpMessageHandler |
||||||
|
.Expect(HttpMethod.Post, _endpoint.ToString()) |
||||||
|
.WithFormData(new Dictionary<string, string> { { "cmd", "_notify-validate" }, { "form", "data" } }) |
||||||
|
.Respond("application/text", "INVALID"); |
||||||
|
|
||||||
|
var verified = await _payPalIPNClient.VerifyIPN(Guid.NewGuid(), formData); |
||||||
|
|
||||||
|
Assert.False(verified); |
||||||
|
Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request)); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task VerifyIPN_OK_Verified_ReturnsTrue() |
||||||
|
{ |
||||||
|
const string formData = "form=data"; |
||||||
|
|
||||||
|
var request = _mockHttpMessageHandler |
||||||
|
.Expect(HttpMethod.Post, _endpoint.ToString()) |
||||||
|
.WithFormData(new Dictionary<string, string> { { "cmd", "_notify-validate" }, { "form", "data" } }) |
||||||
|
.Respond("application/text", "VERIFIED"); |
||||||
|
|
||||||
|
var verified = await _payPalIPNClient.VerifyIPN(Guid.NewGuid(), formData); |
||||||
|
|
||||||
|
Assert.True(verified); |
||||||
|
Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
namespace Bit.Billing.Test.Utilities; |
||||||
|
|
||||||
|
public enum IPNBody |
||||||
|
{ |
||||||
|
SuccessfulPayment, |
||||||
|
ECheckPayment, |
||||||
|
TransactionMissingEntityIds, |
||||||
|
NonUSDPayment, |
||||||
|
SuccessfulPaymentForOrganizationCredit, |
||||||
|
UnsupportedTransactionType, |
||||||
|
SuccessfulRefund, |
||||||
|
RefundMissingParentTransaction, |
||||||
|
SuccessfulPaymentForUserCredit |
||||||
|
} |
||||||
|
|
||||||
|
public static class PayPalTestIPN |
||||||
|
{ |
||||||
|
public static async Task<string> GetAsync(IPNBody ipnBody) |
||||||
|
{ |
||||||
|
var fileName = ipnBody switch |
||||||
|
{ |
||||||
|
IPNBody.ECheckPayment => "echeck-payment.txt", |
||||||
|
IPNBody.NonUSDPayment => "non-usd-payment.txt", |
||||||
|
IPNBody.RefundMissingParentTransaction => "refund-missing-parent-transaction.txt", |
||||||
|
IPNBody.SuccessfulPayment => "successful-payment.txt", |
||||||
|
IPNBody.SuccessfulPaymentForOrganizationCredit => "successful-payment-org-credit.txt", |
||||||
|
IPNBody.SuccessfulRefund => "successful-refund.txt", |
||||||
|
IPNBody.SuccessfulPaymentForUserCredit => "successful-payment-user-credit.txt", |
||||||
|
IPNBody.TransactionMissingEntityIds => "transaction-missing-entity-ids.txt", |
||||||
|
IPNBody.UnsupportedTransactionType => "unsupported-transaction-type.txt" |
||||||
|
}; |
||||||
|
|
||||||
|
var content = await EmbeddedResourceReader.ReadAsync("IPN", fileName); |
||||||
|
|
||||||
|
return content.Replace("\n", string.Empty); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue