diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2e9224..69937b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: - name: Test env: SHOPIFYNET_SHOP_ID: ${{ secrets.SHOPIFYNET_SHOP_ID }} - SHOPIFYNET_SHOP_TOKEN: ${{ secrets.SHOPIFYNET_SHOP_TOKEN }} + SHOPIFYNET_CLIENT_ID: ${{ secrets.SHOPIFYNET_CLIENT_ID }} + SHOPIFYNET_KEY_ID: ${{ secrets.SHOPIFYNET_KEY_ID }} + SHOPIFYNET_PRIVATE_KEY: ${{ secrets.SHOPIFYNET_PRIVATE_KEY }} run: dotnet test --no-restore --verbosity normal - name: Publish to nuget diff --git a/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs b/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs index 02dec24..5729747 100644 --- a/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs +++ b/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs @@ -95,7 +95,15 @@ public async Task RejectsPublicOnlyKeysBeforeSending(string algorithm) using AsymmetricAlgorithm key = algorithm == "ES256" ? ECDsa.Create(ECCurve.NamedCurves.nistP256) : RSA.Create(2048); using var http = new TestHttpClient(_ => throw new AssertFailedException("Public-only key must not be sent.")); var client = new ShopifyOAuthClient("client-id", string.Empty, http); - await Assert.ThrowsExceptionAsync(() => client.RequestClientAssertionAccessTokenAsync( - "test.myshopify.com", "key", key.ExportSubjectPublicKeyInfoPem())); + try + { + await client.RequestClientAssertionAccessTokenAsync( + "test.myshopify.com", "key", key.ExportSubjectPublicKeyInfoPem()); + Assert.Fail("A public-only key must fail signing."); + } + catch (CryptographicException) + { + // Accept platform-specific subclasses, including OpenSSL exceptions on Linux. + } } } diff --git a/ShopifyNet.Tests/ShopifyClientTests.cs b/ShopifyNet.Tests/ShopifyClientTests.cs index ce7099a..bd618cc 100644 --- a/ShopifyNet.Tests/ShopifyClientTests.cs +++ b/ShopifyNet.Tests/ShopifyClientTests.cs @@ -8,8 +8,15 @@ namespace ShopifyNet.Tests; [TestClass] public class ShopifyClientTests { + private static string _accessToken; private ShopifyClient _client; + [ClassInitialize] + public static async Task InitializeClass(TestContext context) + { + _accessToken = await TestHelper.GetAccessTokenAsync(); + } + [TestInitialize] public void Initialize() { @@ -18,7 +25,7 @@ public void Initialize() private static ShopifyClientOptions GetClientOptions(bool throwOnGraphQLErrors = true, bool requestDetailedQueryCost = false) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + return new ShopifyClientOptions(TestHelper.ShopId, _accessToken) { Interceptor = NoOpInterceptor.Instance, ThrowOnGraphQLErrors = throwOnGraphQLErrors, @@ -301,4 +308,4 @@ SYNTAX ERROR!!! Assert.IsNotNull(response.errors); Assert.IsTrue(response.errors.Count > 0); } -} \ No newline at end of file +} diff --git a/ShopifyNet.Tests/TestHelper.cs b/ShopifyNet.Tests/TestHelper.cs index e109d08..bf785f4 100644 --- a/ShopifyNet.Tests/TestHelper.cs +++ b/ShopifyNet.Tests/TestHelper.cs @@ -4,6 +4,16 @@ public class TestHelper { public static string ShopId => Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID"); - public static string Token => Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN", EnvironmentVariableTarget.User) ?? - Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN"); -} \ No newline at end of file + public static string ClientId => Environment.GetEnvironmentVariable("SHOPIFYNET_CLIENT_ID", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_CLIENT_ID"); + public static string PrivateKey => Environment.GetEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY"); + public static string KeyId => Environment.GetEnvironmentVariable("SHOPIFYNET_KEY_ID", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_KEY_ID"); + public static async Task GetAccessTokenAsync() + { + var oauthClient = new ShopifyOAuthClient(ClientId, null); + var token = await oauthClient.RequestClientAssertionAccessTokenAsync(ShopId, KeyId, PrivateKey); + return token.access_token; + } +} diff --git a/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs b/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs index 9136b9c..7dcf666 100644 --- a/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs +++ b/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text.Json; using GraphQLSharp; @@ -14,21 +14,18 @@ public class TokenBucketInterceptorTests private ShopifyClientOptions CreateClientOptions(HttpStatusCode status, object response, IInterceptor interceptor, bool throwOnGraphQLErrors = true) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + var httpClient = new TestHttpClient(_ => new HttpResponseMessage { - Interceptor = interceptor, - ThrowOnGraphQLErrors = throwOnGraphQLErrors, - HttpClient = new TestHttpClient(msg => new HttpResponseMessage - { - StatusCode = status, - Content = JsonContent.Create(response) - }) - }; + StatusCode = status, + Content = JsonContent.Create(response) + }); + + return CreateClientOptions(httpClient, interceptor, throwOnGraphQLErrors); } private ShopifyClientOptions CreateClientOptions(HttpClient httpClient, IInterceptor interceptor, bool throwOnGraphQLErrors = true) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + return new ShopifyClientOptions("test.myshopify.com", "test-token") { Interceptor = interceptor, ThrowOnGraphQLErrors = throwOnGraphQLErrors, @@ -270,7 +267,7 @@ public async Task ShouldProcessInPriorityOrder() int cost = 962; int priority = 0; var interceptor = new TokenBucketInterceptor(r => priority); - var options = new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + var options = new ShopifyClientOptions(TestHelper.ShopId, await TestHelper.GetAccessTokenAsync()) { Interceptor = interceptor, }; @@ -292,4 +289,4 @@ public async Task ShouldProcessInPriorityOrder() var responsesByPriority = responses.OrderBy(i => i.reqPriority).ToArray(); Assert.IsTrue(responsesByPriority.SequenceEqual(responsesByElapsed)); } -} \ No newline at end of file +} diff --git a/ShopifyNet.TypeGenerator/TypeGenerator.cs b/ShopifyNet.TypeGenerator/TypeGenerator.cs index 8888108..1be8841 100644 --- a/ShopifyNet.TypeGenerator/TypeGenerator.cs +++ b/ShopifyNet.TypeGenerator/TypeGenerator.cs @@ -40,10 +40,12 @@ var generator = new GraphQLTypeGenerator(); string csharpCode = await generator.GenerateTypesAsync(options, async query => { - string shopId = Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID", EnvironmentVariableTarget.User)!; - string token = Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN", EnvironmentVariableTarget.User)!; + string shopId = GetRequiredEnvironmentVariable("SHOPIFYNET_SHOP_ID"); + var oauthClient = new ShopifyOAuthClient(GetRequiredEnvironmentVariable("SHOPIFYNET_CLIENT_ID"), null); + var token = await oauthClient.RequestClientAssertionAccessTokenAsync(shopId, + GetRequiredEnvironmentVariable("SHOPIFYNET_KEY_ID"), GetRequiredEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY")); - var shopifyOptions = new ShopifyClientOptions(shopId, token) as IGraphQLClientOptions; + var shopifyOptions = new ShopifyClientOptions(shopId, token.access_token) as IGraphQLClientOptions; var options = new GraphQLClientOptions(shopifyOptions.Uri) { @@ -57,4 +59,9 @@ File.WriteAllText($@"{GetDir()}/../ShopifyNet/Generated.cs", csharpCode); -static string GetDir([CallerFilePath] string filePath = "") => Path.GetDirectoryName(filePath)!; \ No newline at end of file +static string GetDir([CallerFilePath] string filePath = "") => Path.GetDirectoryName(filePath)!; + +static string GetRequiredEnvironmentVariable(string name) => + Environment.GetEnvironmentVariable(name) ?? + Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User) ?? + throw new InvalidOperationException($"Set {name} before running the type generator."); diff --git a/ShopifyNet/OAuth/ShopifyOAuthClient.cs b/ShopifyNet/OAuth/ShopifyOAuthClient.cs index 6368390..82940e8 100644 --- a/ShopifyNet/OAuth/ShopifyOAuthClient.cs +++ b/ShopifyNet/OAuth/ShopifyOAuthClient.cs @@ -53,7 +53,8 @@ public class ShopifyOAuthClient public ShopifyOAuthClient(string clientId, string clientSecret, HttpClient httpClient = null) { _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - _clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); + // The client secret can be null for certain flows, such as client assertion. + _clientSecret = clientSecret; _httpClient = httpClient ?? _defaultHttpClient; } @@ -113,6 +114,9 @@ public static bool IsValidShopDomain(string myShopifyDomain) /// public bool IsValidHmac(IEnumerable> queryParameters) { + if (_clientSecret == null) + throw new ArgumentNullException(nameof(_clientSecret)); + if (queryParameters == null) return false; @@ -389,7 +393,11 @@ private async Task RequestAccessTokenAsync(string myShopifyDomain, Diction parameters[CLIENT_ID_KEY] = _clientId; if (includeClientSecret) + { + if (_clientSecret == null) + throw new ArgumentNullException(nameof(_clientSecret)); parameters[CLIENT_SECRET_KEY] = _clientSecret; + } using var request = new HttpRequestMessage(HttpMethod.Post, $"https://{myShopifyDomain}/admin/oauth/access_token") { diff --git a/ShopifyNet/ShopifyNet.csproj b/ShopifyNet/ShopifyNet.csproj index 8a27710..932392f 100644 --- a/ShopifyNet/ShopifyNet.csproj +++ b/ShopifyNet/ShopifyNet.csproj @@ -5,7 +5,7 @@ https://github.com/Wish-Org/ShopifyNet https://github.com/Wish-Org/ShopifyNet .NET utilities for Shopify GraphQL - 2.19.0 + 2.20.0 shopify graphql types classes schema generator strongly typed