From 0020cd065fa6ecdf59e23554022263d5f0594c95 Mon Sep 17 00:00:00 2001
From: Grzegorz Rola <grola@seostudio.pl>
Date: Thu, 31 Jan 2019 17:11:58 +0100
Subject: [PATCH] Init

---
 composer.json                                 |   4 +-
 src/Authentication/JWTSaasToken.php           |  66 ++++++++
 src/Authentication/JWTToken.php               |  81 ++++++++++
 src/Authentication/NullToken.php              |  54 +++++++
 src/Authentication/Token.php                  |  32 ++++
 src/Client/ApiClientOptions.php               |  32 ++++
 src/Client/CachedClient.php                   |  10 +-
 src/Client/Client.php                         |   4 +-
 src/Client/ClientFactory.php                  |  11 +-
 src/Client/ClientImplementation.php           |  13 +-
 src/Client/RequestCacheInfoResolver.php       |   6 +-
 src/Response/AuthApiResponse.php              |  14 ++
 src/Response/Traits/ApiResponseDecorator.php  | 145 ++++++++++++++++++
 .../Traits/AuthApiResponseDecorator.php       |  38 +++++
 .../Traits/PagedListImplementation.php        |  44 ++++++
 .../Exception/CannotUnserializeException.php  |  13 ++
 src/Serializer/JsonSerializer.php             |  46 ++++++
 src/Serializer/Serializer.php                 |  23 +++
 src/Serializer/SerializerFactory.php          |  16 ++
 src/Serializer/SerializerOptions.php          |  11 ++
 .../unit/Authentication/TestJWTSaasToken.php  |  64 ++++++++
 tests/unit/Authentication/TestJWToken.php     |  33 ++++
 22 files changed, 735 insertions(+), 25 deletions(-)
 create mode 100644 src/Authentication/JWTSaasToken.php
 create mode 100644 src/Authentication/JWTToken.php
 create mode 100644 src/Authentication/NullToken.php
 create mode 100644 src/Authentication/Token.php
 create mode 100644 src/Client/ApiClientOptions.php
 create mode 100644 src/Response/AuthApiResponse.php
 create mode 100644 src/Response/Traits/ApiResponseDecorator.php
 create mode 100644 src/Response/Traits/AuthApiResponseDecorator.php
 create mode 100644 src/Response/Traits/PagedListImplementation.php
 create mode 100644 src/Serializer/Exception/CannotUnserializeException.php
 create mode 100644 src/Serializer/JsonSerializer.php
 create mode 100644 src/Serializer/Serializer.php
 create mode 100644 src/Serializer/SerializerFactory.php
 create mode 100644 src/Serializer/SerializerOptions.php
 create mode 100644 tests/unit/Authentication/TestJWTSaasToken.php
 create mode 100644 tests/unit/Authentication/TestJWToken.php

