Repository: jpadilla/pyjwt
Pull request #563 (2020-12-19 21:14:41)

Add utility functions to assist test skipping


Patch:
diff --git a/jwt/__init__.py b/jwt/__init__.py
index 9d95053..55a5ae3 100644
--- a/jwt/__init__.py
+++ b/jwt/__init__.py
@@ -1,12 +1,10 @@
-from .api_jws import PyJWS
-from .api_jwt import (
-    PyJWT,
-    decode,
-    encode,
+from .api_jws import (
+    PyJWS,
     get_unverified_header,
     register_algorithm,
     unregister_algorithm,
 )
+from .api_jwt import PyJWT, decode, encode
 from .exceptions import (
     DecodeError,
     ExpiredSignatureError,
diff --git a/jwt/api_jws.py b/jwt/api_jws.py
index 17c49db..152c237 100644
--- a/jwt/api_jws.py
+++ b/jwt/api_jws.py
@@ -1,7 +1,7 @@
 import binascii
 import json
 from collections.abc import Mapping
-from typing import Dict, List, Optional, Type, Union
+from typing import Any, Dict, List, Optional, Type
 
 from .algorithms import (
     Algorithm,
@@ -77,7 +77,7 @@ class PyJWS:
 
     def encode(
         self,
-        payload: Union[Dict, bytes],
+        payload: bytes,
         key: str,
         algorithm: str = "HS256",
         headers: Optional[Dict] = None,
@@ -127,15 +127,14 @@ class PyJWS:
 
         return encoded_string.decode("utf-8")
 
-    def decode(
+    def decode_complete(
         self,
         jwt: str,
         key: str = "",
         algorithms: List[str] = None,
         options: Dict = None,
-        complete: bool = False,
         **kwargs,
-    ):
+    ) -> Dict[str, Any]:
         if options is None:
             options = {}
         merged_options = {**self.options, **options}
@@ -150,17 +149,25 @@ class PyJWS:
 
         if verify_signature:
             self._verify_signature(
-                payload, signing_input, header, signature, key, algorithms
+                signing_input, header, signature, key, algorithms
             )
 
-        if complete:
-            return {
-                "payload": payload,
-                "header": header,
-                "signature": signature,
-            }
+        return {
+            "payload": payload,
+            "header": header,
+            "signature": signature,
+        }
 
-        return payload
+    def decode(
+        self,
+        jwt: str,
+        key: str = "",
+        algorithms: List[str] = None,
+        options: Dict = None,
+        **kwargs,
+    ) -> str:
+        decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
+        return decoded["payload"]
 
     def get_unverified_header(self, jwt):
         """Returns back the JWT header parameters as a dict()
@@ -213,7 +220,6 @@ class PyJWS:
 
     def _verify_signature(
         self,
-        payload,
         signing_input,
         header,
         signature,
@@ -249,6 +255,7 @@ class PyJWS:
 
 _jws_global_obj = PyJWS()
 encode = _jws_global_obj.encode
+decode_complete = _jws_global_obj.decode_complete
 decode = _jws_global_obj.decode
 register_algorithm = _jws_global_obj.register_algorithm
 unregister_algorithm = _jws_global_obj.unregister_algorithm
diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py
index 06c89f4..da273c5 100644
--- a/jwt/api_jwt.py
+++ b/jwt/api_jwt.py
@@ -4,7 +4,7 @@ from collections.abc import Iterable, Mapping
 from datetime import datetime, timedelta
 from typing import Any, Dict, List, Optional, Type, Union
 
-from .api_jws import PyJWS
+from . import api_jws
 from .exceptions import (
     DecodeError,
     ExpiredSignatureError,
@@ -16,8 +16,11 @@ from .exceptions import (
 )
 
 
-class PyJWT(PyJWS):
-    header_type = "JWT"
+class PyJWT:
+    def __init__(self, options=None):
+        if options is None:
+            options = {}
+        self.options = {**self._get_default_options(), **options}
 
     @staticmethod
     def _get_default_options() -> Dict[str, Union[bool, List[str]]]:
@@ -33,7 +36,7 @@ class PyJWT(PyJWS):
 
     def encode(
         self,
-        payload: Union[Dict, bytes],
+        payload: Dict[str, Any],
         key: str,
         algorithm: str = "HS256",
         headers: Optional[Dict] = None,
@@ -59,20 +62,18 @@ class PyJWT(PyJWS):
             payload, separators=(",", ":"), cls=json_encoder
         ).encode("utf-8")
 
-        return super().encode(
+        return api_jws.encode(
             json_payload, key, algorithm, headers, json_encoder
         )
 
-    def decode(
+    def decode_complete(
         self,
         jwt: str,
         key: str = "",
         algorithms: List[str] = None,
         options: Dict = None,
-        complete: bool = False,
         **kwargs,
     ) -> Dict[str, Any]:
-
         if options is None:
             options = {"verify_signature": True}
         else:
@@ -83,20 +84,16 @@ class PyJWT(PyJWS):
                 'It is required that you pass in a value for the "algorithms" argument when calling decode().'
             )
 
-        decoded = super().decode(
+        decoded = api_jws.decode_complete(
             jwt,
             key=key,
             algorithms=algorithms,
             options=options,
-            complete=complete,
             **kwargs,
         )
 
         try:
-            if complete:
-                payload = json.loads(decoded["payload"])
-            else:
-                payload = json.loads(decoded)
+            payload = json.loads(decoded["payload"])
         except ValueError as e:
             raise DecodeError("Invalid payload string: %s" % e)
         if not isinstance(payload, dict):
@@ -106,11 +103,19 @@ class PyJWT(PyJWS):
             merged_options = {**self.options, **options}
             self._validate_claims(payload, merged_options, **kwargs)
 
-        if complete:
-            decoded["payload"] = payload
-            return decoded
+        decoded["payload"] = payload
+        return decoded
 
-        return payload
+    def decode(
+        self,
+        jwt: str,
+        key: str = "",
+        algorithms: List[str] = None,
+        options: Dict = None,
+        **kwargs,
+    ) -> Dict[str, Any]:
+        decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
+        return decoded["payload"]
 
     def _validate_claims(
         self, payload, options, audience=None, issuer=None, leeway=0, **kwargs
@@ -215,7 +220,5 @@ class PyJWT(PyJWS):
 
 _jwt_global_obj = PyJWT()
 encode = _jwt_global_obj.encode
+decode_complete = _jwt_global_obj.decode_complete
 decode = _jwt_global_obj.decode
-register_algorithm = _jwt_global_obj.register_algorithm
-unregister_algorithm = _jwt_global_obj.unregister_algorithm
-get_unverified_header = _jwt_global_obj.get_unverified_header
diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py
index 707724e..ecd10cb 100644
--- a/jwt/jwks_client.py
+++ b/jwt/jwks_client.py
@@ -2,7 +2,7 @@ import json
 import urllib.request
 
 from .api_jwk import PyJWKSet
-from .api_jwt import decode as decode_token
+from .api_jwt import decode_complete as decode_token
 from .exceptions import PyJWKClientError
 
 
@@ -50,8 +50,6 @@ class PyJWKClient:
         return signing_key
 
     def get_signing_key_from_jwt(self, token):
-        unverified = decode_token(
-            token, complete=True, options={"verify_signature": False}
-        )
+        unverified = decode_token(token, options={"verify_signature": False})
         header = unverified["header"]
         return self.get_signing_key(header.get("kid"))
diff --git a/jwt/utils.py b/jwt/utils.py
index 495c8c7..f9579d3 100644
--- a/jwt/utils.py
+++ b/jwt/utils.py
@@ -32,7 +32,7 @@ def base64url_decode(input):
     return base64.urlsafe_b64decode(input)
 
 
-def base64url_encode(input):
+def base64url_encode(input: bytes) -> bytes:
     return base64.urlsafe_b64encode(input).replace(b"=", b"")
 
 


Test patch:
diff --git a/tests/keys/__init__.py b/tests/keys/__init__.py
index 050f132..3c11e98 100644
--- a/tests/keys/__init__.py
+++ b/tests/keys/__init__.py
@@ -1,6 +1,7 @@
 import json
 import os
 
+from jwt.algorithms import has_crypto
 from jwt.utils import base64url_decode
 
 BASE_PATH = os.path.dirname(os.path.abspath(__file__))
@@ -22,10 +23,8 @@ try:
     from cryptography.hazmat.primitives.asymmetric import ec
 
     from jwt.algorithms import RSAAlgorithm
-
-    has_crypto = True
 except ImportError:
-    has_crypto = False
+    pass
 
 if has_crypto:
 
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
index d88687f..02c3241 100644
--- a/tests/test_algorithms.py
+++ b/tests/test_algorithms.py
@@ -8,7 +8,7 @@ from jwt.exceptions import InvalidKeyError
 from jwt.utils import base64url_decode
 
 from .keys import load_hmac_key
-from .utils import key_path
+from .utils import crypto_required, key_path
 
 try:
     from jwt.algorithms import (
@@ -19,10 +19,8 @@ try:
     )
 
     from .keys import load_ec_pub_key_p_521, load_rsa_pub_key
-
-    has_crypto = True
 except ImportError:
-    has_crypto = False
+    pass
 
 
 class TestAlgorithms:
@@ -133,45 +131,35 @@ class TestAlgorithms:
             with pytest.raises(InvalidKeyError):
                 algo.from_jwk(keyfile.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_should_parse_pem_public_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
         with open(key_path("testkey2_rsa.pub.pem")) as pem_key:
             algo.prepare_key(pem_key.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_should_accept_pem_private_key_bytes(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
         with open(key_path("testkey_rsa.priv"), "rb") as pem_key:
             algo.prepare_key(pem_key.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_should_accept_unicode_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
         with open(key_path("testkey_rsa.priv")) as rsa_key:
             algo.prepare_key(rsa_key.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_should_reject_non_string_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
         with pytest.raises(TypeError):
             algo.prepare_key(None)
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_verify_should_return_false_if_signature_invalid(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -194,9 +182,7 @@ class TestAlgorithms:
         result = algo.verify(message, pub_key, sig)
         assert not result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_jwk_public_and_private_keys_should_parse_and_verify(self):
         tests = {
             "P-256": ECAlgorithm.SHA256,
@@ -215,9 +201,7 @@ class TestAlgorithms:
             signature = algo.sign(b"Hello World!", priv_key)
             assert algo.verify(b"Hello World!", pub_key, signature)
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_jwk_fails_on_invalid_json(self):
         algo = ECAlgorithm(ECAlgorithm.SHA512)
 
@@ -278,9 +262,7 @@ class TestAlgorithms:
                     '"d": "dGVzdA=="}}'.format(curve, point["x"], point["y"])
                 )
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_public_and_private_keys_should_parse_and_verify(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -293,9 +275,7 @@ class TestAlgorithms:
         signature = algo.sign(b"Hello World!", priv_key)
         assert algo.verify(b"Hello World!", pub_key, signature)
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_private_key_to_jwk_works_with_from_jwk(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -309,9 +289,7 @@ class TestAlgorithms:
             == orig_key.private_numbers().public_numbers
         )
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_public_key_to_jwk_works_with_from_jwk(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -321,9 +299,7 @@ class TestAlgorithms:
         parsed_key = algo.from_jwk(algo.to_jwk(orig_key))
         assert parsed_key.public_numbers() == orig_key.public_numbers()
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_private_key_with_other_primes_is_invalid(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -334,9 +310,7 @@ class TestAlgorithms:
 
                 algo.from_jwk(json.dumps(keydata))
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_private_key_with_missing_values_is_invalid(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -347,9 +321,7 @@ class TestAlgorithms:
 
                 algo.from_jwk(json.dumps(keydata))
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_private_key_can_recover_prime_factors(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -371,9 +343,7 @@ class TestAlgorithms:
         assert control_key.dmq1 == parsed_key.dmq1
         assert control_key.iqmp == parsed_key.iqmp
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_private_key_with_missing_required_values_is_invalid(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -384,9 +354,7 @@ class TestAlgorithms:
 
                 algo.from_jwk(json.dumps(keydata))
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_jwk_raises_exception_if_not_a_valid_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -398,9 +366,7 @@ class TestAlgorithms:
         with pytest.raises(InvalidKeyError):
             algo.from_jwk('{"kty": "RSA"}')
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_to_jwk_returns_correct_values_for_public_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -424,9 +390,7 @@ class TestAlgorithms:
         }
         assert json.loads(key) == expected
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_to_jwk_returns_correct_values_for_private_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -483,18 +447,14 @@ class TestAlgorithms:
         }
         assert json.loads(key) == expected
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_to_jwk_raises_exception_on_invalid_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
         with pytest.raises(InvalidKeyError):
             algo.to_jwk({"not": "a valid key"})
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_from_jwk_raises_exception_on_invalid_key(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -502,36 +462,28 @@ class TestAlgorithms:
             with pytest.raises(InvalidKeyError):
                 algo.from_jwk(keyfile.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_should_reject_non_string_key(self):
         algo = ECAlgorithm(ECAlgorithm.SHA256)
 
         with pytest.raises(TypeError):
             algo.prepare_key(None)
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_should_accept_pem_private_key_bytes(self):
         algo = ECAlgorithm(ECAlgorithm.SHA256)
 
         with open(key_path("testkey_ec.priv"), "rb") as ec_key:
             algo.prepare_key(ec_key.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_should_accept_ssh_public_key_bytes(self):
         algo = ECAlgorithm(ECAlgorithm.SHA256)
 
         with open(key_path("testkey_ec_ssh.pub")) as ec_key:
             algo.prepare_key(ec_key.read())
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_verify_should_return_false_if_signature_invalid(self):
         algo = ECAlgorithm(ECAlgorithm.SHA256)
 
@@ -552,9 +504,7 @@ class TestAlgorithms:
         result = algo.verify(message, pub_key, sig)
         assert not result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_verify_should_return_false_if_signature_wrong_length(self):
         algo = ECAlgorithm(ECAlgorithm.SHA256)
 
@@ -568,9 +518,7 @@ class TestAlgorithms:
         result = algo.verify(message, pub_key, sig)
         assert not result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_pss_sign_then_verify_should_return_true(self):
         algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)
 
@@ -586,9 +534,7 @@ class TestAlgorithms:
         result = algo.verify(message, pub_key, sig)
         assert result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_pss_verify_should_return_false_if_signature_invalid(self):
         algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)
 
@@ -643,9 +589,7 @@ class TestAlgorithmsRFC7520:
         result = algo.verify(signing_input, key, signature)
         assert result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsa_verify_should_return_true_for_test_vector(self):
         """
         This test verifies that RSA PKCS v1.5 verification works with a known
@@ -676,9 +620,7 @@ class TestAlgorithmsRFC7520:
         result = algo.verify(signing_input, key, signature)
         assert result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_rsapss_verify_should_return_true_for_test_vector(self):
         """
         This test verifies that RSA-PSS verification works with a known good
@@ -709,9 +651,7 @@ class TestAlgorithmsRFC7520:
         result = algo.verify(signing_input, key, signature)
         assert result
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_ec_verify_should_return_true_for_test_vector(self):
         """
         This test verifies that ECDSA verification works with a known good
@@ -740,9 +680,7 @@ class TestAlgorithmsRFC7520:
         assert result
 
 
-@pytest.mark.skipif(
-    not has_crypto, reason="Not supported without cryptography>=2.6 library"
-)
+@crypto_required
 class TestEd25519Algorithms:
     hello_world_sig = b"Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg=="
     hello_world = b"Hello World!"
diff --git a/tests/test_api_jwk.py b/tests/test_api_jwk.py
index ba4dbf2..6a1931f 100644
--- a/tests/test_api_jwk.py
+++ b/tests/test_api_jwk.py
@@ -1,24 +1,17 @@
 import json
 
-import pytest
-
 from jwt.api_jwk import PyJWK, PyJWKSet
 
-from .utils import key_path
+from .utils import crypto_required, key_path
 
 try:
     from jwt.algorithms import RSAAlgorithm
-
-    has_crypto = True
 except ImportError:
-    has_crypto = False
+    pass
 
 
 class TestPyJWK:
-    @pytest.mark.skipif(
-        not has_crypto,
-        reason="Scenario requires cryptography to not be installed",
-    )
+    @crypto_required
     def test_should_load_key_from_jwk_data_dict(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -39,10 +32,7 @@ class TestPyJWK:
         assert jwk.key_id == "keyid-abc123"
         assert jwk.public_key_use == "sig"
 
-    @pytest.mark.skipif(
-        not has_crypto,
-        reason="Scenario requires cryptography to not be installed",
-    )
+    @crypto_required
     def test_should_load_key_from_jwk_data_json_string(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -65,10 +55,7 @@ class TestPyJWK:
 
 
 class TestPyJWKSet:
-    @pytest.mark.skipif(
-        not has_crypto,
-        reason="Scenario requires cryptography to not be installed",
-    )
+    @crypto_required
     def test_should_load_keys_from_jwk_data_dict(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -90,10 +77,7 @@ class TestPyJWKSet:
         assert jwk.key_id == "keyid-abc123"
         assert jwk.public_key_use == "sig"
 
-    @pytest.mark.skipif(
-        not has_crypto,
-        reason="Scenario requires cryptography to not be installed",
-    )
+    @crypto_required
     def test_should_load_keys_from_jwk_data_json_string(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py
index a0456dc..d03f92c 100644
--- a/tests/test_api_jws.py
+++ b/tests/test_api_jws.py
@@ -3,7 +3,7 @@ from decimal import Decimal
 
 import pytest
 
-from jwt.algorithms import Algorithm
+from jwt.algorithms import Algorithm, has_crypto
 from jwt.api_jws import PyJWS
 from jwt.exceptions import (
     DecodeError,
@@ -13,7 +13,7 @@ from jwt.exceptions import (
 )
 from jwt.utils import base64url_decode
 
-from .utils import key_path
+from .utils import crypto_required, key_path, no_crypto_required
 
 try:
     from cryptography.hazmat.primitives.serialization import (
@@ -21,10 +21,8 @@ try:
         load_pem_public_key,
         load_ssh_public_key,
     )
-
-    has_crypto = True
 except ImportError:
-    has_crypto = False
+    pass
 
 
 @pytest.fixture
@@ -215,13 +213,32 @@ class TestJWS:
 
         assert decoded_payload == payload
 
+    def test_decodes_complete_valid_jws(self, jws, payload):
+        example_secret = "secret"
+        example_jws = (
+            b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+            b"aGVsbG8gd29ybGQ."
+            b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM"
+        )
+
+        decoded = jws.decode_complete(
+            example_jws, example_secret, algorithms=["HS256"]
+        )
+
+        assert decoded == {
+            "header": {"alg": "HS256", "typ": "JWT"},
+            "payload": payload,
+            "signature": (
+                b"\x80E\xb4\xa5\xd58\x93\x13\xed\x86;^\x85\x87a\xc4"
+                b"\x1ff0\xe1\x9a\x8e\xddq\x08\xa9F\x19p\xc9\xf0\xf3"
+            ),
+        }
+
     # 'Control' Elliptic Curve jws created by another library.
     # Used to test for regressions that could affect both
     # encoding / decoding operations equally (causing tests
     # to still pass).
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_decodes_valid_es384_jws(self, jws):
         example_payload = {"hello": "world"}
         with open(key_path("testkey_ec.pub")) as fp:
@@ -242,9 +259,7 @@ class TestJWS:
     # Used to test for regressions that could affect both
     # encoding / decoding operations equally (causing tests
     # to still pass).
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_decodes_valid_rs384_jws(self, jws):
         example_payload = {"hello": "world"}
         with open(key_path("testkey_rsa.pub")) as fp:
@@ -365,9 +380,7 @@ class TestJWS:
         with pytest.raises(NotImplementedError):
             jws.encode(payload, "secret", algorithm="HS1024")
 
-    @pytest.mark.skipif(
-        has_crypto, reason="Scenario requires cryptography to not be installed"
-    )
+    @no_crypto_required
     def test_missing_crypto_library_better_error_messages(self, jws, payload):
         with pytest.raises(NotImplementedError) as excinfo:
             jws.encode(payload, "secret", algorithm="RS256")
@@ -488,9 +501,7 @@ class TestJWS:
 
         assert "Key ID header parameter must be a string" == str(exc.value)
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_rsa_sha256(self, jws, payload):
         # PEM-formatted RSA key
         with open(key_path("testkey_rsa.priv"), "rb") as rsa_priv_file:
@@ -513,9 +524,7 @@ class TestJWS:
             pub_rsakey = rsa_pub_file.read()
             jws.decode(jws_message, pub_rsakey, algorithms=["RS256"])
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_rsa_sha384(self, jws, payload):
         # PEM-formatted RSA key
         with open(key_path("testkey_rsa.priv"), "rb") as rsa_priv_file:
@@ -537,9 +546,7 @@ class TestJWS:
             pub_rsakey = rsa_pub_file.read()
             jws.decode(jws_message, pub_rsakey, algorithms=["RS384"])
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Not supported without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_rsa_sha512(self, jws, payload):
         # PEM-formatted RSA key
         with open(key_path("testkey_rsa.priv"), "rb") as rsa_priv_file:
@@ -581,9 +588,7 @@ class TestJWS:
             assert "PS384" not in jws_algorithms
             assert "PS512" not in jws_algorithms
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_ecdsa_sha256(self, jws, payload):
         # PEM-formatted EC key
         with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file:
@@ -605,9 +610,7 @@ class TestJWS:
             pub_eckey = ec_pub_file.read()
             jws.decode(jws_message, pub_eckey, algorithms=["ES256"])
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_ecdsa_sha384(self, jws, payload):
 
         # PEM-formatted EC key
@@ -630,9 +633,7 @@ class TestJWS:
             pub_eckey = ec_pub_file.read()
             jws.decode(jws_message, pub_eckey, algorithms=["ES384"])
 
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_encode_decode_with_ecdsa_sha512(self, jws, payload):
         # PEM-formatted EC key
         with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file:
diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py
index 35ba6ba..e575668 100644
--- a/tests/test_api_jwt.py
+++ b/tests/test_api_jwt.py
@@ -17,8 +17,7 @@ from jwt.exceptions import (
     MissingRequiredClaimError,
 )
 
-from .test_api_jws import has_crypto
-from .utils import key_path, utc_timestamp
+from .utils import crypto_required, key_path, utc_timestamp
 
 
 @pytest.fixture
@@ -47,6 +46,27 @@ class TestJWT:
 
         assert decoded_payload == example_payload
 
+    def test_decodes_complete_valid_jwt(self, jwt):
+        example_payload = {"hello": "world"}
+        example_secret = "secret"
+        example_jwt = (
+            b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
+            b".eyJoZWxsbyI6ICJ3b3JsZCJ9"
+            b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8"
+        )
+        decoded = jwt.decode_complete(
+            example_jwt, example_secret, algorithms=["HS256"]
+        )
+
+        assert decoded == {
+            "header": {"alg": "HS256", "typ": "JWT"},
+            "payload": example_payload,
+            "signature": (
+                b'\xb6\xf6\xa0,2\xe8j"J\xc4\xe2\xaa\xa4\x15\xd2'
+                b"\x10l\xbbI\x84\xa2}\x98c\x9e\xd8&\xf5\xcbi\xca?"
+            ),
+        }
+
     def test_load_verify_valid_jwt(self, jwt):
         example_payload = {"hello": "world"}
         example_secret = "secret"
@@ -221,9 +241,7 @@ class TestJWT:
     # Used to test for regressions that could affect both
     # encoding / decoding operations equally (causing tests
     # to still pass).
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_decodes_valid_es256_jwt(self, jwt):
         example_payload = {"hello": "world"}
         with open(key_path("testkey_ec.pub")) as fp:
@@ -243,9 +261,7 @@ class TestJWT:
     # Used to test for regressions that could affect both
     # encoding / decoding operations equally (causing tests
     # to still pass).
-    @pytest.mark.skipif(
-        not has_crypto, reason="Can't run without cryptography library"
-    )
+    @crypto_required
     def test_decodes_valid_rs384_jwt(self, jwt):
         example_payload = {"hello": "world"}
         with open(key_path("testkey_rsa.pub")) as fp:
@@ -313,13 +329,12 @@ class TestJWT:
         secret = "secret"
         jwt_message = jwt.encode(payload, secret)
 
-        decoded_payload, signing, header, signature = jwt._load(jwt_message)
-
         # With 3 seconds leeway, should be ok
         for leeway in (3, timedelta(seconds=3)):
-            jwt.decode(
+            decoded = jwt.decode(
                 jwt_message, secret, leeway=leeway, algorithms=["HS256"]
             )
+            assert decoded == payload
 
         # With 1 seconds, should fail
         for leeway in (1, timedelta(seconds=1)):
diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py
index d263fbb..d607863 100644
--- a/tests/test_jwks_client.py
+++ b/tests/test_jwks_client.py
@@ -9,7 +9,7 @@ from jwt import PyJWKClient
 from jwt.api_jwk import PyJWK
 from jwt.exceptions import PyJWKClientError
 
-from .test_algorithms import has_crypto
+from .utils import crypto_required
 
 RESPONSE_DATA = {
     "keys": [
@@ -40,9 +40,7 @@ def mocked_response(data):
         yield
 
 
-@pytest.mark.skipif(
-    not has_crypto, reason="Not supported without cryptography library"
-)
+@crypto_required
 class TestPyJWKClient:
     def test_get_jwk_set(self):
         url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
diff --git a/tests/utils.py b/tests/utils.py
index 4b988e6..6847aba 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -2,6 +2,10 @@ import os
 from calendar import timegm
 from datetime import datetime
 
+import pytest
+
+from jwt.algorithms import has_crypto
+
 
 def utc_timestamp():
     return timegm(datetime.utcnow().utctimetuple())
@@ -11,3 +15,18 @@ def key_path(key_name):
     return os.path.join(
         os.path.dirname(os.path.realpath(__file__)), "keys", key_name
     )
+
+
+def no_crypto_required(class_or_func):
+    decorator = pytest.mark.skipif(
+        has_crypto,
+        reason="Requires cryptography library not installed",
+    )
+    return decorator(class_or_func)
+
+
+def crypto_required(class_or_func):
+    decorator = pytest.mark.skipif(
+        not has_crypto, reason="Requires cryptography library installed"
+    )
+    return decorator(class_or_func)