From aa18281cee520c4f4dc9a592072bbef8be92bd02 Mon Sep 17 00:00:00 2001 From: Albert Scherman Date: Thu, 18 Jul 2024 10:14:13 +0200 Subject: [PATCH 1/2] NEXT-37028 - Added IAP filter --- src/Context/ContextResolver.php | 57 ++++++++++++------- .../Gateway/InAppFeatures/FilterAction.php | 22 +++++++ src/Response/InAppPurchasesResponse.php | 33 +++++++++++ tests/Context/ContextResolverTest.php | 35 ++++++++++++ .../InAppFeatures/FilterActionTest.php | 30 ++++++++++ tests/Response/InAppPurchasesResponseTest.php | 22 +++++++ 6 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 src/Context/Gateway/InAppFeatures/FilterAction.php create mode 100644 src/Response/InAppPurchasesResponse.php create mode 100644 tests/Context/Gateway/InAppFeatures/FilterActionTest.php create mode 100644 tests/Response/InAppPurchasesResponseTest.php diff --git a/src/Context/ContextResolver.php b/src/Context/ContextResolver.php index 0698ca7..e0cb8b8 100644 --- a/src/Context/ContextResolver.php +++ b/src/Context/ContextResolver.php @@ -9,6 +9,7 @@ use Shopware\App\SDK\Context\ActionButton\ActionButtonAction; use Shopware\App\SDK\Context\Cart\Cart; use Shopware\App\SDK\Context\Gateway\Checkout\CheckoutGatewayAction; +use Shopware\App\SDK\Context\Gateway\InAppFeatures\FilterAction; use Shopware\App\SDK\Context\InAppPurchase\InAppPurchaseProvider; use Shopware\App\SDK\Context\Module\ModuleAction; use Shopware\App\SDK\Context\Order\Order; @@ -39,12 +40,15 @@ public function __construct(private readonly InAppPurchaseProvider $inAppPurchas { } + /** + * @throws \JsonException + */ public function assembleWebhook(RequestInterface $request, ShopInterface $shop): WebhookAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -59,10 +63,10 @@ public function assembleWebhook(RequestInterface $request, ShopInterface $shop): public function assembleActionButton(RequestInterface $request, ShopInterface $shop): ActionButtonAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -108,10 +112,10 @@ public function assembleModule(RequestInterface $request, ShopInterface $shop): public function assembleTaxProvider(RequestInterface $request, ShopInterface $shop): TaxProviderAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -125,10 +129,10 @@ public function assembleTaxProvider(RequestInterface $request, ShopInterface $sh public function assemblePaymentPay(RequestInterface $request, ShopInterface $shop): PaymentPayAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -145,10 +149,10 @@ public function assemblePaymentPay(RequestInterface $request, ShopInterface $sho public function assemblePaymentFinalize(RequestInterface $request, ShopInterface $shop): PaymentFinalizeAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -163,10 +167,10 @@ public function assemblePaymentFinalize(RequestInterface $request, ShopInterface public function assemblePaymentCapture(RequestInterface $request, ShopInterface $shop): PaymentCaptureAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -182,10 +186,10 @@ public function assemblePaymentCapture(RequestInterface $request, ShopInterface public function assemblePaymentRecurringCapture(RequestInterface $request, ShopInterface $shop): PaymentRecurringAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -199,10 +203,10 @@ public function assemblePaymentRecurringCapture(RequestInterface $request, ShopI public function assemblePaymentValidate(RequestInterface $request, ShopInterface $shop): PaymentValidateAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -217,10 +221,10 @@ public function assemblePaymentValidate(RequestInterface $request, ShopInterface public function assemblePaymentRefund(RequestInterface $request, ShopInterface $shop): RefundAction { - $body = json_decode($request->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); $request->getBody()->rewind(); - if (!is_array($body) || !isset($body['source']) || !is_array($body['source'])) { + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { throw new MalformedWebhookBodyException(); } @@ -288,9 +292,24 @@ public function assembleCheckoutGatewayRequest(RequestInterface $request, ShopIn ); } + public function assembleInAppPurchasesFilterRequest(RequestInterface $request, ShopInterface $shop): FilterAction + { + $body = \json_decode($request->getBody()->getContents(), true, flags: \JSON_THROW_ON_ERROR); + $request->getBody()->rewind(); + + if (!\is_array($body) || !isset($body['source']) || !\is_array($body['source'])) { + throw new MalformedWebhookBodyException(); + } + + return new FilterAction( + $shop, + $this->parseSource($body['source']), + new Collection($body['purchases']), + ); + } + /** * @param array $source - * @return ActionSource */ private function parseSource(array $source, ShopInterface $shop): ActionSource { diff --git a/src/Context/Gateway/InAppFeatures/FilterAction.php b/src/Context/Gateway/InAppFeatures/FilterAction.php new file mode 100644 index 0000000..5e30f45 --- /dev/null +++ b/src/Context/Gateway/InAppFeatures/FilterAction.php @@ -0,0 +1,22 @@ + $purchases + */ + public function __construct( + public readonly ShopInterface $shop, + public readonly ActionSource $source, + public readonly Collection $purchases, + ) { + } +} diff --git a/src/Response/InAppPurchasesResponse.php b/src/Response/InAppPurchasesResponse.php new file mode 100644 index 0000000..1d004d1 --- /dev/null +++ b/src/Response/InAppPurchasesResponse.php @@ -0,0 +1,33 @@ + $purchases + * @throws \JsonException + */ + public static function filter(array $purchases): ResponseInterface + { + return self::createResponse(['purchases' => $purchases]); + } + + /** + * @param array $data + * @throws \JsonException + */ + private static function createResponse(array $data): ResponseInterface + { + $psr = new Psr17Factory(); + + return $psr->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($psr->createStream(json_encode($data, JSON_THROW_ON_ERROR))); + } +} diff --git a/tests/Context/ContextResolverTest.php b/tests/Context/ContextResolverTest.php index 179beb3..2450664 100644 --- a/tests/Context/ContextResolverTest.php +++ b/tests/Context/ContextResolverTest.php @@ -1138,6 +1138,41 @@ public function testAssembleCheckoutGatewayRequest(): void static::assertSame('id2', $shippingMethods->get('technicalName2')); } + public function testAssembleInAppPurchasesFilterRequest(): void + { + $contextResolver = new ContextResolver(); + + $body = [ + 'source' => [ + 'url' => 'https://example.com', + 'appVersion' => 'foo', + ], + 'purchases' => [ + 'identifier-1', + 'identifier-2', + 'identifier-3', + ] + ]; + + $expectedPurchases = new Collection([ + 'identifier-1', + 'identifier-2', + 'identifier-3', + ]); + + $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $action = $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); + + static::assertSame('https://example.com', $action->source->url); + static::assertSame('foo', $action->source->appVersion); + + $purchases = $action->purchases; + static::assertCount(3, $purchases); + static::assertInstanceOf(Collection::class, $purchases); + static::assertEquals($expectedPurchases, $purchases); + } + /** * @dataProvider methodsProvider */ diff --git a/tests/Context/Gateway/InAppFeatures/FilterActionTest.php b/tests/Context/Gateway/InAppFeatures/FilterActionTest.php new file mode 100644 index 0000000..a5bbfa9 --- /dev/null +++ b/tests/Context/Gateway/InAppFeatures/FilterActionTest.php @@ -0,0 +1,30 @@ +shop); + static::assertSame($source, $action->source); + static::assertSame($purchases, $action->purchases); + } +} diff --git a/tests/Response/InAppPurchasesResponseTest.php b/tests/Response/InAppPurchasesResponseTest.php new file mode 100644 index 0000000..e3268b1 --- /dev/null +++ b/tests/Response/InAppPurchasesResponseTest.php @@ -0,0 +1,22 @@ +getStatusCode()); + static::assertSame('{"purchases":["foo","bar"]}', $response->getBody()->getContents()); + } +} From 9195769e33003ab42b5d2dc5cfa070f1b647d3f3 Mon Sep 17 00:00:00 2001 From: Lennart Tinkloh Date: Mon, 16 Dec 2024 10:17:43 +0100 Subject: [PATCH 2/2] NEXT-37208 - add IAP filter JWKS support --- src/Context/ContextResolver.php | 8 +- .../Gateway/InAppFeatures/FilterAction.php | 5 +- src/Response/InAppPurchasesResponse.php | 11 ++- tests/Context/ContextResolverTest.php | 81 +++++++++++++++---- .../InAppFeatures/FilterActionTest.php | 4 +- tests/Response/InAppPurchasesResponseTest.php | 4 +- 6 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/Context/ContextResolver.php b/src/Context/ContextResolver.php index e0cb8b8..0107ffb 100644 --- a/src/Context/ContextResolver.php +++ b/src/Context/ContextResolver.php @@ -301,10 +301,14 @@ public function assembleInAppPurchasesFilterRequest(RequestInterface $request, S throw new MalformedWebhookBodyException(); } + if (!isset($body['purchases']) || !\is_array($body['purchases'])) { + throw new MalformedWebhookBodyException(); + } + return new FilterAction( $shop, - $this->parseSource($body['source']), - new Collection($body['purchases']), + $this->parseSource($body['source'], $shop), + new Collection($body['purchases']) ); } diff --git a/src/Context/Gateway/InAppFeatures/FilterAction.php b/src/Context/Gateway/InAppFeatures/FilterAction.php index 5e30f45..16f3e29 100644 --- a/src/Context/Gateway/InAppFeatures/FilterAction.php +++ b/src/Context/Gateway/InAppFeatures/FilterAction.php @@ -11,7 +11,10 @@ class FilterAction { /** - * @param Collection $purchases + * Use this action to filter in-app purchases to be *available* to buy for the customer. + * In the ActionSource you can find any *active* purchases. + * + * @param Collection $purchases - The list of purchases to filter for */ public function __construct( public readonly ShopInterface $shop, diff --git a/src/Response/InAppPurchasesResponse.php b/src/Response/InAppPurchasesResponse.php index 1d004d1..8f39f27 100644 --- a/src/Response/InAppPurchasesResponse.php +++ b/src/Response/InAppPurchasesResponse.php @@ -6,21 +6,20 @@ use Http\Discovery\Psr17Factory; use Psr\Http\Message\ResponseInterface; +use Shopware\App\SDK\Framework\Collection; class InAppPurchasesResponse { /** - * @param array $purchases - * @throws \JsonException + * @param Collection $purchases */ - public static function filter(array $purchases): ResponseInterface + public static function filter(Collection $purchases): ResponseInterface { - return self::createResponse(['purchases' => $purchases]); + return self::createResponse(['purchases' => $purchases->all()]); } /** * @param array $data - * @throws \JsonException */ private static function createResponse(array $data): ResponseInterface { @@ -28,6 +27,6 @@ private static function createResponse(array $data): ResponseInterface return $psr->createResponse(200) ->withHeader('Content-Type', 'application/json') - ->withBody($psr->createStream(json_encode($data, JSON_THROW_ON_ERROR))); + ->withBody($psr->createStream(\json_encode($data, \JSON_THROW_ON_ERROR))); } } diff --git a/tests/Context/ContextResolverTest.php b/tests/Context/ContextResolverTest.php index 2450664..21c771b 100644 --- a/tests/Context/ContextResolverTest.php +++ b/tests/Context/ContextResolverTest.php @@ -1140,26 +1140,24 @@ public function testAssembleCheckoutGatewayRequest(): void public function testAssembleInAppPurchasesFilterRequest(): void { - $contextResolver = new ContextResolver(); + $expectedPurchases = new Collection(['identifier-1', 'identifier-2', 'identifier-3']); + + $inAppPurchaseProvider = $this->createMock(InAppPurchaseProvider::class); + $inAppPurchaseProvider + ->method('decodePurchases') + ->with('["identifier-1", "identifier-2", "identifier-3"]') + ->willReturn($expectedPurchases); + + $contextResolver = new ContextResolver($inAppPurchaseProvider); $body = [ 'source' => [ 'url' => 'https://example.com', 'appVersion' => 'foo', ], - 'purchases' => [ - 'identifier-1', - 'identifier-2', - 'identifier-3', - ] + 'purchases' => ['identifier-1', 'identifier-2', 'identifier-3'], ]; - $expectedPurchases = new Collection([ - 'identifier-1', - 'identifier-2', - 'identifier-3', - ]); - $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); $action = $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); @@ -1173,9 +1171,61 @@ public function testAssembleInAppPurchasesFilterRequest(): void static::assertEquals($expectedPurchases, $purchases); } - /** - * @dataProvider methodsProvider - */ + public function testAssembleInAppPurchasesWithEmptyPurchases(): void + { + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + + $body = [ + 'source' => [ + 'url' => 'https://example.com', + 'appVersion' => 'foo', + ], + 'purchases' => [], + ]; + + $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $action = $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); + + static::assertEmpty($action->purchases); + } + + public function testAssembleInAppPurchasesWithMalformedPurchases(): void + { + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + + $body = [ + 'source' => [ + 'url' => 'https://example.com', + 'appVersion' => 'foo', + ], + 'purchases' => 'foo-bar', + ]; + + $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $this->expectException(MalformedWebhookBodyException::class); + + $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); + } + + public function testAssembleInAppPurchasesWithMalformedSource(): void + { + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + + $body = [ + 'source' => 'foo', + 'purchases' => ['foo', 'bar'], + ]; + + $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $this->expectException(MalformedWebhookBodyException::class); + + $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); + } + + #[DataProvider('methodsProvider')] public function testBodyRewindIsCalled(string $method): void { $body = static::createMock(StreamInterface::class); @@ -1251,6 +1301,7 @@ public static function methodsProvider(): iterable yield ['assemblePaymentRefund']; yield ['assemblePaymentRecurringCapture']; yield ['assembleCheckoutGatewayRequest']; + yield ['assembleInAppPurchasesFilterRequest']; } /** diff --git a/tests/Context/Gateway/InAppFeatures/FilterActionTest.php b/tests/Context/Gateway/InAppFeatures/FilterActionTest.php index a5bbfa9..6fbc70d 100644 --- a/tests/Context/Gateway/InAppFeatures/FilterActionTest.php +++ b/tests/Context/Gateway/InAppFeatures/FilterActionTest.php @@ -16,10 +16,10 @@ class FilterActionTest extends TestCase { public function testConstruct(): void { - $shop = new MockShop('foo', 'https://example.com', 'secret'); - $source = new ActionSource('https://example.com', '1.0.0'); $purchases = new Collection(['purchase-1', 'purchase-2', 'purchase-3']); + $shop = new MockShop('foo', 'https://example.com', 'secret'); + $source = new ActionSource('https://example.com', '1.0.0', new Collection()); $action = new FilterAction($shop, $source, $purchases); diff --git a/tests/Response/InAppPurchasesResponseTest.php b/tests/Response/InAppPurchasesResponseTest.php index e3268b1..a498083 100644 --- a/tests/Response/InAppPurchasesResponseTest.php +++ b/tests/Response/InAppPurchasesResponseTest.php @@ -5,6 +5,7 @@ namespace Shopware\App\SDK\Tests\Response; use PHPUnit\Framework\Attributes\CoversClass; +use Shopware\App\SDK\Framework\Collection; use Shopware\App\SDK\Response\InAppPurchasesResponse; use PHPUnit\Framework\TestCase; @@ -13,10 +14,11 @@ class InAppPurchasesResponseTest extends TestCase { public function testPaid(): void { - $purchases = ['foo', 'bar']; + $purchases = new Collection(['foo', 'bar']); $response = InAppPurchasesResponse::filter($purchases); static::assertSame(200, $response->getStatusCode()); + static::assertSame('application/json', $response->getHeaderLine('Content-Type')); static::assertSame('{"purchases":["foo","bar"]}', $response->getBody()->getContents()); } }