diff --git a/.travis.yml b/.travis.yml index 9a531df0..c40737b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ sudo: required dist: xenial +language: csharp +mono: none os: - linux @@ -22,10 +24,11 @@ addons: - libicu-dev - libssl-dev - libunwind8 + chrome: stable install: - - npm install -g bower - - npm install -g npm + - npm install --global bower + - npm install --global npm script: - ./build.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index d6519312..a1fa166a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/src/ApplePayJS/bin/Debug/netcoreapp2.2/JustEat.ApplePayJS.dll", + "program": "${workspaceRoot}/src/ApplePayJS/bin/Debug/netcoreapp3.0/JustEat.ApplePayJS.dll", "args": [], "cwd": "${workspaceRoot}/src/ApplePayJS", "stopAtEntry": false, diff --git a/ApplePayJS.sln b/ApplePayJS.sln index df0e8291..69363c86 100644 --- a/ApplePayJS.sln +++ b/ApplePayJS.sln @@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Build.ps1 = Build.ps1 build.sh = build.sh CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md + Directory.Build.props = Directory.Build.props global.json = global.json LICENSE = LICENSE NuGet.config = NuGet.config @@ -29,6 +30,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{A4EF .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{F99C01C5-8B5D-4BDD-B40B-35DE14D08D57}" + ProjectSection(SolutionItems) = preProject + .vscode\launch.json = .vscode\launch.json + .vscode\tasks.json = .vscode\tasks.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3F1B1211-EEF9-482B-93CD-6FF250907EB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplePayJS.Tests", "tests\ApplePayJS.Tests\ApplePayJS.Tests.csproj", "{20EE5C1E-2059-4291-93F6-AA0F76C08EBE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +50,10 @@ Global {4CFD067B-FD1A-4303-9322-3138E9104B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CFD067B-FD1A-4303-9322-3138E9104B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CFD067B-FD1A-4303-9322-3138E9104B1F}.Release|Any CPU.Build.0 = Release|Any CPU + {20EE5C1E-2059-4291-93F6-AA0F76C08EBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20EE5C1E-2059-4291-93F6-AA0F76C08EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20EE5C1E-2059-4291-93F6-AA0F76C08EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20EE5C1E-2059-4291-93F6-AA0F76C08EBE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +61,8 @@ Global GlobalSection(NestedProjects) = preSolution {4CFD067B-FD1A-4303-9322-3138E9104B1F} = {ABF2B260-1BAE-45CD-9348-C163CA02ECA3} {A4EFB4F4-BC05-4A67-89AC-4D7D21B71D4E} = {C697BCC9-C4F9-4AD8-8336-E90A239865DE} + {F99C01C5-8B5D-4BDD-B40B-35DE14D08D57} = {C697BCC9-C4F9-4AD8-8336-E90A239865DE} + {20EE5C1E-2059-4291-93F6-AA0F76C08EBE} = {3F1B1211-EEF9-482B-93CD-6FF250907EB9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8797D09A-407D-424A-A8F0-22FB26F0650C} diff --git a/Build.ps1 b/Build.ps1 index 3b981dbf..16539a86 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -58,6 +58,9 @@ else { Write-Host "Publishing solution..." -ForegroundColor Green & $dotnet publish $solutionFile --output $OutputPath --configuration $Configuration +Write-Host "Running tests..." -ForegroundColor Green +& $dotnet test $solutionFile --output $OutputPath --configuration $Configuration + if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" } diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..50bc9752 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,20 @@ + + + Martin Costello + Just Eat + Just Eat (c) 2016-$([System.DateTime]::Now.ToString(yyyy)) + latest + en-US + enable + Apache-2.0 + https://github.com/justeat/ApplePayJSSample + $(PackageProjectUrl)/releases + false + applepay + git + $(PackageProjectUrl).git + latest + 3.0.0 + + + diff --git a/README.md b/README.md index 64f6746b..db113828 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ |:-:|:-:|:-:| | **Build Status** | [![Build status](https://img.shields.io/travis/justeat/ApplePayJSSample/master.svg)](https://travis-ci.org/justeat/ApplePayJSSample) | [![Build status](https://img.shields.io/appveyor/ci/justeattech/applepayjssample/master.svg)](https://ci.appveyor.com/project/justeattech/applepayjssample) | -This repository contains a sample implementation of [Apple Pay JS](https://developer.apple.com/reference/applepayjs/) using ASP.NET Core 2.2 written in C# and JavaScript. +This repository contains a sample implementation of [Apple Pay JS](https://developer.apple.com/reference/applepayjs/) using ASP.NET Core 3.0 written in C# and JavaScript. ## Overview @@ -24,7 +24,7 @@ The key components to look at for the implementation are: To setup the repository to run the sample, perform the steps below: - 1. Install the [.NET Core 2.2.402 SDK](https://www.microsoft.com/net/download/core), Visual Studio 2019 or Visual Studio Code. + 1. Install the [.NET Core 3.0.100 SDK](https://www.microsoft.com/net/download/core), Visual Studio 2019 or Visual Studio Code. 1. Fork this repository. 1. Clone the repository from your fork to your local machine: ```git clone https://github.com/{username}/ApplePayJSSample.git``` 1. Restore the Bower, npm and NuGet packages. diff --git a/appveyor.yml b/appveyor.yml index fa7a4535..d78288a6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -os: Visual Studio 2017 +os: Visual Studio 2019 version: 3.0.{build} environment: @@ -10,8 +10,9 @@ branches: - master install: - - ps: npm install -g bower --loglevel=error - - ps: npm install -g npm + - ps: npm install --global bower --loglevel=error + - ps: npm install --global npm + - ps: choco upgrade googlechrome --confirm --ignore-checksums --no-progress build_script: - ps: .\Build.ps1 diff --git a/build.sh b/build.sh index a73cb7de..32508e27 100755 --- a/build.sh +++ b/build.sh @@ -31,7 +31,9 @@ export PATH="$DOTNET_INSTALL_DIR:$PATH" dotnet_version=$(dotnet --version) if [ "$dotnet_version" != "$CLI_VERSION" ]; then + curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version "2.2.402" --install-dir "$DOTNET_INSTALL_DIR" curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" fi dotnet publish ./ApplePayJS.sln --output $artifacts/publish --configuration $configuration || exit 1 +dotnet test ./ApplePayJS.sln --output $artifacts --configuration $configuration || exit 1 diff --git a/global.json b/global.json index 89b3b0f4..79422f0c 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.2.402" + "version": "3.0.100" } } diff --git a/src/ApplePayJS/ApplePayJS.csproj b/src/ApplePayJS/ApplePayJS.csproj index c4e74d3f..f3c79a45 100644 --- a/src/ApplePayJS/ApplePayJS.csproj +++ b/src/ApplePayJS/ApplePayJS.csproj @@ -1,14 +1,8 @@  - inprocess - Martin Costello + InProcess JustEat.ApplePayJS - Just Eat - Just Eat (c) 2016-$([System.DateTime]::Now.ToString(yyyy)) - latest - en-US Exe - https://avatars3.githubusercontent.com/u/1516790?v=3&s=100 JustEat.ApplePayJS Apache-2.0 https://github.com/justeat/ApplePayJSSample @@ -16,21 +10,14 @@ false applepay true - git - $(PackageProjectUrl).git JustEat.ApplePayJS - netcoreapp2.2 + netcoreapp3.0 latest JustEat.ApplePayJS - 3.0.0 - - - - true @@ -50,6 +37,6 @@ - + diff --git a/src/ApplePayJS/Clients/ApplePayClient.cs b/src/ApplePayJS/Clients/ApplePayClient.cs index 29587ba9..aaa3528b 100644 --- a/src/ApplePayJS/Clients/ApplePayClient.cs +++ b/src/ApplePayJS/Clients/ApplePayClient.cs @@ -1,8 +1,13 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + using System; using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; namespace JustEat.ApplePayJS.Clients { @@ -15,19 +20,24 @@ public ApplePayClient(HttpClient httpClient) _httpClient = httpClient; } - public async Task GetMerchantSessionAsync( + public async Task GetMerchantSessionAsync( Uri requestUri, MerchantSessionRequest request, CancellationToken cancellationToken = default) { // POST the data to create a valid Apple Pay merchant session. - using (var response = await _httpClient.PostAsJsonAsync(requestUri, request, cancellationToken)) - { - response.EnsureSuccessStatusCode(); + string json = JsonSerializer.Serialize(request); + + using var content = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); + + using var response = await _httpClient.PostAsync(requestUri, content, cancellationToken); + + response.EnsureSuccessStatusCode(); + + // Read the opaque merchant session JSON from the response body. + using var stream = await response.Content.ReadAsStreamAsync(); - // Read the opaque merchant session JSON from the response body. - return await response.Content.ReadAsAsync(cancellationToken); - } + return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); } } } diff --git a/src/ApplePayJS/Clients/MerchantCertificate.cs b/src/ApplePayJS/Clients/MerchantCertificate.cs index 351068d3..4e4ac73a 100644 --- a/src/ApplePayJS/Clients/MerchantCertificate.cs +++ b/src/ApplePayJS/Clients/MerchantCertificate.cs @@ -1,3 +1,6 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + using System; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -32,7 +35,7 @@ public string GetMerchantIdentifier() { try { - var merchantCertificate = GetCertificate(); + using var merchantCertificate = GetCertificate(); return GetMerchantIdentifier(merchantCertificate); } catch (InvalidOperationException) @@ -76,23 +79,21 @@ private X509Certificate2 LoadCertificateFromStore() // your application, but it is also required to be able to use an X.509 // certificate with a private key if the user profile is not available, // such as when using IIS hosting in an environment such as Microsoft Azure. - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - { - store.Open(OpenFlags.ReadOnly); - - var certificates = store.Certificates.Find( - X509FindType.FindByThumbprint, - _options.MerchantCertificateThumbprint?.Trim(), - validOnly: false); + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); - if (certificates.Count < 1) - { - throw new InvalidOperationException( - $"Could not find Apple Pay merchant certificate with thumbprint '{_options.MerchantCertificateThumbprint}' from store '{store.Name}' in location '{store.Location}'."); - } + var certificates = store.Certificates.Find( + X509FindType.FindByThumbprint, + _options.MerchantCertificateThumbprint?.Trim(), + validOnly: false); - return certificates[0]; + if (certificates.Count < 1) + { + throw new InvalidOperationException( + $"Could not find Apple Pay merchant certificate with thumbprint '{_options.MerchantCertificateThumbprint}' from store '{store.Name}' in location '{store.Location}'."); } + + return certificates[0]; } } } diff --git a/src/ApplePayJS/Clients/MerchantSessionRequest.cs b/src/ApplePayJS/Clients/MerchantSessionRequest.cs index 07eb9289..73dab2ae 100644 --- a/src/ApplePayJS/Clients/MerchantSessionRequest.cs +++ b/src/ApplePayJS/Clients/MerchantSessionRequest.cs @@ -1,19 +1,22 @@ -using Newtonsoft.Json; +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; namespace JustEat.ApplePayJS.Clients { public class MerchantSessionRequest { - [JsonProperty("merchantIdentifier")] - public string MerchantIdentifier { get; set; } + [JsonPropertyName("merchantIdentifier")] + public string? MerchantIdentifier { get; set; } - [JsonProperty("displayName")] - public string DisplayName { get; set; } + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - [JsonProperty("initiative")] - public string Initiative { get; set; } + [JsonPropertyName("initiative")] + public string? Initiative { get; set; } - [JsonProperty("initiativeContext")] - public string InitiativeContext { get; set; } + [JsonPropertyName("initiativeContext")] + public string? InitiativeContext { get; set; } } } diff --git a/src/ApplePayJS/Controllers/HomeController.cs b/src/ApplePayJS/Controllers/HomeController.cs index 06a6ab09..1c94e1c6 100644 --- a/src/ApplePayJS/Controllers/HomeController.cs +++ b/src/ApplePayJS/Controllers/HomeController.cs @@ -4,6 +4,8 @@ namespace JustEat.ApplePayJS.Controllers { using System; + using System.Net.Mime; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using JustEat.ApplePayJS.Clients; @@ -11,7 +13,6 @@ namespace JustEat.ApplePayJS.Controllers using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Models; - using Newtonsoft.Json.Linq; public class HomeController : Controller { @@ -42,7 +43,7 @@ public IActionResult Index() } [HttpPost] - [Produces("application/json")] + [Produces(MediaTypeNames.Application.Json)] [Route("applepay/validate", Name = "MerchantValidation")] public async Task Validate([FromBody] ValidateMerchantSessionModel model, CancellationToken cancellationToken = default) { @@ -51,7 +52,7 @@ public async Task Validate([FromBody] ValidateMerchantSessionMode // these servers are available here: https://developer.apple.com/documentation/applepayjs/setting_up_server_requirements if (!ModelState.IsValid || string.IsNullOrWhiteSpace(model?.ValidationUrl) || - !Uri.TryCreate(model.ValidationUrl, UriKind.Absolute, out Uri requestUri)) + !Uri.TryCreate(model.ValidationUrl, UriKind.Absolute, out Uri? requestUri)) { return BadRequest(); } @@ -65,10 +66,10 @@ public async Task Validate([FromBody] ValidateMerchantSessionMode MerchantIdentifier = _certificate.GetMerchantIdentifier(), }; - JObject merchantSession = await _client.GetMerchantSessionAsync(requestUri, request, cancellationToken); + JsonDocument merchantSession = await _client.GetMerchantSessionAsync(requestUri, request, cancellationToken); // Return the merchant session as-is to the JavaScript as JSON. - return Json(merchantSession); + return Json(merchantSession.RootElement); } public IActionResult Error() => View(); diff --git a/src/ApplePayJS/Models/ApplePayOptions.cs b/src/ApplePayJS/Models/ApplePayOptions.cs index 9a549a17..3749dd7f 100644 --- a/src/ApplePayJS/Models/ApplePayOptions.cs +++ b/src/ApplePayJS/Models/ApplePayOptions.cs @@ -5,14 +5,14 @@ namespace JustEat.ApplePayJS.Models { public class ApplePayOptions { - public string StoreName { get; set; } + public string? StoreName { get; set; } public bool UseCertificateStore { get; set; } - public string MerchantCertificateFileName { get; set; } + public string? MerchantCertificateFileName { get; set; } - public string MerchantCertificatePassword { get; set; } + public string? MerchantCertificatePassword { get; set; } - public string MerchantCertificateThumbprint { get; set; } + public string? MerchantCertificateThumbprint { get; set; } } } diff --git a/src/ApplePayJS/Models/HomeModel.cs b/src/ApplePayJS/Models/HomeModel.cs index af07e74e..c84a82dc 100644 --- a/src/ApplePayJS/Models/HomeModel.cs +++ b/src/ApplePayJS/Models/HomeModel.cs @@ -1,9 +1,12 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + namespace JustEat.ApplePayJS.Models { public class HomeModel { - public string MerchantId { get; set; } + public string? MerchantId { get; set; } - public string StoreName { get; set; } + public string? StoreName { get; set; } } } diff --git a/src/ApplePayJS/Models/ValidateMerchantSessionModel.cs b/src/ApplePayJS/Models/ValidateMerchantSessionModel.cs index 21edf06e..e6d2a9de 100644 --- a/src/ApplePayJS/Models/ValidateMerchantSessionModel.cs +++ b/src/ApplePayJS/Models/ValidateMerchantSessionModel.cs @@ -9,6 +9,6 @@ public class ValidateMerchantSessionModel { [DataType(DataType.Url)] [Required] - public string ValidationUrl { get; set; } + public string? ValidationUrl { get; set; } } } diff --git a/src/ApplePayJS/Program.cs b/src/ApplePayJS/Program.cs index 99b45ed2..ecd07550 100644 --- a/src/ApplePayJS/Program.cs +++ b/src/ApplePayJS/Program.cs @@ -3,18 +3,18 @@ namespace JustEat.ApplePayJS { - using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Hosting; public static class Program { public static void Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); + CreateHostBuilder(args).Build().Run(); } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults((builder) => builder.UseStartup()); } } diff --git a/src/ApplePayJS/Startup.cs b/src/ApplePayJS/Startup.cs index aacf62d3..f7c58ddf 100644 --- a/src/ApplePayJS/Startup.cs +++ b/src/ApplePayJS/Startup.cs @@ -11,26 +11,27 @@ namespace JustEat.ApplePayJS using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; using Models; public class Startup { - public Startup(IHostingEnvironment env, IConfiguration configuration) + public Startup(IHostEnvironment environment, IConfiguration configuration) { Configuration = configuration; - Environment = env; + Environment = environment; } public IConfiguration Configuration { get; } - public IHostingEnvironment Environment { get; } + public IHostEnvironment Environment { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions(); services.Configure(Configuration.GetSection("ApplePay")); - services.AddAntiforgery(options => + services.AddAntiforgery((options) => { options.Cookie.Name = "antiforgerytoken"; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; @@ -38,7 +39,7 @@ public void ConfigureServices(IServiceCollection services) }); services.AddMvc( - options => + (options) => { // Apple Pay JS requires pages to be served over HTTPS if (Environment.IsProduction()) @@ -47,7 +48,7 @@ public void ConfigureServices(IServiceCollection services) options.Filters.Add(new RequireHttpsAttribute()); } }) - .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); // Register class for managing the application's use of the Apple Pay merchant certificate services.AddSingleton(); @@ -73,9 +74,9 @@ public void ConfigureServices(IServiceCollection services) }); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app) { - if (env.IsDevelopment()) + if (Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } @@ -94,7 +95,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) ServeUnknownFileTypes = true, // Required to serve the files in the .well-known folder }); - app.UseMvcWithDefaultRoute(); + app.UseRouting(); + app.UseEndpoints((endpoints) => endpoints.MapDefaultControllerRoute()); } } } diff --git a/src/ApplePayJS/Views/Home/_Polyfill.cshtml b/src/ApplePayJS/Views/Home/_Polyfill.cshtml index b1de3032..c6520eb0 100644 --- a/src/ApplePayJS/Views/Home/_Polyfill.cshtml +++ b/src/ApplePayJS/Views/Home/_Polyfill.cshtml @@ -1,3 +1,8 @@ +@* + Copyright (c) Just Eat, 2016. All rights reserved. + Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +*@ + @model string @* Use this polyfill during development to test your Apple Pay JS implementation diff --git a/tests/ApplePayJS.Tests/ApplePayJS.Tests.csproj b/tests/ApplePayJS.Tests/ApplePayJS.Tests.csproj new file mode 100644 index 00000000..c619c322 --- /dev/null +++ b/tests/ApplePayJS.Tests/ApplePayJS.Tests.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + false + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ApplePayJS.Tests/IntegrationTests.cs b/tests/ApplePayJS.Tests/IntegrationTests.cs new file mode 100644 index 00000000..1eb33b4b --- /dev/null +++ b/tests/ApplePayJS.Tests/IntegrationTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using JustEat.HttpClientInterception; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Support.UI; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace ApplePayJS.Tests +{ + public class IntegrationTests : IAsyncLifetime + { + public IntegrationTests(ITestOutputHelper outputHelper) + { + Fixture = new TestFixture() + { + OutputHelper = outputHelper, + }; + } + + private TestFixture Fixture { get; } + + public async Task InitializeAsync() + { + await Fixture.StartServerAsync(); + } + + public Task DisposeAsync() + { + if (Fixture != null) + { + Fixture.Dispose(); + } + + return Task.CompletedTask; + } + + [Fact] + public void Can_Pay_With_Apple_Pay() + { + // Arrange + var builder = new HttpRequestInterceptionBuilder() + .Requests() + .ForPost() + .ForUrl("https://apple-pay-gateway-cert.apple.com/paymentservices/startSession") + .Responds() + .WithJsonContent(new { }) + .RegisterWith(Fixture.Interceptor); + + using var driver = CreateWebDriver(); + driver.Navigate().GoToUrl(Fixture.ServerAddress); + + // Act + driver.FindElement(By.Id("amount")).Clear(); + driver.FindElement(By.Id("amount")).SendKeys("1.23"); + driver.FindElement(By.Id("apple-pay-button")).Click(); + + // Assert + var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(5)); + wait.Until((p) => p.FindElement(By.ClassName("card-name")).Displayed); + + driver.FindElement(By.ClassName("card-name")).Text.ShouldBe("American Express"); + driver.FindElement(By.Id("billing-contact")).FindElement(By.ClassName("contact-name")).Text.ShouldBe("John Smith"); + driver.FindElement(By.Id("shipping-contact")).FindElement(By.ClassName("contact-name")).Text.ShouldBe("John Smith"); + } + + private static IWebDriver CreateWebDriver() + { + string chromeDriverDirectory = Path.GetDirectoryName(typeof(IntegrationTests).Assembly.Location) ?? "."; + + var options = new ChromeOptions() + { + AcceptInsecureCertificates = true, + }; + + if (!System.Diagnostics.Debugger.IsAttached) + { + options.AddArgument("--headless"); + } + + return new ChromeDriver(chromeDriverDirectory, options); + } + } +} diff --git a/tests/ApplePayJS.Tests/MerchantCertificateGenerator.cs b/tests/ApplePayJS.Tests/MerchantCertificateGenerator.cs new file mode 100644 index 00000000..4a47a145 --- /dev/null +++ b/tests/ApplePayJS.Tests/MerchantCertificateGenerator.cs @@ -0,0 +1,49 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Xunit; + +namespace ApplePayJS.Tests +{ + public static class MerchantCertificateGenerator + { + [Fact(Skip = "Enable this test to generate a new dummy Apple Pay merchant certificate to use for the tests.")] + public static async Task Generate_Fake_Apple_Pay_Merchant_Certificate() + { + // Arrange + string certificateName = "applepay.local"; + string certificatePassword = "Pa55w0rd!"; + string certificateFileName = "CHANGE_ME"; + + var builder = new SubjectAlternativeNameBuilder(); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddIpAddress(IPAddress.IPv6Loopback); + builder.AddDnsName("localhost"); + + var distinguishedName = new X500DistinguishedName($"CN={certificateName}"); + + using var key = RSA.Create(2048); + var request = new CertificateRequest(distinguishedName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyAgreement | X509KeyUsageFlags.KeyEncipherment, false)); + request.CertificateExtensions.Add(new X509Extension("1.2.840.113635.100.6.32", Guid.NewGuid().ToByteArray(), false)); + request.CertificateExtensions.Add(builder.Build()); + + var utcNow = DateTimeOffset.UtcNow; + + X509Certificate2 certificate = request.CreateSelfSigned(utcNow.AddDays(-1), utcNow.AddDays(3650)); + certificate.FriendlyName = certificateName; + + byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, certificatePassword); + + File.Delete(certificateFileName); + await File.WriteAllBytesAsync(certificateFileName, pfxBytes); + } + } +} diff --git a/tests/ApplePayJS.Tests/TestFixture.cs b/tests/ApplePayJS.Tests/TestFixture.cs new file mode 100644 index 00000000..19247fb2 --- /dev/null +++ b/tests/ApplePayJS.Tests/TestFixture.cs @@ -0,0 +1,164 @@ +// Copyright (c) Just Eat, 2016. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using JustEat.ApplePayJS; +using JustEat.HttpClientInterception; +using MartinCostello.Logging.XUnit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace ApplePayJS.Tests +{ + public class TestFixture : WebApplicationFactory, ITestOutputHelperAccessor + { + private IHost? _host; + private bool _disposed; + + public TestFixture() + : base() + { + ClientOptions.AllowAutoRedirect = false; + ClientOptions.BaseAddress = new Uri("https://localhost"); + Interceptor = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration(); + } + + public HttpClientInterceptorOptions Interceptor { get; } + + public ITestOutputHelper? OutputHelper { get; set; } + + public Uri ServerAddress => ClientOptions.BaseAddress; + + public async Task StartServerAsync() + { + if (_host == null) + { + await CreateHttpServer(); + } + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices( + (services) => services.AddSingleton( + (_) => new HttpRequestInterceptionFilter(Interceptor))); + + builder.ConfigureAppConfiguration(ConfigureTests) + .ConfigureLogging((loggingBuilder) => loggingBuilder.ClearProviders().AddXUnit(this).AddDebug()) + .UseContentRoot(GetContentRootPath()); + + builder.ConfigureKestrel( + (kestrelOptions) => kestrelOptions.ConfigureHttpsDefaults( + (connectionOptions) => connectionOptions.ServerCertificate = new X509Certificate2("localhost-dev.pfx", "Pa55w0rd!"))); + + builder.UseUrls(ServerAddress.ToString()); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!_disposed) + { + if (disposing) + { + _host?.Dispose(); + } + + _disposed = true; + } + } + + private static void ConfigureTests(IConfigurationBuilder builder) + { + string? directory = Path.GetDirectoryName(typeof(TestFixture).Assembly.Location); + string fullPath = Path.Combine(directory ?? ".", "testsettings.json"); + + builder.AddJsonFile(fullPath); + } + + private static Uri FindFreeServerAddress() + { + int port = GetFreePortNumber(); + + return new UriBuilder() + { + Scheme = "https", + Host = "localhost", + Port = port, + }.Uri; + } + + private static int GetFreePortNumber() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + + try + { + return ((IPEndPoint)listener.LocalEndpoint).Port; + } + finally + { + listener.Stop(); + } + } + + private async Task CreateHttpServer() + { + // Configure the server address for the server to listen on for HTTP requests + ClientOptions.BaseAddress = FindFreeServerAddress(); + + var builder = CreateHostBuilder().ConfigureWebHost(ConfigureWebHost); + + _host = builder.Build(); + + // Force creation of the Kestrel server and start it + var hostedService = _host.Services.GetService(); + await hostedService.StartAsync(default); + } + + private string GetContentRootPath() + { + var attribute = GetTestAssemblies() + .SelectMany((p) => p.GetCustomAttributes()) + .Where((p) => string.Equals(p.Key, "JustEat.ApplePayJS", StringComparison.OrdinalIgnoreCase)) + .OrderBy((p) => p.Priority) + .First(); + + return attribute.ContentRootPath; + } + + private sealed class HttpRequestInterceptionFilter : IHttpMessageHandlerBuilderFilter + { + internal HttpRequestInterceptionFilter(HttpClientInterceptorOptions options) + { + Options = options; + } + + private HttpClientInterceptorOptions Options { get; } + + public Action Configure(Action next) + { + return (builder) => + { + next(builder); + builder.AdditionalHandlers.Add(Options.CreateHttpMessageHandler()); + }; + } + } + } +} diff --git a/tests/ApplePayJS.Tests/applepay-dev.pfx b/tests/ApplePayJS.Tests/applepay-dev.pfx new file mode 100644 index 00000000..dad6b947 Binary files /dev/null and b/tests/ApplePayJS.Tests/applepay-dev.pfx differ diff --git a/tests/ApplePayJS.Tests/localhost-dev.pfx b/tests/ApplePayJS.Tests/localhost-dev.pfx new file mode 100644 index 00000000..c30b0ab5 Binary files /dev/null and b/tests/ApplePayJS.Tests/localhost-dev.pfx differ diff --git a/tests/ApplePayJS.Tests/testsettings.json b/tests/ApplePayJS.Tests/testsettings.json new file mode 100644 index 00000000..c4ee5e9b --- /dev/null +++ b/tests/ApplePayJS.Tests/testsettings.json @@ -0,0 +1,17 @@ +{ + "ApplePay": { + "DefaultLanguage": "en-GB", + "StoreName": "Test Store", + "UseCertificateStore": false, + "MerchantCertificateFileName": "applepay-dev.pfx", + "MerchantCertificatePassword": "Pa55w0rd!", + "UsePolyfill": true + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "System": "Warning" + } + } +} diff --git a/tests/ApplePayJS.Tests/xunit.runner.json b/tests/ApplePayJS.Tests/xunit.runner.json new file mode 100644 index 00000000..1d280220 --- /dev/null +++ b/tests/ApplePayJS.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "methodDisplay": "method" +}