diff --git a/Dockerfile b/Dockerfile index d71f884..67c6d89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.10-slim -COPY requirements.txt /ikabotapi/ +COPY . /ikabotapi/ WORKDIR /ikabotapi # Install dependencies @@ -12,6 +12,4 @@ RUN apt-get update && \ RUN python -m playwright install && \ python -m playwright install-deps -COPY . . - CMD ["gunicorn", "-c", "gunicorn.conf.py", "run:app"] \ No newline at end of file diff --git a/apps/token/SupportedUserAgents.json b/apps/token/SupportedUserAgents.json new file mode 100644 index 0000000..17c1067 --- /dev/null +++ b/apps/token/SupportedUserAgents.json @@ -0,0 +1,26 @@ +[ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.4", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 OPR/108.0.0.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.5", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.5", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.3", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.3", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 OPR/108.0.0.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.3", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.14", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.10", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.3", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Geck", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.2", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0." +] \ No newline at end of file diff --git a/apps/token/TokenGenerator.py b/apps/token/TokenGenerator.py index e85e99d..b94b992 100644 --- a/apps/token/TokenGenerator.py +++ b/apps/token/TokenGenerator.py @@ -1,3 +1,4 @@ +import json import os from fake_useragent import FakeUserAgent @@ -10,33 +11,41 @@ class TokenGenerator: Usage: ``` - token_generator = TokenGenerator() + token_generator = TokenGenerator(supported_user_agents=["User Agent 1", "User Agent 2"]) token = token_generator.get_token() ``` """ - def __init__(self): + def __init__(self, supported_user_agents): """ Initialize TokenGenerator. - Sets the path to the HTML file used for token generation. + - Sets the supported user agents. """ current_directory = os.path.dirname(os.path.abspath(__file__)) self.html_file_path = f"file:///{current_directory}/token.html" + self.supported_user_agents = supported_user_agents - def get_token(self): + def get_token(self, user_agent: str = None): """ Get a token from the HTML file. - Waits for the presence of a specific div element in the HTML file and returns its text content as the token. + This method waits for the presence of a specific div element in the HTML file and returns its text content as the token. + + Args: + - user_agent (str, optional): The user agent string to use for the browser. If not provided, a random user agent will be used. Returns: - str: The generated token. """ with sync_playwright() as playwright: - random_useragent = FakeUserAgent().random + if user_agent and user_agent in self.supported_user_agents: + playwright_useragent = user_agent + else: + playwright_useragent = FakeUserAgent().random browser = playwright.chromium.launch(headless=True) - context = browser.new_context(user_agent=random_useragent) + context = browser.new_context(user_agent=playwright_useragent) page = context.new_page() page.goto(self.html_file_path) token_element = page.wait_for_selector("body > div") diff --git a/apps/token/__init__.py b/apps/token/__init__.py index 594e9f0..643c0c5 100644 --- a/apps/token/__init__.py +++ b/apps/token/__init__.py @@ -1,4 +1,6 @@ +import json import logging +import os import threading import time @@ -10,7 +12,9 @@ blueprint = Blueprint("token_blueprint", __name__, url_prefix="") -token_generator = TokenGenerator() +current_directory = os.path.dirname(os.path.abspath(__file__)) +supported_user_agents = json.load(open(f"{current_directory}/SupportedUserAgents.json")) +token_generator = TokenGenerator(supported_user_agents=supported_user_agents) tokenList = [] refreshCounter = 0 diff --git a/apps/token/routes.py b/apps/token/routes.py index 1f7da28..bba1213 100644 --- a/apps/token/routes.py +++ b/apps/token/routes.py @@ -3,7 +3,17 @@ import threading import time -from apps.token import blueprint, getTokenFromList, lock, logger, threadqueue +from flask import jsonify, request + +from apps.token import ( + blueprint, + getTokenFromList, + lock, + logger, + supported_user_agents, + threadqueue, + token_generator, +) @blueprint.route("/token", methods=["GET"]) @@ -23,5 +33,58 @@ def token_route(): time.sleep(0.01) return token except Exception: - logger.exception("Ran into error while generating token. Retrying...") + logger.exception("Error in token route") return "Error" + + +@blueprint.route("/v1/token", methods=["GET"]) +def v1_token_route(): + try: + if "user_agent" not in request.args: + return ( + jsonify( + { + "status": "error", + "message": "Bad Request: Missing user_agent query parameter", + } + ), + 400, + ) + + user_agent = request.args.get("user_agent") + + if user_agent == "": + return ( + jsonify( + { + "status": "error", + "message": "Bad Request: Empty user_agent query parameter", + } + ), + 400, + ) + + if user_agent not in supported_user_agents: + return ( + jsonify( + { + "status": "error", + "message": "Bad Request: Unsupported user_agent query parameter", + } + ), + 400, + ) + + token = token_generator.get_token(user_agent) + return jsonify(token), 200 + except Exception: + logger.exception("Error in v1_token route") + return ( + jsonify( + { + "status": "error", + "message": "An error occurred during token generation", + } + ), + 500, + ) diff --git a/tests/test_setup_logger.py b/tests/test_setup_logger.py new file mode 100644 index 0000000..d6fdfc1 --- /dev/null +++ b/tests/test_setup_logger.py @@ -0,0 +1,39 @@ +import logging + +import pytest +from pytest_mock import MockerFixture + +from apps import setup_logger + + +def test_setup_logger_with_webhook_url_defined(mocker: MockerFixture): + mocker.patch("settings.LOGS_WEBHOOK_URL", "https://example.com/webhook") + + logger = logging.getLogger() + logger.handlers.clear() + + setup_logger() + + assert len(logger.handlers) == 2 # One for console and one for Discord + + +def test_setup_logger_with_webhook_url_not_defined(mocker: MockerFixture): + mocker.patch("settings.LOGS_WEBHOOK_URL", None) + + logger = logging.getLogger() + logger.handlers.clear() + + setup_logger() + + assert len(logger.handlers) == 1 # Only one for console + + +def test_setup_logger_with_empty_webhook_url(mocker: MockerFixture): + mocker.patch("settings.LOGS_WEBHOOK_URL", "") + + logger = logging.getLogger() + logger.handlers.clear() + + setup_logger() + + assert len(logger.handlers) == 1 # Only one for console diff --git a/tests/test_token_generator.py b/tests/test_token_generator.py index ac70c17..c62db39 100644 --- a/tests/test_token_generator.py +++ b/tests/test_token_generator.py @@ -1,29 +1,52 @@ +import json +import os + import pytest +from pytest_mock import MockerFixture from apps.token.TokenGenerator import TokenGenerator +from tests.token_validator import verify_token_format @pytest.fixture def token_generator(): - token_generator = TokenGenerator() + json_file_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "apps", "token", "SupportedUserAgents.json" + ) + ) + supported_user_agents = json.load(open(json_file_path)) + token_generator = TokenGenerator(supported_user_agents=supported_user_agents) yield token_generator -def test_token_not_empty(token_generator): - token = token_generator.get_token() - assert token is not None, "Token should not be None" - assert token != "", "Token should not be an empty string" +def test_get_token_returns_unique_tokens(token_generator): + tokens = [token_generator.get_token() for _ in range(5)] + assert len(set(tokens)) == len(tokens), "Tokens should be unique" -def test_tokens_are_unique(token_generator): - tokens = [token_generator.get_token() for _ in range(5)] +def test_get_token_returns_unique_tokens_with_specific_user_agent(token_generator): + tokens = [ + token_generator.get_token( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" + ) + for _ in range(5) + ] assert len(set(tokens)) == len(tokens), "Tokens should be unique" -def test_token_characteristics(token_generator): +def test_get_token_returns_valid_token(token_generator): token = token_generator.get_token() - assert isinstance(token, str), "Token should be a string" - assert " " not in token, "Token should not contain any whitespace" - assert any(c.isupper() for c in token), "The string must contain uppercase letters." - assert any(c.islower() for c in token), "The string must contain lowercase letters." - assert any(c.isdigit() for c in token), "The string must contain digits." + verify_token_format(token) + + +def test_get_token_returns_valid_token_with_specific_user_agent(token_generator): + token = token_generator.get_token( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" + ) + verify_token_format(token) + + +def test_get_token_returns_valid_token_with_empty_user_agent(token_generator): + token = token_generator.get_token(user_agent="") + verify_token_format(token) diff --git a/tests/test_token_route.py b/tests/test_token_route.py index 6cd1bec..bea5312 100644 --- a/tests/test_token_route.py +++ b/tests/test_token_route.py @@ -1,9 +1,11 @@ import json +import token import pytest from pytest_mock import MockerFixture from apps import create_app +from tests.token_validator import verify_token_format @pytest.fixture @@ -14,6 +16,58 @@ def client(): yield client -def test_get_token_should_return_status_code_ok(client): +def test_token_should_return_status_code_200_even_on_error(client): + """ + Test that the /token route returns status code 200. + Note: This test reflects the behavior where the route may return status code 200 even if there is an error. + """ response = client.get("/token") assert response.status_code == 200 + + +def test_v1_token_route_without_user_agent_should_return_400(client): + response = client.get("/v1/token") + assert response.status_code == 400 + response_json = json.loads(response.data) + assert response_json["status"] == "error" + assert response_json["message"] == "Bad Request: Missing user_agent query parameter" + + +def test_v1_token_route_with_empty_user_agent_should_return_400(client): + user_agent = "" + response = client.get(f"/v1/token?user_agent={user_agent}") + assert response.status_code == 400 + response_json = json.loads(response.data) + assert response_json["status"] == "error" + assert response_json["message"] == "Bad Request: Empty user_agent query parameter" + + +def test_v1_token_route_with_unsupported_user_agent_should_return_400(client): + user_agent = "Unsupported User Agent" + response = client.get(f"/v1/token?user_agent={user_agent}") + assert response.status_code == 400 + response_json = json.loads(response.data) + assert response_json["status"] == "error" + assert ( + response_json["message"] + == "Bad Request: Unsupported user_agent query parameter" + ) + + +def test_v1_token_route_with_supported_user_agent_should_return_200(client): + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.3" + response = client.get(f"/v1/token?user_agent={user_agent}") + assert response.status_code == 200 + token = json.loads(response.data) + verify_token_format(token) + + +def test_v1_token_route_error(client, mocker: MockerFixture): + mocker.patch( + "apps.token.TokenGenerator.get_token", + side_effect=Exception("Test Exception"), + ) + + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.3" + response = client.get(f"/v1/token?user_agent={user_agent}") + assert response.status_code == 500 diff --git a/tests/token_validator.py b/tests/token_validator.py new file mode 100644 index 0000000..9cf5d14 --- /dev/null +++ b/tests/token_validator.py @@ -0,0 +1,17 @@ +def verify_token_format(token): + """ + Verify the format of the token. + + Args: + token (str): The token to be verified. + + Raises: + AssertionError: If the token does not meet the required format criteria. + """ + assert token is not None, "Token should not be None" + assert token != "", "Token should not be an empty string" + assert isinstance(token, str), "Token should be a string" + assert " " not in token, "Token should not contain any whitespace" + assert any(c.isupper() for c in token), "The string must contain uppercase letters." + assert any(c.islower() for c in token), "The string must contain lowercase letters." + assert any(c.isdigit() for c in token), "The string must contain digits."