From 4deb5305e730b9a04aea86ff2fbd5279ba6fe30d Mon Sep 17 00:00:00 2001 From: jonazakana Date: Wed, 15 Oct 2025 15:58:47 +0200 Subject: [PATCH 1/2] edit spotify service --- server/routes/spotify.py | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 7f7f337..8181305 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -37,23 +37,11 @@ def spotify_login(): return redirect(url) -@bp.route("/spotify/callback", methods=["GET"]) +@bp.route("/spotify/callback") def spotify_callback(): - """ - Callback Spotify après l'authentification - --- - tags: - - Spotify - responses: - 200: - description: Retourne les tokens d'accès Spotify - """ - if "error" in request.args: - return jsonify({"error": request.args["error"]}), 400 - code = request.args.get("code") if not code: - return jsonify({"error": "Missing authorization code"}), 400 + return jsonify({"error": "Missing code"}), 400 data = { "grant_type": "authorization_code", @@ -63,22 +51,14 @@ def spotify_callback(): "client_secret": CLIENT_SECRET } - res = requests.post(TOKEN_URL, data=data) - if res.status_code != 200: - return jsonify({"error": res.text}), 400 - + headers = {"Content-Type": "application/x-www-form-urlencoded"} + res = requests.post("https://accounts.spotify.com/api/token", data=data, headers=headers) tokens = res.json() - access_token = tokens.get("access_token") - refresh_token = tokens.get("refresh_token") - expires_in = tokens.get("expires_in") - session["spotify_tokens"] = { - "access_token": access_token, - "refresh_token": refresh_token, - "expires_at": datetime.now().timestamp() + expires_in - } + if "access_token" not in tokens: + return jsonify({"error": "Spotify token exchange failed", "details": tokens}), 400 - return jsonify({ - "message": "Spotify connected successfully!", - "tokens": tokens - }) \ No newline at end of file + # Redirige vers ton FRONT avec les tokens dans le hash fragment (sécurisé) + front_url = os.getenv("FRONT_REDIRECT_URI") + redirect_url = f"{front_url}#access_token={tokens['access_token']}&refresh_token={tokens.get('refresh_token','')}" + return redirect(redirect_url) From b65d1b096bc731d56e528eabf9c1aa8811f2b316 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Mon, 20 Oct 2025 12:40:41 +0200 Subject: [PATCH 2/2] add pytest --- logs/api.log | 7 ++++++ pytest.ini | 5 ++++ server/app.py | 27 +++++++++++++++----- server/requirements.txt | 4 +++ server/routes/spotify.py | 54 +++++++++++++++++----------------------- tests/__init__.py | 0 tests/conftest.py | 22 ++++++++++++++++ tests/test_about.py | 14 +++++++++++ 8 files changed, 96 insertions(+), 37 deletions(-) create mode 100644 logs/api.log create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_about.py diff --git a/logs/api.log b/logs/api.log new file mode 100644 index 0000000..5a3d1a5 --- /dev/null +++ b/logs/api.log @@ -0,0 +1,7 @@ +2025-10-20 10:42:30,488 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: 00c9054d4b4dc08a1cd512fdc7bedeeb). +2025-10-20 10:43:12,883 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: 27e603dee3f998f774b30b7dc19fff29). +2025-10-20 10:47:31,256 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: 4596e2a8c1c163c50a0295b2b2d68e4a). +2025-10-20 10:48:18,455 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: af714c3798c54aece11d596133567db5). +2025-10-20 10:57:25,663 [INFO] AREA-API: App started in TEST mode (SQLite in-memory). +2025-10-20 10:59:21,018 [INFO] AREA-API: App started in TEST mode (SQLite in-memory). +2025-10-20 12:23:10,150 [INFO] AREA-API: App started in TEST mode (SQLite in-memory). diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3e1f437 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +pythonpath = server +testpaths = server/tests +python_files = test_*.py +addopts = -v diff --git a/server/app.py b/server/app.py index 8ad113e..bb79f15 100644 --- a/server/app.py +++ b/server/app.py @@ -12,29 +12,44 @@ logger = setup_logger() -def create_app(): +def create_app(mode="default"): app = Flask(__name__) swagger = Swagger(app, config=swagger_config, template=template) app.config.from_object(Config) + if app.config.get("TESTING") or mode == "test": + app.config.update({ + "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", + "SQLALCHEMY_ENGINE_OPTIONS": {}, + }) + logger.info("App started in TEST mode (SQLite in-memory).") + db.init_app(app) migrate.init_app(app) jwt.init_app(app) - CORS(app) - register_routes(app) + if app.config.get("TESTING") or mode == "test": + with app.app_context(): + db.create_all() + return app + wait_for_db(app) with app.app_context(): db.create_all() seed_services() scheduler = BackgroundScheduler() - scheduler.add_job(lambda: check_hooks(app), "interval", seconds=app.config["SCHEDULER_INTERVAL"], id="check_hooks_job") + scheduler.add_job( + lambda: check_hooks(app), + "interval", + seconds=app.config["SCHEDULER_INTERVAL"], + id="check_hooks_job" + ) scheduler.start() - - logger.info("FLASK successfully running") + + logger.info("Flask successfully running") return app diff --git a/server/requirements.txt b/server/requirements.txt index 0e08cf3..eb3317d 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -10,3 +10,7 @@ APScheduler==3.10.4 flasgger discord cryptography +pytest +pytest-flask +pytest-mock + diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 8181305..fa5fedd 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -12,53 +12,45 @@ AUTH_URL = "https://accounts.spotify.com/authorize" TOKEN_URL = "https://accounts.spotify.com/api/token" -@bp.route("/spotify/login", methods=["GET"]) +import urllib.parse + +@bp.route("/spotify/login") def spotify_login(): - """ - Redirige l'utilisateur vers Spotify pour se connecter à l'application - --- - tags: - - Spotify - responses: - 302: - description: Redirection vers la page d'autorisation Spotify - """ - + frontend = request.args.get("frontend", "web") + scope = "user-read-currently-playing user-read-playback-state" + state = f"frontend:{frontend}" + params = { "client_id": CLIENT_ID, "response_type": "code", "redirect_uri": REDIRECT_URI, "scope": scope, - "show_dialog": "true" + "state": state, } - url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}" - return redirect(url) + auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}" + return redirect(auth_url) + @bp.route("/spotify/callback") def spotify_callback(): code = request.args.get("code") - if not code: - return jsonify({"error": "Missing code"}), 400 + state = request.args.get("state", "frontend:web") - data = { - "grant_type": "authorization_code", - "code": code, - "redirect_uri": REDIRECT_URI, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET - } + frontend = state.split(":")[1] - headers = {"Content-Type": "application/x-www-form-urlencoded"} - res = requests.post("https://accounts.spotify.com/api/token", data=data, headers=headers) - tokens = res.json() + tokens = {...} - if "access_token" not in tokens: - return jsonify({"error": "Spotify token exchange failed", "details": tokens}), 400 + if frontend == "mobile": + redirect_uri = "areaapp://auth/spotify/callback" + else: + redirect_uri = "http://localhost:5173/spotify/callback" - # Redirige vers ton FRONT avec les tokens dans le hash fragment (sécurisé) - front_url = os.getenv("FRONT_REDIRECT_URI") - redirect_url = f"{front_url}#access_token={tokens['access_token']}&refresh_token={tokens.get('refresh_token','')}" + redirect_url = ( + f"{redirect_uri}?access_token={tokens['access_token']}" + f"&refresh_token={tokens.get('refresh_token','')}" + ) return redirect(redirect_url) + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4f26637 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,22 @@ +from dotenv import load_dotenv +import os +import pytest + +# Cherche le .env à la racine du projet ou dans /server +for path in [".env", "server/.env"]: + env_path = os.path.join(os.path.dirname(__file__), "..", path) + env_path = os.path.abspath(env_path) + if os.path.exists(env_path): + load_dotenv(dotenv_path=env_path) + break + +from server.app import create_app + +@pytest.fixture +def client(): + app = create_app(mode="test") + app.config["TESTING"] = True + + with app.test_client() as client: + yield client + diff --git a/tests/test_about.py b/tests/test_about.py new file mode 100644 index 0000000..915a50d --- /dev/null +++ b/tests/test_about.py @@ -0,0 +1,14 @@ +def test_about_json(client): + """Test the /about.json endpoint returns correct JSON structure.""" + + response = client.get("/about.json") + assert response.status_code == 200 + + data = response.get_json() + + assert "server" in data + assert "client" in data + + assert "services" in data["server"] + assert isinstance(data["server"]["services"], list) + assert any(s["name"] == "spotify" for s in data["server"]["services"])