diff --git a/src/Core/Models/Business/BillingInfo.cs b/src/Core/Models/Business/BillingInfo.cs index 5266558ee..dab20541f 100644 --- a/src/Core/Models/Business/BillingInfo.cs +++ b/src/Core/Models/Business/BillingInfo.cs @@ -139,7 +139,6 @@ namespace Bit.Core.Models.Business { public BillingInvoice(Invoice inv) { - Amount = inv.AmountDue / 100M; Date = inv.Created; Url = inv.HostedInvoiceUrl; PdfUrl = inv.InvoicePdf; diff --git a/src/Core/Services/Implementations/AppleIapService.cs b/src/Core/Services/Implementations/AppleIapService.cs index 69fe601cf..2b20b040a 100644 --- a/src/Core/Services/Implementations/AppleIapService.cs +++ b/src/Core/Services/Implementations/AppleIapService.cs @@ -85,15 +85,16 @@ namespace Bit.Core.Services receipt.ContainsKey("UserId") ? new Guid(receipt["UserId"]) : (Guid?)null); } - private async Task GetReceiptStatusAsync(string receiptData, bool prod = true, + // Internal for testing + internal async Task GetReceiptStatusAsync(string receiptData, bool prod = true, int attempt = 0, AppleReceiptStatus lastReceiptStatus = null) { try { if (attempt > 4) { - throw new Exception("Failed verifying Apple IAP after too many attempts. Last attempt status: " + - lastReceiptStatus?.Status ?? "null"); + throw new Exception( + $"Failed verifying Apple IAP after too many attempts. Last attempt status: {lastReceiptStatus?.Status.ToString() ?? "null"}"); } var url = string.Format("https://{0}.itunes.apple.com/verifyReceipt", prod ? "buy" : "sandbox"); diff --git a/test/Api.Test/Models/Request/Accounts/PremiumRequestModelTests.cs b/test/Api.Test/Models/Request/Accounts/PremiumRequestModelTests.cs new file mode 100644 index 000000000..1f3c7cbb8 --- /dev/null +++ b/test/Api.Test/Models/Request/Accounts/PremiumRequestModelTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Bit.Api.Models.Request.Accounts; +using Bit.Core.Settings; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Bit.Api.Test.Models.Request.Accounts +{ + public class PremiumRequestModelTests + { + public static IEnumerable GetValidateData() + { + // 1. selfHosted + // 2. formFile + // 3. country + // 4. expected + + yield return new object[] { true, null, null, false }; + yield return new object[] { true, null, "US", false }; + yield return new object[] { true, new NotImplementedFormFile(), null, false }; + yield return new object[] { true, new NotImplementedFormFile(), "US", false }; + + yield return new object[] { false, null, null, false }; + yield return new object[] { false, null, "US", true }; // Only true, cloud with null license AND a Country + yield return new object[] { false, new NotImplementedFormFile(), null, false }; + yield return new object[] { false, new NotImplementedFormFile(), "US", false }; + } + + [Theory] + [MemberData(nameof(GetValidateData))] + public void Validate_Success(bool selfHosted, IFormFile formFile, string country, bool expected) + { + var gs = new GlobalSettings + { + SelfHosted = selfHosted + }; + + var sut = new PremiumRequestModel + { + License = formFile, + Country = country, + }; + + Assert.Equal(expected, sut.Validate(gs)); + } + } + + public class NotImplementedFormFile : IFormFile + { + public string ContentType => throw new NotImplementedException(); + + public string ContentDisposition => throw new NotImplementedException(); + + public IHeaderDictionary Headers => throw new NotImplementedException(); + + public long Length => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public string FileName => throw new NotImplementedException(); + + public void CopyTo(Stream target) => throw new NotImplementedException(); + public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Stream OpenReadStream() => throw new NotImplementedException(); + } +} diff --git a/test/Core.Test/Models/Business/BillingInfo.cs b/test/Core.Test/Models/Business/BillingInfo.cs new file mode 100644 index 000000000..0023b4669 --- /dev/null +++ b/test/Core.Test/Models/Business/BillingInfo.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Business; +using Xunit; + +namespace Bit.Core.Test.Models.Business +{ + public class BillingInfoTests + { + [Fact] + public void BillingInvoice_Amount_ShouldComeFrom_InvoiceTotal() + { + var invoice = new Stripe.Invoice + { + AmountDue = 1000, + Total = 2000, + }; + + var billingInvoice = new BillingInfo.BillingInvoice(invoice); + + // Should have been set from Total + Assert.Equal(20M, billingInvoice.Amount); + } + } +} diff --git a/test/Core.Test/Services/AppleIapServiceTests.cs b/test/Core.Test/Services/AppleIapServiceTests.cs new file mode 100644 index 000000000..74f7e3fe0 --- /dev/null +++ b/test/Core.Test/Services/AppleIapServiceTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Core; +using Xunit; + +namespace Bit.Core.Test.Services +{ + [SutProviderCustomize] + public class AppleIapServiceTests + { + [Theory, BitAutoData] + public async Task GetReceiptStatusAsync_MoreThanFourAttempts_Throws(SutProvider sutProvider) + { + var result = await sutProvider.Sut.GetReceiptStatusAsync("test", false, 5, null); + Assert.Null(result); + + var errorLog = sutProvider.GetDependency>() + .ReceivedCalls() + .SingleOrDefault(LogOneWarning); + + Assert.True(errorLog != null, "Must contain one error log of warning level containing 'null'"); + + static bool LogOneWarning(ICall call) + { + if (call.GetMethodInfo().Name != "Log") + { + return false; + } + + var args = call.GetArguments(); + var logLevel = (LogLevel)args[0]; + var exception = (Exception)args[3]; + + return logLevel == LogLevel.Warning && exception.Message.Contains("null"); + } + } + } +}