Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: auto disconnect delay #428

Merged
merged 13 commits into from
Oct 2, 2024
8 changes: 8 additions & 0 deletions src/Account/LinkShop.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ public function exists()
return (bool) $this->getShopUuid();
}

/**
* @return string|null
*/
public function linkedAt()
{
return $this->configuration->getShopUuidDateUpd();
}

/**
* @return bool
*
Expand Down
45 changes: 34 additions & 11 deletions src/Account/Session/ShopSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class ShopSession extends Session implements SessionInterface
*/
protected $linkShop;

/**
* @var int
*/
protected $oauth2ClientReceiptTimeout = 60;

/**
* @param ConfigurationRepository $configurationRepository
* @param ShopProvider $oauth2ClientProvider
Expand Down Expand Up @@ -84,7 +89,7 @@ public function __construct(
public function refreshToken($refreshToken = null)
{
try {
$this->assertAssociationState();
$this->assertAssociationState($this->oauth2ClientReceiptTimeout);
$shopUuid = $this->getShopUuid();
$accessToken = $this->getAccessToken($shopUuid);

Expand Down Expand Up @@ -138,6 +143,16 @@ public function cleanup()
$this->configurationRepository->updateAccessToken('');
}

/**
* @param int $oauth2ClientReceiptTimeout
*
* @return void
*/
public function setOauth2ClientReceiptTimeout($oauth2ClientReceiptTimeout)
{
$this->oauth2ClientReceiptTimeout = $oauth2ClientReceiptTimeout;
}

/**
* @param string $shopUid
*
Expand All @@ -161,23 +176,31 @@ protected function getAccessToken($shopUid)
}

/**
* @return string
*/
private function getShopUuid()
{
return $this->linkShop->getShopUuid();
}

/**
* @throws InconsistentAssociationStateException
* @param int $waitForOAuth2ClientSeconds
*
* @return void
*
* @throws InconsistentAssociationStateException
*/
public function assertAssociationState()
protected function assertAssociationState($waitForOAuth2ClientSeconds = 60)
jokesterfr marked this conversation as resolved.
Show resolved Hide resolved
{
$linkedAtTs = $currentTs = time();
if ($this->linkShop->linkedAt()) {
$linkedAtTs = (new \DateTime($this->linkShop->linkedAt()))->getTimestamp();
}

if ($this->linkShop->exists() &&
$currentTs - $linkedAtTs > $waitForOAuth2ClientSeconds &&
!$this->oauth2ClientProvider->getOauth2Client()->exists()) {
throw new InconsistentAssociationStateException('Invalid OAuth2 client');
}
}

/**
* @return string
*/
private function getShopUuid()
{
return $this->linkShop->getShopUuid();
}
}
23 changes: 23 additions & 0 deletions src/Adapter/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,27 @@ public function getUncached($key, $idShopGroup = null, $idShop = null, $default

return $default;
}

/**
* @param string $key
* @param int|null $idShopGroup
* @param int|null $idShop
* @param mixed $default
*
* @return \Configuration
*
* @throw \Exception
*/
public function getUncachedConfiguration($key, $idShopGroup = null, $idShop = null, $default = false)
{
$id = \Configuration::getIdByName($key, $idShopGroup, $idShop);
if ($id > 0) {
$found = (new \Configuration($id));
$found->clearCache();

return $found;
}

throw new \Exception('Configuration entry not found');
}
}
17 changes: 17 additions & 0 deletions src/Repository/ConfigurationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ public function getShopUuid()
return $this->configuration->get(ConfigurationKeys::PSX_UUID_V4);
}

/**
* @return string|null
*/
public function getShopUuidDateUpd()
{
try {
$entry = $this->configuration->getUncachedConfiguration(
ConfigurationKeys::PSX_UUID_V4,
$this->configuration->getIdShopGroup(),
$this->configuration->getIdShop());

return $entry->date_upd;
} catch (\Exception $e) {
return null;
}
}