diff --git a/composer.json b/composer.json
index fc3057c..653e02b 100644
--- a/composer.json
+++ b/composer.json
@@ -9,8 +9,8 @@
   "require": {
     "php": ">=5.5",
     "psr/log": "^1.0.1",
-    "wpdesk/wp-cache": "dev-master",
-    "wpdesk/wp-http-client": "dev-master",
+    "wpdesk/wp-cache": "dev-feature/first-release",
+    "wpdesk/wp-http-client": "dev-feature/first-release",
     "psr/simple-cache": "^1.0"
   },
   "require-dev": {
diff --git a/src/Authentication/JWTSaasToken.php b/src/Authentication/JWTSaasToken.php
new file mode 100644
index 0000000..3465b21
--- /dev/null
+++ b/src/Authentication/JWTSaasToken.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace WPDesk\ApiClient\Authentication;
+
+class JWTSaasToken implements Token
+{
+    const SHOP_ID_PARAM = 'shop';
+
+    const ROLE_PARAM = 'ROLE_SHOP';
+
+
+    /** @var JWTToken */
+    private $token;
+
+    /**
+     * JWTToken constructor.
+     * @param string $token
+     */
+    public function __construct(JWTToken $token)
+    {
+        $this->token = $token;
+    }
+
+    public function getAuthString()
+    {
+        return $this->token->getAuthString();
+    }
+
+    public function isExpired()
+    {
+        return $this->token->isExpired();
+    }
+
+    public function isSignatureValid()
+    {
+        return $this->token->isSignatureValid();
+    }
+
+    public function __toString()
+    {
+        return $this->token->__toString();
+    }
+
+    /**
+     * If there is shop id in the token
+     *
+     * @return bool
+     */
+    public function hasShopId()
+    {
+        $info = $this->token->getDecodedPublicTokenInfo();
+        return !empty($info[self::SHOP_ID_PARAM]) && in_array(self::ROLE_PARAM, $info['roles']);
+    }
+
+    /**
+     * Get shop id from token
+     *
+     * @return int
+     */
+    public function getShopId()
+    {
+        $info = $this->token->getDecodedPublicTokenInfo();
+        return (int)$info[self::SHOP_ID_PARAM];
+    }
+
+}
\ No newline at end of file
diff --git a/src/Authentication/JWTToken.php b/src/Authentication/JWTToken.php
new file mode 100644
index 0000000..c6eaccf
--- /dev/null
+++ b/src/Authentication/JWTToken.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace WPDesk\ApiClient\Authentication;
+
+class JWTToken implements Token
+{
+    const CONSIDER_EXPIRED_WHEN_LESS = 2;
+
+    const EXPIRED_IN_SECONDS_PARAM = 'exp';
+
+    /** @var  string */
+    private $token;
+
+    /**
+     * JWTToken constructor.
+     * @param string $token
+     */
+    public function __construct($token)
+    {
+        $this->token = $token;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->token;
+    }
+
+    /**
+     * Get string to perform authentication
+     *
+     * @return string
+     */
+    public function getAuthString()
+    {
+        return 'Bearer ' . $this->__toString();
+    }
+
+    /**
+     * Returns public data from token
+     *
+     * @return array
+     */
+    public function getDecodedPublicTokenInfo()
+    {
+        $tokenParts = explode('.', $this->__toString());
+        if (!empty($tokenParts[1])) {
+            $infoPart = base64_decode($tokenParts[1]);
+            return json_decode($infoPart, true);
+        }
+        return [];
+    }
+
+    /**
+     * Is token expired or very soon to be expired?
+     *
+     * @return bool
+     */
+    public function isExpired()
+    {
+        $tokenInfo = $this->getDecodedPublicTokenInfo();
+        if (!empty($tokenInfo[self::EXPIRED_IN_SECONDS_PARAM])) {
+            return $tokenInfo[self::EXPIRED_IN_SECONDS_PARAM] - time() < self::CONSIDER_EXPIRED_WHEN_LESS;
+        }
+        return true;
+    }
+
+    /**
+     * Validates token signature
+     *
+     * @return bool
+     */
+    public function isSignatureValid()
+    {
+        // @TODO
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/src/Authentication/NullToken.php b/src/Authentication/NullToken.php
new file mode 100644
index 0000000..d46c595
--- /dev/null
+++ b/src/Authentication/NullToken.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace WPDesk\ApiClient\Authentication;
+
+/**
+ * Null object pattern
+ *
+ * @package WPDesk\SaasPlatformClient\Authentication
+ */
+class NullToken implements Token
+{
+    public function __construct()
+    {
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return '';
+    }
+
+    /**
+     * Get string to perform authentication
+     *
+     * @return string
+     */
+    public function getAuthString()
+    {
+        return '';
+    }
+
+    /**
+     * Is token expired or very soon to be expired?
+     *
+     * @return bool
+     */
+    public function isExpired()
+    {
+        return true;
+    }
+
+    /**
+     * Validates token signature
+     *
+     * @return bool
+     */
+    public function isSignatureValid()
+    {
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/src/Authentication/Token.php b/src/Authentication/Token.php
new file mode 100644
index 0000000..bc5a98e
--- /dev/null
+++ b/src/Authentication/Token.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace WPDesk\ApiClient\Authentication;
+
+interface Token
+{
+    /**
+     * Get string to perform authentication
+     *
+     * @return string
+     */
+    public function getAuthString();
+
+    /**
+     * Is token expired or very soon to be expired?
+     *
+     * @return bool
+     */
+    public function isExpired();
+
+    /**
+     * Validates token signature
+     *
+     * @return bool
+     */
+    public function isSignatureValid();
+
+    /**
+     * @return string
+     */
+    public function __toString();
+}
\ No newline at end of file
diff --git a/src/Client/ApiClientOptions.php b/src/Client/ApiClientOptions.php
new file mode 100644
index 0000000..808f292
--- /dev/null
+++ b/src/Client/ApiClientOptions.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace WPDesk\ApiClient\Client;
+
+use Psr\Log\LoggerInterface;
+use WPDesk\ApiClient\Serializer\SerializerOptions;
+use WPDesk\HttpClient\HttpClientOptions;
+
+interface ApiClientOptions extends HttpClientOptions, SerializerOptions
+{
+
+    /**
+     * @return LoggerInterface
+     */
+    public function getLogger();
+
+    /**
+     * @return string
+     */
+    public function getApiUrl();
+
+    /**
+     * @return array
+     */
+    public function getDefaultRequestHeaders();
+
+    /**
+     * @return bool
+     */
+    public function isCachedClient();
+
+}
\ No newline at end of file
diff --git a/src/Client/CachedClient.php b/src/Client/CachedClient.php
index 7d23f34..3c6ee45 100644
--- a/src/Client/CachedClient.php
+++ b/src/Client/CachedClient.php
@@ -1,12 +1,12 @@
 <?php
 
-namespace WPDesk\ApiClient\ApiClient;
+namespace WPDesk\ApiClient\Client;
 
 use Psr\SimpleCache\CacheInterface;
-use WPDesk\ApiClient\Cache\CacheDispatcher;
-use WPDesk\ApiClient\Cache\CacheItemCreator;
-use WPDesk\ApiClient\Cache\CacheItemVerifier;
-use WPDesk\ApiClient\HttpClient\HttpClient;
+use WPDesk\Cache\CacheDispatcher;
+use WPDesk\Cache\CacheItemCreator;
+use WPDesk\Cache\CacheItemVerifier;
+use WPDesk\HttpClient\HttpClient;
 use WPDesk\ApiClient\Request\Request;
 use WPDesk\ApiClient\Response\Response;
 use WPDesk\ApiClient\Serializer\Serializer;
diff --git a/src/Client/Client.php b/src/Client/Client.php
index e63e184..c3372f9 100644
--- a/src/Client/Client.php
+++ b/src/Client/Client.php
@@ -1,8 +1,8 @@
 <?php
 
-namespace WPDesk\ApiClient\ApiClient;
+namespace WPDesk\ApiClient\Client;
 
-use WPDesk\ApiClient\HttpClient\HttpClient;
+use WPDesk\HttpClient\HttpClient;
 use WPDesk\ApiClient\Request\Request;
 use WPDesk\ApiClient\Response\Response;
 use WPDesk\ApiClient\Serializer\Serializer;
diff --git a/src/Client/ClientFactory.php b/src/Client/ClientFactory.php
index b92f459..81ad8aa 100644
--- a/src/Client/ClientFactory.php
+++ b/src/Client/ClientFactory.php
@@ -1,19 +1,18 @@
 <?php
 
-namespace WPDesk\ApiClient\ApiClient;
+namespace WPDesk\ApiClient\Client;
 
-use WPDesk\ApiClient\Cache\WordpressCache;
-use WPDesk\ApiClient\HttpClient\HttpClientFactory;
-use WPDesk\ApiClient\PlatformFactoryOptions;
+use WPDesk\Cache\WordpressCache;
+use WPDesk\HttpClient\HttpClientFactory;
 use WPDesk\ApiClient\Serializer\SerializerFactory;
 
 class ClientFactory
 {
     /**
-     * @param PlatformFactoryOptions $options
+     * @param ApiClientOptions $options
      * @return Client
      */
-    public function createClient(PlatformFactoryOptions $options)
+    public function createClient(ApiClientOptions $options)
     {
         $httpClientFactory = new HttpClientFactory();
         $serializerFactory = new SerializerFactory();
diff --git a/src/Client/ClientImplementation.php b/src/Client/ClientImplementation.php
index 02e9c52..feaeb75 100644
--- a/src/Client/ClientImplementation.php
+++ b/src/Client/ClientImplementation.php
@@ -1,17 +1,16 @@
 <?php
 
-namespace WPDesk\ApiClient\ApiClient;
+namespace WPDesk\ApiClient\Client;
 
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use WPDesk\HttpClient\HttpClient;
 use WPDesk\HttpClient\HttpClientResponse;
-use WPDesk\SaasPlatformClient\Platform;
-use WPDesk\SaasPlatformClient\Request\Request;
-use WPDesk\SaasPlatformClient\Response\RawResponse;
-use WPDesk\SaasPlatformClient\Response\Response;
-use WPDesk\SaasPlatformClient\Serializer\Serializer;
-use WPDesk\SaasPlatformClient\HttpClient\HttpClientRequestException;
+use WPDesk\ApiClient\Request\Request;
+use WPDesk\ApiClient\Response\RawResponse;
+use WPDesk\ApiClient\Response\Response;
+use WPDesk\ApiClient\Serializer\Serializer;
+use WPDesk\HttpClient\HttpClientRequestException;
 
 class ClientImplementation implements Client, LoggerAwareInterface
 {
diff --git a/src/Client/RequestCacheInfoResolver.php b/src/Client/RequestCacheInfoResolver.php
index 65e2014..43a0b7e 100644
--- a/src/Client/RequestCacheInfoResolver.php
+++ b/src/Client/RequestCacheInfoResolver.php
@@ -1,9 +1,9 @@
 <?php
 
-namespace WPDesk\SaasPlatformClient\ApiClient;
+namespace WPDesk\ApiClient\Client;
 
-use WPDesk\SaasPlatformClient\Cache\CacheInfoResolver;
-use WPDesk\SaasPlatformClient\Cache\HowToCache;
+use WPDesk\Cache\CacheInfoResolver;
+use WPDesk\Cache\HowToCache;
 use WPDesk\SaasPlatformClient\Request\AuthRequest;
 use WPDesk\SaasPlatformClient\Request\BasicRequest;
 use WPDesk\SaasPlatformClient\Request\Request;
diff --git a/src/Response/AuthApiResponse.php b/src/Response/AuthApiResponse.php
new file mode 100644
index 0000000..86f34fa
--- /dev/null
+++ b/src/Response/AuthApiResponse.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace WPDesk\ApiClient\Response;
+
+use WPDesk\ApiClient\Response\Traits\AuthApiResponseDecorator;
+
+class AuthApiResponse implements ApiResponse
+{
+    const RESPONSE_CODE_BAD_CREDENTIALS = 401;
+
+    const RESPONSE_CODE_NOT_EXISTS = 404;
+
+    use AuthApiResponseDecorator;
+}
\ No newline at end of file
diff --git a/src/Response/Traits/ApiResponseDecorator.php b/src/Response/Traits/ApiResponseDecorator.php
new file mode 100644
index 0000000..0960c90
--- /dev/null
+++ b/src/Response/Traits/ApiResponseDecorator.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace WPDesk\ApiClient\Response\Traits;
+
+use WPDesk\ApiClient\Response\AuthApiResponse;
+use WPDesk\ApiClient\Response\RawResponse;
+use WPDesk\ApiClient\Response\Response;
+
+trait ApiResponseDecorator
+{
+
+    /** @var RawResponse */
+    private $rawResponse;
+
+    /**
+     * RawResponseDecorator constructor.
+     * @param Response $rawResponse
+     */
+    public function __construct(Response $rawResponse)
+    {
+        $this->rawResponse = $rawResponse;
+    }
+
+    /**
+     * Returns response http code
+     *
+     * @return int
+     */
+    public function getResponseCode()
+    {
+        return $this->rawResponse->getResponseCode();
+    }
+
+    /**
+     * Returns response body as array
+     *
+     * @return array
+     */
+    public function getResponseBody()
+    {
+        return $this->rawResponse->getResponseBody();
+    }
+
+    /**
+     * Returns response body as array
+     *
+     * @return array
+     */
+    public function getResponseErrorBody()
+    {
+        return $this->rawResponse->getResponseErrorBody();
+    }
+
+    /**
+     * Returns response body as array
+     *
+     * @return array
+     */
+    public function getResponseHeaders()
+    {
+        return $this->rawResponse->getResponseHeaders();
+    }
+
+    /**
+     * Get links structure to the other request
+     *
+     * @return array
+     */
+    public function getLinks()
+    {
+        $body = $this->getResponseBody();
+        return $body['_links'];
+    }
+
+    /**
+     * Is it a BAD REQUEST response
+     *
+     * @return bool
+     */
+    public function isBadRequest()
+    {
+        return $this->getResponseCode() === RawResponse::RESPONSE_CODE_ERROR_BAD_REQUEST;
+    }
+
+    /**
+     * Is it a DOMAIN NOT ALLOWED response
+     *
+     * @return bool
+     */
+    public function isDomainNotAllowed()
+    {
+        return $this->getResponseCode() === RawResponse::RESPONSE_CODE_DOMAIN_NOT_ALLOWED;
+    }
+
+    /**
+     * Is it a FATAL ERROR response
+     *
+     * @return bool
+     */
+    public function isServerFatalError()
+    {
+        return $this->getResponseCode() === RawResponse::RESPONSE_CODE_ERROR_FATAL;
+    }
+
+    /**
+     * Is any error occured
+     *
+     * @return bool
+     */
+    public function isError()
+    {
+        return $this->rawResponse->isError();
+    }
+
+    /**
+     * Is requested resource exists
+     *
+     * @return bool
+     */
+    public function isNotExists()
+    {
+        return $this->getResponseCode() === AuthApiResponse::RESPONSE_CODE_NOT_EXISTS;
+    }
+
+    /**
+     * Is maintenance.
+     *
+     * @return bool
+     */
+    public function isMaintenance()
+    {
+        return $this->rawResponse->isMaintenance();
+    }
+
+    /**
+     * Get platform version hash string.
+     *
+     * @return bool|string
+     */
+    public function getPlatformVersionHash()
+    {
+        return $this->rawResponse->getPlatformVersionHash();
+    }
+
+    }
\ No newline at end of file
diff --git a/src/Response/Traits/AuthApiResponseDecorator.php b/src/Response/Traits/AuthApiResponseDecorator.php
new file mode 100644
index 0000000..a052865
--- /dev/null
+++ b/src/Response/Traits/AuthApiResponseDecorator.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace WPDesk\ApiClient\Response\Traits;
+
+use WPDesk\ApiClient\Response\AuthApiResponse;
+
+trait AuthApiResponseDecorator
+{
+    use ApiResponseDecorator;
+
+    /**
+     * @return bool
+     */
+    public function isBadCredentials()
+    {
+        return $this->getResponseCode() === AuthApiResponse::RESPONSE_CODE_BAD_CREDENTIALS;
+    }
+
+    /**
+     * Is bad credential because token expires
+     *
+     * @return bool
+     */
+    public function isTokenExpired()
+    {
+        return $this->isBadCredentials();
+    }
+
+    /**
+     * Is bad credential because token is invalid
+     *
+     * @return bool
+     */
+    public function isTokenInvalid()
+    {
+        return $this->isBadCredentials();
+    }
+}
\ No newline at end of file
diff --git a/src/Response/Traits/PagedListImplementation.php b/src/Response/Traits/PagedListImplementation.php
new file mode 100644
index 0000000..2328647
--- /dev/null
+++ b/src/Response/Traits/PagedListImplementation.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace WPDesk\ApiClient\Response\Traits;
+
+trait PagedListImplementation
+{
+    /*
+     * @return array
+     */
+    public function getRawPage()
+    {
+        $body = $this->getResponseBody();
+        if ($body['_embedded'] !== null && $body['_embedded']['item'] !== null) {
+            return $body['_embedded']['item'];
+        }
+        return [];
+    }
+
+    /**
+     * @return int
+     */
+    public function getPageCount()
+    {
+        return (int)floor($this->getItemCount() / $this->getItemsPerPage());
+    }
+
+    /**
+     * @return int
+     */
+    public function getItemsPerPage()
+    {
+        $body = $this->getResponseBody();
+        return (int)$body['itemsPerPage'];
+    }
+
+    /**
+     * @return int
+     */
+    public function getItemCount()
+    {
+        $body = $this->getResponseBody();
+        return (int)$body['totalItems'];
+    }
+}
\ No newline at end of file
diff --git a/src/Serializer/Exception/CannotUnserializeException.php b/src/Serializer/Exception/CannotUnserializeException.php
new file mode 100644
index 0000000..0d60348
--- /dev/null
+++ b/src/Serializer/Exception/CannotUnserializeException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace WPDesk\ApiClient\Serializer\Exception;
+
+/**
+ * Thrown when serializer cannot unserialize string data
+ *
+ * @package WPDesk\ApiClient\Serializer\Exception
+ */
+class CannotUnserializeException extends \RuntimeException
+{
+
+}
\ No newline at end of file
diff --git a/src/Serializer/JsonSerializer.php b/src/Serializer/JsonSerializer.php
new file mode 100644
index 0000000..55fa230
--- /dev/null
+++ b/src/Serializer/JsonSerializer.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace WPDesk\ApiClient\Serializer;
+
+use WPDesk\ApiClient\Serializer\Exception\CannotUnserializeException;
+
+class JsonSerializer implements Serializer
+{
+
+    /**
+     * Convert data to string
+     *
+     * @param mixed $data
+     * @return string
+     */
+    public function serialize($data)
+    {
+        return json_encode($data, JSON_FORCE_OBJECT);
+    }
+
+    /**
+     * Convert string to php data
+     *
+     * @param string $data
+     * @return mixed
+     */
+    public function unserialize($data)
+    {
+        $unserializedResult = json_decode($data, true);
+        if ($unserializedResult === null) {
+            throw new CannotUnserializeException("Cannot unserialize data: {$data}");
+        }
+
+        return $unserializedResult;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMime()
+    {
+        return 'application/json';
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/Serializer/Serializer.php b/src/Serializer/Serializer.php
new file mode 100644
index 0000000..1faa8f9
--- /dev/null
+++ b/src/Serializer/Serializer.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace WPDesk\ApiClient\Serializer;
+
+interface Serializer
+{
+    /**
+     * @param mixed $data
+     * @return string
+     */
+    public function serialize($data);
+
+    /**
+     * @param string $data
+     * @return mixed
+     */
+    public function unserialize($data);
+
+    /**
+     * @return string
+     */
+    public function getMime();
+}
\ No newline at end of file
diff --git a/src/Serializer/SerializerFactory.php b/src/Serializer/SerializerFactory.php
new file mode 100644
index 0000000..532099e
--- /dev/null
+++ b/src/Serializer/SerializerFactory.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace WPDesk\ApiClient\Serializer;
+
+class SerializerFactory
+{
+    /**
+     * @param SerializerOptions $options
+     * @return Serializer
+     */
+    public function createSerializer(SerializerOptions $options)
+    {
+        $className = $options->getSerializerClass();
+        return new $className;
+    }
+}
\ No newline at end of file
diff --git a/src/Serializer/SerializerOptions.php b/src/Serializer/SerializerOptions.php
new file mode 100644
index 0000000..d1441bc
--- /dev/null
+++ b/src/Serializer/SerializerOptions.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace WPDesk\ApiClient\Serializer;
+
+interface SerializerOptions
+{
+    /**
+     * @return string
+     */
+    public function getSerializerClass();
+}
\ No newline at end of file
diff --git a/tests/unit/Authentication/TestJWTSaasToken.php b/tests/unit/Authentication/TestJWTSaasToken.php
new file mode 100644
index 0000000..526253d
--- /dev/null
+++ b/tests/unit/Authentication/TestJWTSaasToken.php
@@ -0,0 +1,64 @@
+<?php
+
+use WPDesk\ApiClient\Authentication\JWTSaasToken;
+use WPDesk\ApiClient\Authentication\JWTToken;
+
+class TestJWTSaasToken extends \PHPUnit\Framework\TestCase
+{
+    /**
+     * Provides tokens with info about expected shop id encoded inside
+     *
+     * @return array
+     */
+    public function provideTokensWithShopInfo()
+    {
+        return [
+            [
+                'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDA2NjQsImV4cCI6MTUzNDk0NDI2NCwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjoxfQ.rG6_U67zKTinkqR4324wOhS0YP9P9_2DH-OMsfijTPt408wNGwh5YKqkA4kglP5wMQQ80UGo7qyYd9R3fc465JMB9DQTbALATz_UMk8fr_-LWl9Gj8cuzQvEMQL2Saya_nggAn0bmWlVG27k6-6ezfBW7Hhb1d1GtoydbiYMA_ntLVPzrRjicvcIoftroadX6rsuCSHy-lLvHd1pj6E6eOEX2IcZXoa9eqd_3-j4dgszNUoPDag8bYXQzmIDIqyseBQ3eaQr69Tj-npTaMDjSyHwFQqtr57leX9aVVo8xPeF2ulrT4u4VgCv6kQt1Fao5_G6WLJ9FpJE19k_e4uebSwmDwiPoSnqU30tmIoVpcqfzxoNDKxfaK6am4HvfiQ6l_A2Cb7Wi9Wh5Px1A2a1NjFNKq-hxI3bVb30cKvacL29tYcjT-315RKv57onnCjVJWaGfzPJ44ZibB2VaVF9HO8Rjci8PJ35lF0NOw7YYb7rTQB0JOsLhHrwe9cjgKBsxwIAOVx1fFt6fq1RRJzUpcHKAx0f7kwGmHzXNwpNIexneBYg1Btz5UfW3yiztxggBPYvDfFAdLc5hfFURFhLtqaQzCklomRYBkbEkBGSt_5iz7HLYiiWuM51TzXgUMa631KwCuyBA675fwEMnwQ01YM51Et7NQgRcja737kSliM',
+                'shop' => 1
+            ],
+            [
+                'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDEyMDAsImV4cCI6MTUzNDk0NDgwMCwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjo1fQ.NAh6p40uSgyhcRkXxLSJhsGSn-mRAIYGsbtuMqSrplHk7LxrOQvvLBKfW0cZpl4y683ECGYqykJYmGb3nnWPKOQv5Pl2GExLItvzWuVju6KvkTi-_RD1xVXv6tCxkI6umcrJaI0bUWQhjLogrjNgIAR81DRbfG81lctIRzhP-CtADD9J7uEFrfWIGIbBgVOMdiw4nsAvqB9ECTDbnv5TRsxmOnyhm42mWxfpbrzd7WaRugIMWbysL2_5HXwqLV7jaaPlBHFpGs3cpgpRF0fsZ1VZeM7LAiiwvNuBwVsZbmD_ZKKdXzwL183RbsuSP4C_7gjH4UXEflRWRR6nzom_AyCFhqGrdu1obSNLiIeIt94p_VavOmwicwQKmuhOHGnYHVguTLYW2FcQIS2nMosswnXoZzTJcL3YwQQsMT6Xg-Da43bWZMIWvcK7d-nSvOGHW0OrHzWQRZOcH3RKANzPUEG5L0-KL55yacMbAPq6ykGc-I-_q1IDBxfxE2rSaxGKa72O60uwjX2Mafdhp9YWuESNm2Gvi09hYo3_dBUAHjaSdXOWqJhb8h1ND8c2IJNxltTIukz1fDdLkFvTttZi4g5rjsJoDN94Y3f-19qokY8lzhx8z3Zc4hgbNQ9p8CYrmjB6FDa59Psuxbd-ODwzNm_uLsn1z4U5uo68QHa5g50',
+                'shop' => 5
+            ],
+            [ // case with wpdesk and shop role
+                'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDE0MzYsImV4cCI6MTUzNDk0NTAzNiwicm9sZXMiOlsiUk9MRV9XUERFU0siLCJST0xFX1NIT1AiXSwidXNlcm5hbWUiOjEsImlwIjoiMTcyLjE5LjAuMSIsInNob3AiOjJ9.pD9qQQJB_Mn6hSBcK5unEyrQ2n-3BgnzCRtk1V_u7BL0SKqgqILQN5NGJYpG2dO1Rb54pCNgAVzr6axu9f6ijmGetpHX_V1HSy7HOM4xdI8H7BaPHaK-2KUKTrVFM7lEp_sRc4KBkfSW4Ju2EH0e0Oty42EkIWeTb3J3npTHK_RjXI47xX5e_gZW8inRfJx24EhiSnlkMX77lIpXsqqCyRKqW64niLrQzKMNeSOJ_5HgBvzk9OyR9H5O8_s8EAliD24LvynoxgQOp9r0A78EIBRoqdZKJSS7kSgl_8eRFTGgxZBPGmyBKNWvxrd2XnRp19QPgymMi2kR0HJDWRemtSFHxYsjTrVVnpeiHjZ7WTKNawZPB-7ELPIHm_C_-GuJ4I1fnnM_uUna2rh6mr3KeRQF4LJGmDjesdHIpy8FDmepqsIcAAxpmAi6kJRtDr6QAxoh20zi0jCni1pPmLQyoDsau7U7l3Nev_STFbFiHBk-mLYbhNqV_MCcFY1-GeWynfehehWoVeGTShVoxXrDSWklPb58DaQKuwXJ8-kvmvWVhVqWGqF5l7qUj3D2CIxr4tXsQ6FUPkntvFw3S4GrLqAl-P7XoR5alRxSyC6KypYeY7pHRZBDDeoFWgCOJhEymWtKrcTt-Yzd2EL3vQYnwhYOp0hw1n-eXnE1WkaB4Wg',
+                'shop' => 2
+            ]
+        ];
+    }
+
+    /**
+     * Provides tokens when can't get info about shop
+     *
+     * @return array
+     */
+    public function provideTokensWithInvalidShopInfo()
+    {
+        return [
+            ['invalid' => 'siUk9MRV9XUERFU0siLCJST0xFX1NIT1AiXSwidXNlcm5hbWUiOjEsImlwIjoiMTcyLjE5LjAuMSIsInNob3AiOjJ9.qeroBbMHVgQWypDaF6FPmaZadu7D1u1JObCn9rXy44pGL1mWESdVCI6pU_MajeCJsQ-V3emWMOfqHl5AIiH7BZb2iW04ahGJjJTtRGaEfGH5ztyG-00TVzf51V0mtX3i3DuEEJd8F6LDHK8C51oqm6WkHVv__GhsqOlMz_WTDdst-IRUK-kgcUBhbYy-XzVxt4LADQhs4SsJqd-0wYPaobTFRQaHel69oGF_ymwJ4snla4uJxlfIHLtmYRArOIcDNKcPIOH8cMo-UJP9B15IxuOGO0M2hR74LNSmDUpIHmeQ24rEiwp73q2yhXYIHhIBdExDk7GAHFhOILIqa05D48V8fkPLr0TFVVCACvylJHwj3UsYPzI1WvhdrEHcGr1MC8bWB2KAC_RpB5Vvo7kPp0YXcUa7nbszL90v1njKpVrcSrpYG69a4Ym8ZXSdAS7jJmoSH-Xf3JXXi_6vtxtSsm-DG_zwd7VoWupBu1rG1h_ARW-Lambj_Ql2XZEAWAvdR6LqHkL5SyIZ7CnscbNY_VbYoyRHsQIwhzbaQdBSwBa4qu0VsEHtwdV2acbDZhojm-DMb4b7vr3znyw_WnHPSvjOGRXmW2KhD0bwwfrAJcquc0rL5N04FRVYXR7AnJjN-j1j0bfhvDN9a5W-yHidmp9_OZ0HVCqK5c0IIsiv-Ms'],
+            ['no shop id' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDE5MjMsImV4cCI6MTUzNDk0NTUyMywicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEifQ.S8Dkz9C7d0zHRn5_uRzjdBHH437JJZ4CdHTRc-Bwn1bnP9rj0SGn3AbCA53TlZjBl2EX0cigRlEMYsKEr0eQdQK8dellP0EjtPSaX5_5nEuU_H0X_6oJT8wYiS9N2jVm6W3UiKHMJBCR9JQfhjHYmMRx-NKhAi5xUgiGIWC7IWhNvLzHu52sRozkG2jTssnHGid087FdnQ1aG9c4dYY9piSw-5xBf8FruP9sDoWhTvFzESHIlwzgL5MNHQx-9HUH6caWoblpas959YqbD29h49YuCHLbifR1sMjSUWnkw69WLGDAFs8aKPCR5FsfKg3Rac0s957Cl3ES60eL7kWJ2mk1kRjxpFa6me1pWvh7Pt6lpDsJPLvTIbcZ_aDX5taOOiucL8YO-PbCB37Ffi2L4Am0-b4EGrqarBprjUWe2CJA9tCfjjFm0McOotDNVjLDmU7wAdHv8hXEOq4Kr2wd1z7suRqZii3VjMotoWFz9Cq4BTu1cs_Z_OLq5aC_uhnWq-SsKx3pdfL5tDXZ_CPqDCW0_3692FT8Z_puk1UUtMI_I_GkY_gp-npH72D8VcYdI-0j4LJoEtviGk6-0Lk47CiygGw79TaFjcEhwxPoYD_7MwMEw7vJiYYO8PVptlSuQy7j2-cPI0XPHlu19tVzA97ilqOlRAxt_SihXMfSoyo'],
+            ['no shop role' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIwNjEsImV4cCI6MTUzNDk0NTY2MSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjo1fQ.PS5SQixw9k9_-Waxho4GKJ1QudCSzee6gy9_cFjWnVcBpMGH-PkHf5iDO5rWwqQSsKpBgMi5m-FFCYoelHa5q1-XAn6FkRlj0aXNcQZCY6YGfqIaDeCP-0DEVoSqfaceFXLZbTyPz4aKrurONjWdZwuw9uull6Ho4Nq1B_E5QTpJBYehmebgWnS5OjHDjJVW0UrknwKHiRSohP-vCA5P2HQGZpZXrUCkLFcL2PqCCchORuHUQr3xjn2OleE4Jt1RsD5Lymo3m8s6WkRw9GepWV7-vJ0WDh3Mu_irkstZlCyujfh0PNnRSEyr9jshW2OXnDX_l0dLzsd5n6pPX1OVa58FpkZNb3s7hAsVxQ2Dd9GUg2o-qMZDCP9LbXn_CGUqiyXo4iIdal4balF0DpUGNEfggwqnL4leTsHqUirsN1bhLLhRmkAYooCfFuEMA9F5G66zF8Vwl5AuZSz8Nrj-T0krM54H5ykhzVttpWyRA9TcVywKAC9E2DFDOMfajtJA_LcMv5xg-m0uhhxlGk25HAY8yVWQdPfBp5KIq2jO3vLPzVmBDPFjtDgwiUXvXVakFS7AmtI8DRWh8dq92F2dGDV67uZ_WK5kqBQEtO1abfM5EF0zcQ9r_E4KqjLJfXyCyLMucw8CUdsE8MEcCfjpE06_mSuRMW1dpoSPJWGXE5Y'],
+            ['no whole shop info' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIwMjksImV4cCI6MTUzNDk0NTYyOSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEifQ.XTvcWiKWLda4TnHhWnqrOKCBseiHmjnDYgmPCKhX6NrEYMu4mN0puKgnoeOWJVZHBjFQ5X--khMFKNKU4e7Zgc6MWgnSc5niKyZpr73D-PVfUkOG81we2-DSteik2i3F2R2cTSIUbYMJKeqe7fQ0qTZ3-7s6kNgKAkIhGraQrbbxi-FTp4uNbUrDwcDEyBZ_T9zmcR9jXsKLVJszHdmc8K_eVYwj2jOR8bouGOqpCHwlGz0bEpcSPqXJ_DsotVeM91QIBls4JzImTFbYr06N1GdG4oFzMNWqVhO9iJ4-ElN_COPLp1ou0i1Ha0ZedYajI2CJrhailbF5ybZd5x08tiDoMa26aVpH09wOe_P4zYno0ksMnZ66WpPcnBgnPA_dlgcqUUkc93igicD8XCKEBokEyF16IzI3A8Y85FEuwkIj-JadM8ZTJqHErxNbhZyuEGqWvQfwhawade-3CxOUz3krbG8LxhNPBYrm2t8NQpRwwFoQPa0lvEdMiGo4JL0J7hW02shT2iGPOUwJ0_KfpXCU5ensd60Mou0GQhnUPhKvnA0CtvQn5JgX_TM5OKKCSvtcIEaW2pqq96--S8atnQp2GwZU0Ccs8R6HAfH61d8cwL8abwKvr6A5wQoskn28YPwWu8g9Tvt556i2oIvFWEjrFOnfQyhmQj1GCVMafFc'],
+        ];
+    }
+
+    /**
+     * @dataProvider provideTokensWithShopInfo
+     */
+    public function testValidShopInfoFromToken($token, $shop)
+    {
+        $saasToken = new JWTSaasToken(new JWTToken($token));
+        $this->assertTrue($saasToken->hasShopId(), 'Token should have shop id info');
+        $this->assertEquals($shop, $saasToken->getShopId(), 'Invalid shop id provided in token');
+    }
+
+    /**
+     * @dataProvider provideTokensWithInvalidShopInfo
+     */
+    public function testTokensWithInvalidShopInfo($token)
+    {
+        $saasToken = new JWTSaasToken(new JWTToken($token));
+        $this->assertFalse($saasToken->hasShopId(), 'Token should NOT have shop id info');
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/Authentication/TestJWToken.php b/tests/unit/Authentication/TestJWToken.php
new file mode 100644
index 0000000..492f115
--- /dev/null
+++ b/tests/unit/Authentication/TestJWToken.php
@@ -0,0 +1,33 @@
+<?php
+
+use WPDesk\ApiClient\Authentication\JWTToken;
+
+class TestJWToken extends \PHPUnit\Framework\TestCase
+{
+
+    public function testExpiredToken()
+    {
+        $expiredToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ4NTc2OTEsImV4cCI6MTUzNDg2MTI5MSwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjoxfQ.iB9u8f3uBdq1KhkkYPAVpPFSwvkK4CQypfFQIiM8N5acQSFxv-jyzD2guGs4HtLOdMvu4Dt5zkd4ZfQFJNT2b6k7i33FyU4AsEDAVtwHL-TcqsnomXn70CjB5Rhgd2LteFl6wPp3XonE4SZ6Oo3vtBZzSoNNkR6-7T3OseJMwoJ7qzlEFBixNXG6UTlXPJky_b-rbfhFORlInxVzvs4GJgkJM3F3Ugy4bLSSPtBYxWcwKnKkFE8L4Z87Pezp4v35aXqJFpvJ3zll0gJ1F32Z2vx1oCmu0jOHkzzmu3wA2u6gK0iNgp591M7MKH2_3HaLfFY06cLoLDN6TR6wmzSvZYmzSw8C68MUH7uGWyGRU9j4YtdL8Bom3v8D1J-IC4Jx6-QPE665nd1VgzHZb1TFkHseUx3kLF5Jhgq7095NJ79QTC-6XTW2bN-T-dbbFkvjCU-B-9Ti09uMUEn4Rtlt_lThbr_lA9Wyc4qXAecqCz6dAC2UTy-_KxLwvfNrQ4sSS4y3B8bPh8qJrI0EsIbi5nY20sgBT71abUG9DHmIzj4rA5YW_DOu7Cez0WhNoCHjhMEymATrD2cWYxFCPWQfTTBuoD1HZgVfwYI9B-aZPw669orNRGkBtP8Bm5ghmCZoRHpISiY7UoIzgOnVKhB-Qv43g1uRprk7nYnfVEglqXg';
+
+        $jwtToken = new JWTToken($expiredToken);
+        $this->assertTrue($jwtToken->isSignatureValid(), 'Signature should be valid');
+        $this->assertTrue($jwtToken->isExpired(), 'Token should be expired long ago');
+    }
+
+    public function testImmortalToken()
+    {
+        $tokenWithExpiredDateInFuture = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIyODgsImV4cCI6MzI1MDM2ODAwMDAsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfU0hPUCJdLCJ1c2VybmFtZSI6MywiaXAiOiIxNzIuMTkuMC4xIiwic2hvcCI6NX0.gWsHsCGm8iUc13lGc9PgUtwv-qIYWAl98-jdzOdzuQ_PwreLuEFonP-hutp0_IXZtHeE6e1XLID-Cgn0YaahLrxybVGBTbcVgTJVOMB9Fv-gP9lHYNqvBdfQRcdXiO6PYCBbfpezunhcmiML_ebFfprdoMn8kG3K-XbEkyRB7MOQg0-dZ35tncVCZDfzLh5fucFzteQmCcddIosKpqr-rjYjRRCAB-aTE1vbAulZa1_VmP17l6m64__QBjHW9r07OTq7QviayXpOB_4mBdxI26XjgDXANCzlejcka5Uh7AS03cP3zDP8Mc0VaVZxqUhwotQ93s0VtyMNyUn_nkMebAuwXkDOmgsQzJHhlPD8hs9vzmNbN6ymKwsSZ-E3q1yPTQwXzu-rERdCaNifAIKlUyTETlvE3aoN7M0JjdCIVHBkf81ygppMITYJ0eytjyqBg0wjdRlMCpHEtGVosqLe8sNZo7NeZ9tPexhZxWXQNRA6VyE1NwDYd9yPsKBSPrulFcvkgoGOuL98rfE9iizMDYD60G0eTgafnR_99-IH4yoBMIjf4UNVzAcw8revbSE-3bgmxpdjVTJtVXw9LH13BHdB6GN7KJDwH4NRoz_MQJMSBELU18zY2yjmkNLXYI3YArTE3OQ-qBzD7rgPrcJBmoWTswbNVnTl2csFVHdQZOo';
+
+        $jwtToken = new JWTToken($tokenWithExpiredDateInFuture);
+        $this->assertTrue($jwtToken->isSignatureValid(), 'Signature should be valid');
+        $this->assertFalse($jwtToken->isExpired(), 'Token should NOT be expired. Have 3000 years of live');
+    }
+
+    public function testStringValueAndBearer() {
+        $someToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIyODgsImV4cCI6MzI1MDM2ODAwMDAsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfU0hPUCJdLCJ1c2VybmFtZSI6MywiaXAiOiIxNzIuMTkuMC4xIiwic2hvcCI6NX0.gWsHsCGm8iUc13lGc9PgUtwv-qIYWAl98-jdzOdzuQ_PwreLuEFonP-hutp0_IXZtHeE6e1XLID-Cgn0YaahLrxybVGBTbcVgTJVOMB9Fv-gP9lHYNqvBdfQRcdXiO6PYCBbfpezunhcmiML_ebFfprdoMn8kG3K-XbEkyRB7MOQg0-dZ35tncVCZDfzLh5fucFzteQmCcddIosKpqr-rjYjRRCAB-aTE1vbAulZa1_VmP17l6m64__QBjHW9r07OTq7QviayXpOB_4mBdxI26XjgDXANCzlejcka5Uh7AS03cP3zDP8Mc0VaVZxqUhwotQ93s0VtyMNyUn_nkMebAuwXkDOmgsQzJHhlPD8hs9vzmNbN6ymKwsSZ-E3q1yPTQwXzu-rERdCaNifAIKlUyTETlvE3aoN7M0JjdCIVHBkf81ygppMITYJ0eytjyqBg0wjdRlMCpHEtGVosqLe8sNZo7NeZ9tPexhZxWXQNRA6VyE1NwDYd9yPsKBSPrulFcvkgoGOuL98rfE9iizMDYD60G0eTgafnR_99-IH4yoBMIjf4UNVzAcw8revbSE-3bgmxpdjVTJtVXw9LH13BHdB6GN7KJDwH4NRoz_MQJMSBELU18zY2yjmkNLXYI3YArTE3OQ-qBzD7rgPrcJBmoWTswbNVnTl2csFVHdQZOo';
+
+        $jwtToken = new JWTToken($someToken);
+        $this->assertEquals($jwtToken->__toString(), $someToken);
+        $this->assertEquals($jwtToken->getAuthString(), 'Bearer ' . $someToken);
+    }
+}
\ No newline at end of file
-- 
GitLab