diff --git a/.idea/laravel-shopify.iml b/.idea/laravel-shopify.iml index f39d0417..b0819070 100644 --- a/.idea/laravel-shopify.iml +++ b/.idea/laravel-shopify.iml @@ -137,6 +137,14 @@ + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index f8b21236..0bf0b935 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -1,5 +1,17 @@ + + + + + + @@ -139,9 +151,17 @@ + + + + + + + + - + @@ -149,9 +169,16 @@ + + + + \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ddbac182..9fc87217 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,7 +23,7 @@ src/Exceptions/ src/Objects/Enums/ src/resources/ - src/Messaging/Events/AppLoggedIn.php + src/Messaging/Events/ src/ShopifyAppProvider.php diff --git a/src/Actions/ActivatePlan.php b/src/Actions/ActivatePlan.php index 8b957567..72cb5493 100644 --- a/src/Actions/ActivatePlan.php +++ b/src/Actions/ActivatePlan.php @@ -8,6 +8,7 @@ use Osiset\ShopifyApp\Contracts\Objects\Values\PlanId; use Osiset\ShopifyApp\Contracts\Queries\Plan as IPlanQuery; use Osiset\ShopifyApp\Contracts\Queries\Shop as IShopQuery; +use Osiset\ShopifyApp\Messaging\Events\PlanActivatedEvent; use Osiset\ShopifyApp\Objects\Enums\ChargeStatus; use Osiset\ShopifyApp\Objects\Enums\ChargeType; use Osiset\ShopifyApp\Objects\Enums\PlanType; @@ -142,6 +143,8 @@ public function __invoke(ShopId $shopId, PlanId $planId, ChargeReference $charge $charge = $this->chargeCommand->make($transfer); $this->shopCommand->setToPlan($shopId, $planId); + event(new PlanActivatedEvent($shop, $plan, $charge)); + return $charge; } } diff --git a/src/Actions/AuthenticateShop.php b/src/Actions/AuthenticateShop.php index 93ad11d2..c2d8767c 100644 --- a/src/Actions/AuthenticateShop.php +++ b/src/Actions/AuthenticateShop.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Osiset\ShopifyApp\Contracts\ApiHelper as IApiHelper; +use Osiset\ShopifyApp\Messaging\Events\AppInstalledEvent; use Osiset\ShopifyApp\Objects\Values\ShopDomain; use Osiset\ShopifyApp\Util; @@ -109,6 +110,8 @@ public function __invoke(Request $request): array call_user_func($this->dispatchWebhooksAction, $result['shop_id'], false); call_user_func($this->afterAuthorizeAction, $result['shop_id']); + event(new AppInstalledEvent($result['shop_id'])); + return [$result, true]; } } diff --git a/src/Http/Middleware/IframeProtection.php b/src/Http/Middleware/IframeProtection.php index fb297530..e03bacd3 100644 --- a/src/Http/Middleware/IframeProtection.php +++ b/src/Http/Middleware/IframeProtection.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Cache; use Osiset\ShopifyApp\Contracts\Queries\Shop as IShopQuery; use Osiset\ShopifyApp\Objects\Values\ShopDomain; +use Osiset\ShopifyApp\Util; /** * Responsibility for protection against clickjaking @@ -44,6 +45,7 @@ public function __construct( public function handle(Request $request, Closure $next) { $response = $next($request); + $ancestors = Util::getShopifyConfig('iframe_ancestors'); $shop = Cache::remember( 'frame-ancestors_'.$request->get('shop'), @@ -57,9 +59,15 @@ function () use ($request) { ? $shop->name : '*.myshopify.com'; + $iframeAncestors = "frame-ancestors https://$domain https://admin.shopify.com"; + + if (!blank($ancestors)) { + $iframeAncestors .= ' '.$ancestors; + } + $response->headers->set( 'Content-Security-Policy', - "frame-ancestors https://$domain https://admin.shopify.com" + $iframeAncestors ); return $response; diff --git a/src/Http/Middleware/VerifyShopify.php b/src/Http/Middleware/VerifyShopify.php index b8ac15b3..b7d43e60 100644 --- a/src/Http/Middleware/VerifyShopify.php +++ b/src/Http/Middleware/VerifyShopify.php @@ -83,7 +83,7 @@ public function __construct( * @param Request $request The request object. * @param Closure $next The next action. * - * @throws SignatureVerificationException If HMAC verification fails. + * @throws SignatureVerificationException|HttpException If HMAC verification fails. * * @return mixed */ @@ -102,9 +102,12 @@ public function handle(Request $request, Closure $next) } if (!Util::useNativeAppBridge()) { - $storeResult = !$this->isApiRequest($request) && $this->checkPreviousInstallation($request); + $shop = $this->getShopIfAlreadyInstalled($request); + $storeResult = !$this->isApiRequest($request) && $shop; if ($storeResult) { + $this->loginFromShop($shop); + return $next($request); } } @@ -512,4 +515,36 @@ protected function checkPreviousInstallation(Request $request): bool return $shop && $shop->password && ! $shop->trashed(); } + + /** + * Get shop model if there is a store record in the database. + * + * @param Request $request The request object. + * + * @return ?ShopModel + */ + protected function getShopIfAlreadyInstalled(Request $request): ?ShopModel + { + $shop = $this->shopQuery->getByDomain(ShopDomain::fromRequest($request), [], true); + + return $shop && $shop->password && ! $shop->trashed() ? $shop : null; + } + + /** + * Login and validate store + * + * @param ShopModel $shop + * + * @return void + */ + protected function loginFromShop(ShopModel $shop): void + { + // Override auth guard + if (($guard = Util::getShopifyConfig('shop_auth_guard'))) { + $this->auth->setDefaultDriver($guard); + } + + // All is well, login the shop + $this->auth->login($shop); + } } diff --git a/src/Messaging/Events/AppInstalledEvent.php b/src/Messaging/Events/AppInstalledEvent.php new file mode 100644 index 00000000..cef4e658 --- /dev/null +++ b/src/Messaging/Events/AppInstalledEvent.php @@ -0,0 +1,35 @@ +shopId = $shop_id; + } +} diff --git a/src/Messaging/Events/AppUninstalledEvent.php b/src/Messaging/Events/AppUninstalledEvent.php new file mode 100644 index 00000000..cb02e57b --- /dev/null +++ b/src/Messaging/Events/AppUninstalledEvent.php @@ -0,0 +1,35 @@ +shop = $shop; + } +} diff --git a/src/Messaging/Events/PlanActivatedEvent.php b/src/Messaging/Events/PlanActivatedEvent.php new file mode 100644 index 00000000..8e3b8180 --- /dev/null +++ b/src/Messaging/Events/PlanActivatedEvent.php @@ -0,0 +1,53 @@ +shop = $shop; + $this->plan = $plan; + $this->chargeId = $chargeId; + } +} diff --git a/src/Messaging/Events/ShopAuthenticatedEvent.php b/src/Messaging/Events/ShopAuthenticatedEvent.php new file mode 100644 index 00000000..3cd16b18 --- /dev/null +++ b/src/Messaging/Events/ShopAuthenticatedEvent.php @@ -0,0 +1,35 @@ +shopId = $shopId; + } +} diff --git a/src/Messaging/Events/ShopDeletedEvent.php b/src/Messaging/Events/ShopDeletedEvent.php new file mode 100644 index 00000000..f901dd1f --- /dev/null +++ b/src/Messaging/Events/ShopDeletedEvent.php @@ -0,0 +1,35 @@ +shop = $shop; + } +} diff --git a/src/Messaging/Jobs/AppUninstalledJob.php b/src/Messaging/Jobs/AppUninstalledJob.php index c973e69c..047758a6 100644 --- a/src/Messaging/Jobs/AppUninstalledJob.php +++ b/src/Messaging/Jobs/AppUninstalledJob.php @@ -10,6 +10,7 @@ use Osiset\ShopifyApp\Actions\CancelCurrentPlan; use Osiset\ShopifyApp\Contracts\Commands\Shop as IShopCommand; use Osiset\ShopifyApp\Contracts\Queries\Shop as IShopQuery; +use Osiset\ShopifyApp\Messaging\Events\AppUninstalledEvent; use Osiset\ShopifyApp\Objects\Values\ShopDomain; use Osiset\ShopifyApp\Util; use stdClass; @@ -89,6 +90,8 @@ public function handle( // Soft delete the shop. $shopCommand->softDelete($shopId); + event(new AppUninstalledEvent($shop)); + return true; } } diff --git a/src/ShopifyAppProvider.php b/src/ShopifyAppProvider.php index 2e95706b..0102d220 100644 --- a/src/ShopifyAppProvider.php +++ b/src/ShopifyAppProvider.php @@ -5,6 +5,7 @@ use Illuminate\Routing\Redirector; use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use Osiset\ShopifyApp\Actions\ActivatePlan as ActivatePlanAction; use Osiset\ShopifyApp\Actions\ActivateUsageCharge as ActivateUsageChargeAction; @@ -70,6 +71,10 @@ public function boot() $this->bootMiddlewares(); $this->bootMacros(); $this->bootDirectives(); + + if (version_compare($this->app->version(), '8.0.0', '<')) { + $this->registerEvents(); + } } /** @@ -86,6 +91,13 @@ public function register() WebhookJobMakeCommand::class, ]); + if (version_compare($this->app->version(), '8.0.0', '>=')) { + $this->booting(function () { + $this->registerEvents(); + }); + } + + // Services (start) $this->app->bind(IApiHelper::class, function () { return new ApiHelper(); @@ -346,4 +358,15 @@ private function bootDirectives(): void { Blade::directive('sessionToken', new SessionToken()); } + + private function registerEvents(): void + { + $events = Util::getShopifyConfig('listen'); + + foreach ($events as $event => $listeners) { + foreach (array_unique($listeners, SORT_REGULAR) as $listener) { + Event::listen($event, $listener); + } + } + } } diff --git a/src/Traits/AuthController.php b/src/Traits/AuthController.php index 4476151f..8f2865f2 100644 --- a/src/Traits/AuthController.php +++ b/src/Traits/AuthController.php @@ -11,6 +11,7 @@ use Osiset\ShopifyApp\Exceptions\MissingAuthUrlException; use Osiset\ShopifyApp\Exceptions\MissingShopDomainException; use Osiset\ShopifyApp\Exceptions\SignatureVerificationException; +use Osiset\ShopifyApp\Messaging\Events\ShopAuthenticatedEvent; use Osiset\ShopifyApp\Objects\Values\ShopDomain; use Osiset\ShopifyApp\Util; @@ -57,6 +58,8 @@ public function authenticate(Request $request, AuthenticateShop $authShop) $shopDomain = $shopDomain->toNative(); $shopOrigin = $shopDomain ?? $request->user()->name; + event(new ShopAuthenticatedEvent($result['shop_id'])); + return View::make( 'shopify-app::auth.fullpage_redirect', [ diff --git a/src/Traits/ShopModel.php b/src/Traits/ShopModel.php index 57545cc8..5d8c7cae 100644 --- a/src/Traits/ShopModel.php +++ b/src/Traits/ShopModel.php @@ -11,6 +11,7 @@ use Osiset\ShopifyApp\Contracts\Objects\Values\AccessToken as AccessTokenValue; use Osiset\ShopifyApp\Contracts\Objects\Values\ShopDomain as ShopDomainValue; use Osiset\ShopifyApp\Contracts\Objects\Values\ShopId as ShopIdValue; +use Osiset\ShopifyApp\Messaging\Events\ShopDeletedEvent; use Osiset\ShopifyApp\Objects\Values\AccessToken; use Osiset\ShopifyApp\Objects\Values\SessionContext; use Osiset\ShopifyApp\Objects\Values\ShopDomain; @@ -51,6 +52,10 @@ trait ShopModel protected static function bootShopModel(): void { static::addGlobalScope(new Namespacing()); + + static::deleted(function ($shop) { + event(new ShopDeletedEvent($shop)); + }); } /** diff --git a/src/Util.php b/src/Util.php index 67a5a3e1..293bf7cb 100644 --- a/src/Util.php +++ b/src/Util.php @@ -17,7 +17,7 @@ class Util /** * HMAC creation helper. * - * @param array $opts The options for building the HMAC. + * @param array $opts The options for building the HMAC. * @param string $secret The app secret key. * * @return Hmac @@ -61,7 +61,7 @@ public static function createHmac(array $opts, string $secret): Hmac * See: https://github.com/rack/rack/blob/f9ad97fd69a6b3616d0a99e6bedcfb9de2f81f6c/lib/rack/query_parser.rb#L36 * * @param string $queryString The query string. - * @param string|null $delimiter The delimiter. + * @param string|null $delimiter The delimiter. * * @return mixed */ @@ -77,7 +77,7 @@ public static function parseQueryString(string $queryString, string $delimiter = ); foreach ($split as $part) { - if (! $part) { + if (!$part) { continue; } @@ -135,7 +135,7 @@ public static function base64UrlDecode($data) /** * Checks if the route should be registered or not. * - * @param string $routeToCheck The route name to check. + * @param string $routeToCheck The route name to check. * @param bool|array $routesToExclude The routes which are to be excluded. * * @return bool @@ -158,8 +158,8 @@ public static function registerPackageRoute(string $routeToCheck, $routesToExclu * Used as a helper function so it is accessible in Blade. * The second param of `shop` is important for `config_api_callback`. * - * @param string $key The key to lookup. - * @param mixed $shop The shop domain (string, ShopDomain, etc). + * @param string $key The key to lookup. + * @param mixed $shop The shop domain (string, ShopDomain, etc). * * @return mixed */ @@ -207,8 +207,8 @@ public static function getShopifyConfig(string $key, $shop = null) public static function getGraphQLWebhookTopic(string $topic): string { return Str::of($topic) - ->upper() - ->replaceMatches('/[^A-Z_]/', '_'); + ->upper() + ->replaceMatches('/[^A-Z_]/', '_'); } @@ -246,4 +246,11 @@ public static function useNativeAppBridge(): bool return !$frontendEngine->isSame($reactEngine); } + + public static function hasAppLegacySupport(string $feature): bool + { + $legacySupports = self::getShopifyConfig('app_legacy_supports') ?? []; + + return (bool) Arr::get($legacySupports, $feature, true); + } } diff --git a/src/resources/config/shopify-app.php b/src/resources/config/shopify-app.php index ad53f005..cda82baf 100644 --- a/src/resources/config/shopify-app.php +++ b/src/resources/config/shopify-app.php @@ -330,6 +330,42 @@ 'billing_redirect' => env('SHOPIFY_BILLING_REDIRECT', '/billing/process'), + + /* + |-------------------------------------------------------------------------- + | Enable legacy support for features + |-------------------------------------------------------------------------- + | + */ + 'app_legacy_supports' => [ + 'after_authenticate_job' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Register listeners to the events + |-------------------------------------------------------------------------- + | + */ + + 'listen' => [ + \Osiset\ShopifyApp\Messaging\Events\AppInstalledEvent::class => [ + // \App\Listeners\MyListener::class, + ], + \Osiset\ShopifyApp\Messaging\Events\ShopAuthenticatedEvent::class => [ + // \App\Listeners\MyListener::class, + ], + \Osiset\ShopifyApp\Messaging\Events\ShopDeletedEvent::class => [ + // \App\Listeners\MyListener::class, + ], + \Osiset\ShopifyApp\Messaging\Events\AppUninstalledEvent::class => [ + // \App\Listeners\MyListener::class, + ], + \Osiset\ShopifyApp\Messaging\Events\PlanActivatedEvent::class => [ + // \App\Listeners\MyListener::class, + ], + ], + /* |-------------------------------------------------------------------------- | Shopify Webhooks @@ -392,8 +428,13 @@ | This, like webhooks and scripttag jobs, will fire every time a shop | authenticates, not just once. | + | */ + /* + * @deprecated This will be removed in the next major version. + * @see + */ 'after_authenticate_job' => [ /* [ @@ -498,26 +539,26 @@ */ 'theme_support' => [ - /** + /* * Specify the name of the template the app will integrate with */ 'templates' => ['product', 'collection', 'index'], - /** + /* * Interval for caching the request: minutes, seconds, hours, days, etc. */ 'cache_interval' => 'hours', - /** + /* * Cache duration */ 'cache_duration' => '12', - /** + /* * At which levels of theme support the use of "theme app extension" is not available * and script tags will be installed. * Available levels: FULL, PARTIAL, UNSUPPORTED. */ 'unacceptable_levels' => [ - Osiset\ShopifyApp\Objects\Enums\ThemeSupportLevel::UNSUPPORTED - ] + Osiset\ShopifyApp\Objects\Enums\ThemeSupportLevel::UNSUPPORTED, + ], ], /* @@ -542,4 +583,6 @@ | */ 'frontend_engine' => env('SHOPIFY_FRONTEND_ENGINE', 'BLADE'), + + 'iframe_ancestors' => '', ]; diff --git a/tests/Actions/ActivatePlanTest.php b/tests/Actions/ActivatePlanTest.php index be6fb66f..97bb7c54 100644 --- a/tests/Actions/ActivatePlanTest.php +++ b/tests/Actions/ActivatePlanTest.php @@ -2,7 +2,9 @@ namespace Osiset\ShopifyApp\Test\Actions; +use Illuminate\Support\Facades\Event; use Osiset\ShopifyApp\Actions\ActivatePlan; +use Osiset\ShopifyApp\Messaging\Events\PlanActivatedEvent; use Osiset\ShopifyApp\Objects\Values\ChargeId; use Osiset\ShopifyApp\Objects\Values\ChargeReference; use Osiset\ShopifyApp\Storage\Models\Charge; @@ -27,6 +29,7 @@ public function setUp(): void public function testRunRecurring(): void { + Event::fake(); // Create a plan $plan = factory(Util::getShopifyConfig('models.plan', Plan::class))->states('type_recurring')->create(); @@ -56,10 +59,12 @@ public function testRunRecurring(): void ); $this->assertInstanceOf(ChargeId::class, $result); + Event::assertDispatched(PlanActivatedEvent::class); } public function testRunOnetime(): void { + Event::fake(); // Create a plan $plan = factory(Util::getShopifyConfig('models.plan', Plan::class))->states('type_onetime')->create(); @@ -89,6 +94,7 @@ public function testRunOnetime(): void ); $this->assertInstanceOf(ChargeId::class, $result); + Event::assertDispatched(PlanActivatedEvent::class); } //TODO we need to test for both myshopify and admin hosts diff --git a/tests/Actions/AuthenticateShopTest.php b/tests/Actions/AuthenticateShopTest.php index 49fcb4de..f5699be6 100644 --- a/tests/Actions/AuthenticateShopTest.php +++ b/tests/Actions/AuthenticateShopTest.php @@ -2,8 +2,10 @@ namespace Osiset\ShopifyApp\Test\Actions; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Request; use Osiset\ShopifyApp\Actions\AuthenticateShop; +use Osiset\ShopifyApp\Messaging\Events\AppInstalledEvent; use Osiset\ShopifyApp\Test\Stubs\Api as ApiStub; use Osiset\ShopifyApp\Test\TestCase; @@ -88,6 +90,7 @@ public function testShouldGoToAuthRedirectForInvalidHmac(): void public function testRuns(): void { + Event::fake(); // Build request $currentRequest = Request::instance(); $newRequest = $currentRequest->duplicate( @@ -121,5 +124,6 @@ public function testRuns(): void [, $status] = call_user_func($this->action, $newRequest); $this->assertTrue($status); + Event::assertDispatched(AppInstalledEvent::class); } } diff --git a/tests/Http/Middleware/IframeProtectionTest.php b/tests/Http/Middleware/IframeProtectionTest.php index a02448db..14bba2d8 100644 --- a/tests/Http/Middleware/IframeProtectionTest.php +++ b/tests/Http/Middleware/IframeProtectionTest.php @@ -63,4 +63,28 @@ public function testIframeProtectionWithUnauthorizedShop(): void $this->assertNotEmpty($currentHeader); $this->assertEquals($expectedHeader, $currentHeader); } + + public function testIframeProtectionWithExistingAncestorsInConfig(): void + { + $shop = factory($this->model)->create(); + $this->auth->login($shop); + $this->app['config']->set('shopify-app.iframe_ancestors', 'https://example.com'); + + $domain = auth()->user()->name; + $expectedHeader = "frame-ancestors https://$domain https://admin.shopify.com https://example.com"; + + $request = new Request(); + $shopQueryStub = $this->createStub(ShopQuery::class); + $shopQueryStub->method('getByDomain')->willReturn($shop); + $next = function () { + return new Response('Test Response'); + }; + + $middleware = new IframeProtection($shopQueryStub); + $response = $middleware->handle($request, $next); + $currentHeader = $response->headers->get('content-security-policy'); + + $this->assertNotEmpty($currentHeader); + $this->assertEquals($expectedHeader, $currentHeader); + } } diff --git a/tests/Http/Middleware/VerifyShopifyTest.php b/tests/Http/Middleware/VerifyShopifyTest.php index 047584c7..79869ed2 100644 --- a/tests/Http/Middleware/VerifyShopifyTest.php +++ b/tests/Http/Middleware/VerifyShopifyTest.php @@ -317,4 +317,36 @@ public function testTokenProcessingAndMissMatchingShops(): void $this->expectException(HttpException::class); $this->runMiddleware(VerifyShopify::class, $newRequest); } + + public function testNotNativeAppbridgeWithTokenProcessingAndLoginShop(): void + { + // Create a shop that matches the token from buildToken + factory($this->model)->create(['name' => 'shop-name.myshopify.com']); + $this->app['config']->set('shopify-app.frontend_engine', 'REACT'); + + // Setup the request + $currentRequest = Request::instance(); + $newRequest = $currentRequest->duplicate( + // Query Params + [ + 'shop' => 'shop-name.myshopify.com', + ], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + [ + 'HTTP_Authorization' => "Bearer {$this->buildToken()}", + ] + ); + + // Run the middleware + $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $this->assertTrue($result[0]); + } } diff --git a/tests/Messaging/Jobs/AppUninstalledTest.php b/tests/Messaging/Jobs/AppUninstalledTest.php index f4882360..e4250563 100644 --- a/tests/Messaging/Jobs/AppUninstalledTest.php +++ b/tests/Messaging/Jobs/AppUninstalledTest.php @@ -2,6 +2,8 @@ namespace Osiset\ShopifyApp\Test\Messaging\Jobs; +use Illuminate\Support\Facades\Event; +use Osiset\ShopifyApp\Messaging\Events\AppUninstalledEvent; use Osiset\ShopifyApp\Messaging\Jobs\AppUninstalledJob; use Osiset\ShopifyApp\Objects\Enums\ChargeStatus; use Osiset\ShopifyApp\Storage\Models\Charge; @@ -32,6 +34,7 @@ public function testJobSoftDeletesShopAndCharges(): void $this->assertNotNull($shop->plan); $this->assertNotEmpty($shop->password); + Event::fake(); // Run the job AppUninstalledJob::dispatchSync( $shop->getDomain()->toNative(), @@ -46,5 +49,6 @@ public function testJobSoftDeletesShopAndCharges(): void $this->assertFalse($shop->hasCharges()); $this->assertNull($shop->plan); $this->assertEmpty($shop->password); + Event::assertDispatched(AppUninstalledEvent::class); } } diff --git a/tests/Traits/AuthControllerTest.php b/tests/Traits/AuthControllerTest.php index 9988ffe3..a435c7c9 100644 --- a/tests/Traits/AuthControllerTest.php +++ b/tests/Traits/AuthControllerTest.php @@ -3,7 +3,10 @@ namespace Osiset\ShopifyApp\Test\Traits; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Request; use Osiset\ShopifyApp\Exceptions\MissingShopDomainException; +use Osiset\ShopifyApp\Messaging\Events\ShopAuthenticatedEvent; use Osiset\ShopifyApp\Test\Stubs\Api as ApiStub; use Osiset\ShopifyApp\Test\TestCase; use Osiset\ShopifyApp\Util; @@ -20,6 +23,7 @@ public function setUp(): void public function testAuthRedirectsToShopifyWhenNoCode(): void { + Event::fake(); // Run the request $response = $this->call('post', '/authenticate', ['shop' => 'example.myshopify.com']); @@ -29,6 +33,8 @@ public function testAuthRedirectsToShopifyWhenNoCode(): void 'authUrl', 'https://example.myshopify.com/admin/oauth/authorize?client_id='.Util::getShopifyConfig('api_key').'&scope=read_products%2Cwrite_products%2Cread_themes&redirect_uri=https%3A%2F%2Flocalhost%2Fauthenticate' ); + + Event::assertDispatched(ShopAuthenticatedEvent::class); } public function testAuthAcceptsShopWithCode(): void diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 13cff9aa..e88ee5b8 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -123,4 +123,15 @@ public function testUseNativeAppBridgeIsFalse(): void $this->assertFalse($result); } + + public function testHasAppLegacySupport(): void + { + $supportedFeatures = $this->app['config']->get('shopify-app.app_legacy_supports', []); + foreach ($supportedFeatures as $feature => $val) { + $this->assertSame( + $val, + Util::hasAppLegacySupport($feature) + ); + } + } }