/**
* @param string $uuid Firebase User UUID
*
Expand Down
3 changes: 3 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class TestCase extends \PHPUnit\Framework\TestCase
*/
protected function setUp(): void
{
// Don't remove this line
\Configuration::clearConfigurationCacheForTesting();

parent::setUp();

if (true === $this->enableTransactions) {
Expand Down
182 changes: 113 additions & 69 deletions tests/Unit/Account/Session/ShopSession/RefreshTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PrestaShop\Module\PsAccounts\Account\LinkShop;
use PrestaShop\Module\PsAccounts\Account\Session\ShopSession;
use PrestaShop\Module\PsAccounts\Account\Token\Token;
use PrestaShop\Module\PsAccounts\Cqrs\CommandBus;
use PrestaShop\Module\PsAccounts\Exception\RefreshTokenException;
use PrestaShop\Module\PsAccounts\Provider\OAuth2\Oauth2Client;
use PrestaShop\Module\PsAccounts\Provider\OAuth2\ShopProvider;
Expand All @@ -29,6 +30,46 @@ class RefreshTokenTest extends TestCase
*/
protected $oauth2Client;

/**
* @inject
*
* @var ShopProvider
*/
protected $shopProvider;

/**
* @var \PrestaShop\Module\PsAccounts\Vendor\Lcobucci\JWT\Token
*/
protected $validAccessToken;

function setUp(): void
{
parent::setUp();

$this->validAccessToken = $this->makeJwtToken(new \DateTimeImmutable('tomorrow'));
$shopProvider = $this->createMock(ShopProvider::class);
$shopProvider->method('getAccessToken')
->willReturn(new AccessToken([
'access_token' => (string)$this->validAccessToken
]));
$shopProvider->method('getOauth2Client')
->willReturn($this->oauth2Client);

$commandBus = $this->createMock(CommandBus::class);

$this->shopSession = new ShopSession(
$this->configurationRepository,
$shopProvider,
$this->linkShop,
$commandBus
);

// Fix single shop context
$this->configuration->setIdShop(null);
$this->configuration->setIdShopGroup(null);
$this->shopSession->cleanup();
}

/**
* @return void
*/
Expand All @@ -44,95 +85,98 @@ public function tearDown(): void
*/
public function itShouldClearConfigurationAndThrowIfNoOauth()
{
$shopSession = $this->createMockedSession(null, false, true);
$shopSession->cleanup();

$accessToken = $this->makeJwtToken(new \DateTimeImmutable('yesterday'), [
'sub' => $this->faker->uuid,
]);
$refreshToken = $this->makeJwtToken(new \DateTimeImmutable('+1 year'));
$shopSession->setToken((string) $accessToken, (string) $refreshToken);

$e = null;
try {
$shopSession->refreshToken();
} catch (\Exception $e) {}
// OAuth2Client has been cleared
$this->oauth2Client->delete();

$this->assertInstanceOf(RefreshTokenException::class, $e);
$token = $shopSession->getToken();
$this->assertEquals('', (string) $token->getJwt());
$this->assertEquals('', $token->getRefreshToken());
}
// Shop is linked
$this->linkShop->update(new \PrestaShop\Module\PsAccounts\Account\Dto\LinkShop([
'shopId' => 1,
'uid' => $this->faker->uuid,
'employeeId' => 5,
'ownerUid' => $this->faker->uuid,
'ownerEmail' => $this->faker->safeEmail,
]));

/**
* @test
*/
public function itShouldNotClearConfigurationIfShopIsUnlinked()
{
$newAccessToken = $this->makeJwtToken(new \DateTimeImmutable('tomorrow'));
$shopSession = $this->createMockedSession(new AccessToken([
'access_token' => (string)$newAccessToken
]), false, false);
$shopSession->cleanup();
$this->shopSession->setOauth2ClientReceiptTimeout(1);

$this->oauth2Client->update($this->faker->uuid, $this->faker->uuid);
sleep(2);

$token = $shopSession->refreshToken();
$this->shopSession->refreshToken();
} catch (RefreshTokenException $e) {
//$this->module->getLogger()->info($e->getMessage());
}

$this->assertEquals((string) $newAccessToken, (string) $shopSession->getToken()->getJwt());
$this->assertEquals(new Token($newAccessToken), $token);
$this->assertInstanceOf(RefreshTokenException::class, $e);
$this->assertEquals(1, preg_match('/Invalid OAuth2 client/', $e->getMessage()));
$token = $this->shopSession->getToken();
$this->assertEquals("", (string) $token->getJwt());
$this->assertEquals("", (string) $token->getRefreshToken());
}

/**
* @test
*/
public function itShouldRefreshToken()
public function itShouldNotClearConfigurationAndThrowIfNoOauth()
{
$newAccessToken = $this->makeJwtToken(new \DateTimeImmutable('tomorrow'));
$shopSession = $this->createMockedSession(new AccessToken([
'access_token' => (string)$newAccessToken
]), true, true);
$shopSession->cleanup();
$e = null;
try {
// Shop is linked
$this->linkShop->update(new \PrestaShop\Module\PsAccounts\Account\Dto\LinkShop([
'shopId' => 1,
'uid' => $this->faker->uuid,
]));

// OAuth2Client has been cleared
$this->oauth2Client->delete();

$this->oauth2Client->update($this->faker->uuid, $this->faker->uuid);
$this->shopSession->setOauth2ClientReceiptTimeout(1);

$token = $shopSession->refreshToken();
//sleep(2);

$this->assertEquals((string) $newAccessToken, (string) $shopSession->getToken()->getJwt());
$this->assertEquals(new Token($newAccessToken), $token);
$this->shopSession->refreshToken();
} catch (RefreshTokenException $e) {
//$this->module->getLogger()->info($e->getMessage());
}

$this->assertNull($e);
$token = $this->shopSession->getToken();
$this->assertEquals((string) $this->validAccessToken, (string) $token->getJwt());
$this->assertEquals("", (string) $token->getRefreshToken());
}

/**
* @param AccessToken|null $tokenResponse
* @param bool $oauth2ClientExists
* @param bool $linkShopExists
*
* @return ShopSession
* @test
*/
private function createMockedSession($tokenResponse, $oauth2ClientExists = true, $linkShopExists = true)
public function itShouldRefreshToken()
{
$shopProvider = $this->getMockBuilder(ShopProvider::class)
->disableOriginalConstructor()
->getMock();

$oauth2Client = $this->getMockBuilder(Oauth2Client::class)
->disableOriginalConstructor()
->getMock();

$linkShop = $this->getMockBuilder(LinkShop::class)
->disableOriginalConstructor()
->getMock();

$shopProvider->method('getAccessToken')->willReturn($tokenResponse);
$shopProvider->method('getOauth2Client')->willReturn($oauth2Client);
$oauth2Client->method('exists')->willReturn($oauth2ClientExists);
$linkShop->method('exists')->willReturn($linkShopExists);

return new ShopSession(
$this->configurationRepository,
$shopProvider,
$linkShop,
$this->commandBus
);
$e = null;
try {
// Shop is linked
$this->linkShop->update(new \PrestaShop\Module\PsAccounts\Account\Dto\LinkShop([
'shopId' => 1,
'uid' => $this->faker->uuid,
]));

// OAuth2Client exists
$this->oauth2Client->update(
$this->faker->uuid,
$this->faker->password
);

$this->shopSession->setOauth2ClientReceiptTimeout(1);

//sleep(2);

$token = $this->shopSession->refreshToken();
} catch (RefreshTokenException $e) {
//$this->module->getLogger()->info($e->getMessage());
}

$this->assertNull($e);
$this->assertEquals((string) $this->validAccessToken, (string) $token->getJwt());
$this->assertEquals("", (string) $token->getRefreshToken());
$this->assertEquals(new Token($this->validAccessToken), $token);
}
}
1 change: 1 addition & 0 deletions tests/phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
defaultTestSuite="all"
processIsolation="false"
>
<testsuites>
<testsuite name="all">
Expand Down
Loading