diff --git a/api/vercel_function.py b/api/vercel_function.py index d099b92..5812542 100644 --- a/api/vercel_function.py +++ b/api/vercel_function.py @@ -1 +1,5 @@ -from app import app +from app import create_app +from config import ProductionConfig + + +app = create_app(ProductionConfig) diff --git a/app/__init__.py b/app/__init__.py index 5f11444..69c7e0f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,99 +1,28 @@ -import os -from datetime import timedelta +from flask import Flask -from flask import Flask, jsonify -from flask_jwt_extended import JWTManager -from flask_migrate import Migrate -from flask_sqlalchemy import SQLAlchemy -from dotenv import load_dotenv -from sqlalchemy import MetaData -from flask_smorest import Api +from app.extensions import api, db, jwt, migrate +from config import DevelopmentConfig -def register_blueprints(): +def create_app(config_class=DevelopmentConfig): + app = Flask(__name__) + app.config.from_object(config_class) + + # initialize extensions + db.init_app(app) + migrate.init_app(app, db) + jwt.init_app(app) + api.init_app(app) + + # register blueprints + from app.routes.auth import bp as auth_bp from app.routes.category import bp as category_bp - from app.routes.subcategory import bp as subcategory_bp from app.routes.product import bp as product_bp - from app.routes.auth import bp as auth_bp + from app.routes.subcategory import bp as subcategory_bp api.register_blueprint(category_bp, url_prefix="/categories") api.register_blueprint(subcategory_bp, url_prefix="/subcategories") api.register_blueprint(product_bp, url_prefix="/products") api.register_blueprint(auth_bp, url_prefix="/auth") - -app = Flask(__name__) - -load_dotenv() - -# sqlalchemy -app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("SQLALCHEMY_DATABASE_URI") -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - -# jwt -app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY") -app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=3) -app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=3) - -# flask-smorest -app.config["API_TITLE"] = "Ecommerce REST API" -app.config["API_VERSION"] = "v1" -app.config["OPENAPI_VERSION"] = "3.0.2" - -# flask-smorest openapi swagger -app.config["OPENAPI_URL_PREFIX"] = "/" -app.config["OPENAPI_SWAGGER_UI_PATH"] = "/" -app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" - -# flask-smorest Swagger UI top level authorize dialog box -app.config["API_SPEC_OPTIONS"] = { - "components": { - "securitySchemes": { - "access_token": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "Enter your JWT access token", - }, - "refresh_token": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "Enter your JWT refresh token", - }, - } - } -} - -# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB) -# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names -naming_convention = { - "ix": "%(table_name)s_%(column_0_name)s_idx", # Indexes - "uq": "%(table_name)s_%(column_0_name)s_key", # Unique constraints - "ck": "%(table_name)s_%(constraint_name)s_check", # Check constraints - "fk": "%(table_name)s_%(column_0_name)s_fkey", # Foreign keys - "pk": "%(table_name)s_pkey" # Primary keys -} -metadata = MetaData(naming_convention=naming_convention) -db = SQLAlchemy(app, metadata=metadata) -migrate = Migrate(app, db) -jwt = JWTManager(app) -api = Api(app) - -register_blueprints() - - -@jwt.expired_token_loader -def expired_token_callback(jwt_header, jwt_payload): - err = "Access token expired. Use your refresh token to get a new one." - if jwt_payload['type'] == 'refresh': - err = "Refresh token expired. Please login again." - return jsonify(code="token_expired", error=err), 401 - -@jwt.invalid_token_loader -def invalid_token_callback(error): - return jsonify(code="invalid_token", error="Invalid token provided."), 401 - -@jwt.unauthorized_loader -def missing_token_callback(error): - return jsonify(code="authorization_required", error="JWT needed for this operation. Login, if needed."), 401 + return app diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..afdcbcb --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,43 @@ +from flask import jsonify +from flask_jwt_extended import JWTManager +from flask_migrate import Migrate +from flask_smorest import Api +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import MetaData + + +# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB) +# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names +naming_convention = { + "ix": "%(table_name)s_%(column_0_name)s_idx", # Indexes + "uq": "%(table_name)s_%(column_0_name)s_key", # Unique constraints + "ck": "%(table_name)s_%(constraint_name)s_check", # Check constraints + "fk": "%(table_name)s_%(column_0_name)s_fkey", # Foreign keys + "pk": "%(table_name)s_pkey", # Primary keys +} +metadata = MetaData(naming_convention=naming_convention) +db = SQLAlchemy(metadata=metadata) +migrate = Migrate(db) +jwt = JWTManager() +api = Api() + + +@jwt.expired_token_loader +def expired_token_callback(jwt_header, jwt_payload): + err = "Access token expired. Use your refresh token to get a new one." + if jwt_payload["type"] == "refresh": + err = "Refresh token expired. Please login again." + return jsonify(code="token_expired", error=err), 401 + + +@jwt.invalid_token_loader +def invalid_token_callback(error): + return jsonify(code="invalid_token", error="Invalid token provided."), 401 + + +@jwt.unauthorized_loader +def missing_token_callback(error): + return jsonify( + code="authorization_required", + error="JWT needed for this operation. Login, if needed.", + ), 401 diff --git a/config.py b/config.py new file mode 100644 index 0000000..b9291c2 --- /dev/null +++ b/config.py @@ -0,0 +1,62 @@ +import os +from datetime import timedelta + +from dotenv import load_dotenv + + +load_dotenv() + + +class Config: + # sqlalchemy + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # jwt + JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") + JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=3) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=3) + + # flask-smorest + API_TITLE = "Ecommerce REST API" + API_VERSION = "v1" + OPENAPI_VERSION = "3.0.2" + + # flask-smorest openapi swagger + OPENAPI_URL_PREFIX = "/" + OPENAPI_SWAGGER_UI_PATH = "/" + OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" + + # flask-smorest Swagger UI top level authorize dialog box + API_SPEC_OPTIONS = { + "components": { + "securitySchemes": { + "access_token": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Enter your JWT access token", + }, + "refresh_token": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Enter your JWT refresh token", + }, + } + } + } + + +class DevelopmentConfig(Config): + DEBUG = True + SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") + + +class TestingConfig(Config): + TESTING = True + SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" + JWT_SECRET_KEY = os.urandom(24).hex() + + +class ProductionConfig(Config): + SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") diff --git a/populate_db.py b/populate_db.py index fdae8d2..527abe3 100644 --- a/populate_db.py +++ b/populate_db.py @@ -1,11 +1,13 @@ from faker import Faker -from app import app, db +from app import create_app, db from app.models import Category, Subcategory, Product, category_subcategory, subcategory_product import random +app = create_app() fake = Faker() + def create_categories(num=5): categories = [] for _ in range(num): diff --git a/tests/conftest.py b/tests/conftest.py index ec74981..f5f4bea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,24 +1,30 @@ -import os - import pytest -# TODO: Fix hack. Changes the env var before initializing the db for testing -os.environ["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" -os.environ["JWT_SECRET_KEY"] = os.urandom(24).hex() - -from app import app, db +from app import create_app, db +from config import TestingConfig from tests import utils @pytest.fixture -def client(): - app.config["TESTING"] = True - with app.test_client() as client: - with app.app_context(): - db.create_all() - yield client - with app.app_context(): - db.drop_all() +def app(): + app = create_app(TestingConfig) + + # setup + app_context = app.app_context() + app_context.push() + db.create_all() + + yield app + + # teardown + db.session.remove() + db.drop_all() + app_context.pop() + + +@pytest.fixture +def client(app): + return app.test_client() @pytest.fixture diff --git a/tests/test_auth.py b/tests/test_auth.py index 28d567e..52eb61f 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -16,22 +16,19 @@ class TestAuth: @pytest.fixture(autouse=True) def setup(self, client): self.client = client - with client.application.app_context(): - assert User.query.count() == 0 + assert User.query.count() == 0 def _verify_user_in_db(self, email, should_exist=True): - with self.client.application.app_context(): - user = User.get(email=email) - if should_exist: - assert user is not None - assert user.email == email - return user - else: - assert user is None + user = User.get(email=email) + if should_exist: + assert user is not None + assert user.email == email + return user + else: + assert user is None def _count_users(self): - with self.client.application.app_context(): - return User.query.count() + return User.query.count() def _test_invalid_request_data(self, endpoint, expected_status=422): response = self.client.post(endpoint, json={}) @@ -47,9 +44,7 @@ def _test_invalid_request_data(self, endpoint, expected_status=422): assert response.status_code == expected_status def _decode_token(self, token): - # Needs Flask app context for secret/algorithms from current_app.config - with self.client.application.app_context(): - return decode_token(token, allow_expired=False) + return decode_token(token, allow_expired=False) def _assert_jwt_structure(self, token, expected_sub, expected_type, fresh=False): assert token.count(".") == 2, f"Token does not have three segments: {token}" @@ -169,8 +164,6 @@ def test_refresh_token_missing_auth(self): utils.verify_token_error_response(response, "authorization_required") def test_refresh_token_expired(self): - expired_headers = utils.get_expired_token_headers( - self.client.application.app_context() - ) + expired_headers = utils.get_expired_token_headers() response = self.client.post("/auth/refresh", headers=expired_headers) utils.verify_token_error_response(response, "token_expired") diff --git a/tests/test_category.py b/tests/test_category.py index 6e2527b..4f1012d 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -13,22 +13,19 @@ class TestCategory: @pytest.fixture(autouse=True) def setup(self, client): self.client = client - with client.application.app_context(): - assert Category.query.count() == 0 + assert Category.query.count() == 0 def _count_categories(self): - with self.client.application.app_context(): - return Category.query.count() + return Category.query.count() def _verify_category_in_db(self, name, should_exist=True): - with self.client.application.app_context(): - category = Category.query.filter_by(name=name).first() - if should_exist: - assert category is not None - assert category.name == name - return category - else: - assert category is None + category = Category.query.filter_by(name=name).first() + if should_exist: + assert category is not None + assert category.name == name + return category + else: + assert category is None def test_create_category(self, create_category): response = create_category(self.TEST_CATEGORY_NAME) @@ -130,13 +127,13 @@ def test_delete_category(self, create_authenticated_headers, create_category): @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_create_category_token_error(self, get_headers, expected_code): - headers = get_headers(self) + headers = get_headers() response = self.client.post( "/categories", json={"name": "CreateTokenError"}, headers=headers ) @@ -146,9 +143,9 @@ def test_create_category_token_error(self, get_headers, expected_code): @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_update_category_token_error(self, get_headers, create_category, expected_code): @@ -156,7 +153,7 @@ def test_update_category_token_error(self, get_headers, create_category, expecte data = response.get_json() cat_id = data["id"] - update_headers = get_headers(self) + update_headers = get_headers() update_resp = self.client.put( f"/categories/{cat_id}", json={"name": "UpdatedName"}, @@ -170,9 +167,9 @@ def test_update_category_token_error(self, get_headers, create_category, expecte @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_delete_category_token_error(self, get_headers, create_category, expected_code): @@ -180,7 +177,7 @@ def test_delete_category_token_error(self, get_headers, create_category, expecte data = response.get_json() cat_id = data["id"] - delete_headers = get_headers(self) + delete_headers = get_headers() delete_resp = self.client.delete(f"/categories/{cat_id}", headers=delete_headers) utils.verify_token_error_response(delete_resp, expected_code) diff --git a/tests/test_product.py b/tests/test_product.py index 9152af8..541854e 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -14,22 +14,19 @@ class TestProduct: @pytest.fixture(autouse=True) def setup(self, client): self.client = client - with client.application.app_context(): - assert Product.query.count() == 0 + assert Product.query.count() == 0 def _count_products(self): - with self.client.application.app_context(): - return Product.query.count() + return Product.query.count() def _verify_product_in_db(self, name, should_exist=True): - with self.client.application.app_context(): - product = Product.query.filter_by(name=name).first() - if should_exist: - assert product is not None - assert product.name == name - return product - else: - assert product is None + product = Product.query.filter_by(name=name).first() + if should_exist: + assert product is not None + assert product.name == name + return product + else: + assert product is None def test_create_product(self, create_product): response = create_product(self.TEST_PRODUCT_NAME, self.TEST_PRODUCT_DESC) @@ -157,13 +154,13 @@ def test_get_product_by_name(self, create_product, name): @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_create_product_token_error(self, get_headers, expected_code): - headers = get_headers(self) + headers = get_headers() response = self.client.post( "/products", json={"name": "CreateTokenError"}, headers=headers ) @@ -173,9 +170,9 @@ def test_create_product_token_error(self, get_headers, expected_code): @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_update_product_token_error(self, get_headers, create_product, expected_code): @@ -183,7 +180,7 @@ def test_update_product_token_error(self, get_headers, create_product, expected_ data = response.get_json() p_id = data["id"] - update_headers = get_headers(self) + update_headers = get_headers() update_resp = self.client.put( f"/products/{p_id}", json={"name": "UpdatedName"}, @@ -197,9 +194,9 @@ def test_update_product_token_error(self, get_headers, create_product, expected_ @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_delete_product_token_error(self, get_headers, create_product, expected_code): @@ -207,7 +204,7 @@ def test_delete_product_token_error(self, get_headers, create_product, expected_ data = response.get_json() p_id = data["id"] - delete_headers = get_headers(self) + delete_headers = get_headers() delete_resp = self.client.delete(f"/products/{p_id}", headers=delete_headers) utils.verify_token_error_response(delete_resp, expected_code) diff --git a/tests/test_relationships.py b/tests/test_relationships.py index 4190845..855b512 100644 --- a/tests/test_relationships.py +++ b/tests/test_relationships.py @@ -10,40 +10,34 @@ class TestRelationships: @pytest.fixture(autouse=True) def setup(self, client): self.client = client - with client.application.app_context(): - assert Category.query.count() == 0 - assert Subcategory.query.count() == 0 - assert Product.query.count() == 0 + assert Category.query.count() == 0 + assert Subcategory.query.count() == 0 + assert Product.query.count() == 0 def _category_subcategory_ids(self, category_id): - with self.client.application.app_context(): - category = Category.query.get(category_id) - assert category is not None - return sorted([subcategory.id for subcategory in category.subcategories]) + category = Category.query.get(category_id) + assert category is not None + return sorted([subcategory.id for subcategory in category.subcategories]) def _subcategory_category_ids(self, subcategory_id): - with self.client.application.app_context(): - subcategory = Subcategory.query.get(subcategory_id) - assert subcategory is not None - return sorted([category.id for category in subcategory.categories]) + subcategory = Subcategory.query.get(subcategory_id) + assert subcategory is not None + return sorted([category.id for category in subcategory.categories]) def _subcategory_product_ids(self, subcategory_id): - with self.client.application.app_context(): - subcategory = Subcategory.query.get(subcategory_id) - assert subcategory is not None - return sorted([product.id for product in subcategory.products]) + subcategory = Subcategory.query.get(subcategory_id) + assert subcategory is not None + return sorted([product.id for product in subcategory.products]) def _product_subcategory_ids(self, product_id): - with self.client.application.app_context(): - product = Product.query.get(product_id) - assert product is not None - return sorted([subcategory.id for subcategory in product.subcategories]) + product = Product.query.get(product_id) + assert product is not None + return sorted([subcategory.id for subcategory in product.subcategories]) def _category_product_ids_via_subcategories(self, category_id): - with self.client.application.app_context(): - category = Category.query.get(category_id) - assert category is not None - return sorted({product.id for subcategory in category.subcategories for product in subcategory.products.all()}) + category = Category.query.get(category_id) + assert category is not None + return sorted({product.id for subcategory in category.subcategories for product in subcategory.products.all()}) def _assert_related_collection(self, resp, key, expected_ids=None, status_code=200): assert resp.status_code == status_code diff --git a/tests/test_subcategory.py b/tests/test_subcategory.py index 728cd25..e38e8d5 100644 --- a/tests/test_subcategory.py +++ b/tests/test_subcategory.py @@ -13,22 +13,19 @@ class TestSubcategory: @pytest.fixture(autouse=True) def setup(self, client): self.client = client - with client.application.app_context(): - assert Subcategory.query.count() == 0 + assert Subcategory.query.count() == 0 def _count_subcategories(self): - with self.client.application.app_context(): - return Subcategory.query.count() + return Subcategory.query.count() def _verify_subcategory_in_db(self, name, should_exist=True): - with self.client.application.app_context(): - subcategory = Subcategory.query.filter_by(name=name).first() - if should_exist: - assert subcategory is not None - assert subcategory.name == name - return subcategory - else: - assert subcategory is None + subcategory = Subcategory.query.filter_by(name=name).first() + if should_exist: + assert subcategory is not None + assert subcategory.name == name + return subcategory + else: + assert subcategory is None def test_create_subcategory(self, create_subcategory): response = create_subcategory(self.TEST_SUBCATEGORY_NAME) @@ -129,13 +126,13 @@ def test_delete_subcategory(self, create_authenticated_headers, create_subcatego @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_create_subcategory_token_error(self, get_headers, expected_code): - headers = get_headers(self) + headers = get_headers() response = self.client.post( "/subcategories", json={"name": "CreateTokenError"}, headers=headers ) @@ -145,9 +142,9 @@ def test_create_subcategory_token_error(self, get_headers, expected_code): @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_update_subcategory_token_error(self, get_headers, create_subcategory, expected_code): @@ -155,7 +152,7 @@ def test_update_subcategory_token_error(self, get_headers, create_subcategory, e data = response.get_json() sc_id = data["id"] - update_headers = get_headers(self) + update_headers = get_headers() update_resp = self.client.put( f"/subcategories/{sc_id}", json={"name": "UpdatedName"}, @@ -169,9 +166,9 @@ def test_update_subcategory_token_error(self, get_headers, create_subcategory, e @pytest.mark.parametrize( "get_headers, expected_code", [ - (lambda self: utils.get_expired_token_headers(self.client.application.app_context()), "token_expired"), - (lambda self: utils.get_invalid_token_headers(), "invalid_token"), - (lambda self: None, "authorization_required") + (utils.get_expired_token_headers, "token_expired"), + (utils.get_invalid_token_headers, "invalid_token"), + (lambda: None, "authorization_required") ] ) def test_delete_subcategory_token_error(self, get_headers, create_subcategory, expected_code): @@ -179,7 +176,7 @@ def test_delete_subcategory_token_error(self, get_headers, create_subcategory, e data = response.get_json() sc_id = data["id"] - delete_headers = get_headers(self) + delete_headers = get_headers() delete_resp = self.client.delete(f"/subcategories/{sc_id}", headers=delete_headers) utils.verify_token_error_response(delete_resp, expected_code) diff --git a/tests/utils.py b/tests/utils.py index 36b5c9b..222474e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,12 +14,11 @@ def get_auth_header(token): return {"Authorization": f"Bearer {token}"} -def get_expired_token_headers(app_context, id=1): - with app_context: - token = create_access_token( - identity=str(id), expires_delta=timedelta(seconds=-1) - ) - return get_auth_header(token) +def get_expired_token_headers(id=1): + token = create_access_token( + identity=str(id), expires_delta=timedelta(seconds=-1) + ) + return get_auth_header(token) def get_invalid_token_headers():