import secrets from flask import Flask, jsonify from sqlalchemy import create_engine from app.db import Base, create_session_factory from app.security.auth import require_auth from app.security.policy import Role from app.services.idempotency import IdempotencyService, _request_fingerprint_payload, idempotent from app.services.identity import IdentityRepository, PasswordService from app.utils.api_errors import ApiError from app.utils.locale import t def make_app(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) session_factory = create_session_factory(engine) app = Flask(__name__) app.config.update(TESTING=True, SESSION_COOKIE_SECURE=False) app.config["SECRET_KEY"] = secrets.token_hex(32) app.extensions["crowdsight_session_factory"] = session_factory @app.errorhandler(ApiError) def handle_api_error(error): return {**error.to_payload(t)}, error.status_code @app.post("/mutate") @require_auth @idempotent def mutate(): return jsonify({"success": True, "data": {"value": "created"}}), 201 @app.post("/mutate-alt") @require_auth @idempotent def mutate_alt(): return jsonify({"success": True, "data": {"value": "alternate"}}), 201 with session_factory() as session: repo = IdentityRepository(session) org = repo.create_organization(name="Org A", slug="idempotent-org") user = repo.create_user( email="idempotent@example.com", password_hash=PasswordService.hash_password("correct horse battery staple"), ) repo.create_membership(user.id, org.id, Role.ADMIN) session.commit() return app, engine def login(client): response = client.post( "/mutate", json={"value": "created"}, headers={"Idempotency-Key": "mutation-1"}, ) return response def test_idempotent_route_replays_completed_response(): app, engine = make_app() try: client = app.test_client() auth_app = Flask(__name__) # Login through the real blueprint in a small shared app is covered separately; # seed the opaque cookie via the auth endpoint mounted on this test app. from app.api.auth import auth_bp app.register_blueprint(auth_bp, url_prefix="/api/auth") login_response = client.post( "/api/auth/login", json={"email": "idempotent@example.com", "password": "correct horse battery staple"}, ) assert login_response.status_code == 200 csrf = client.get_cookie("crowdsight_csrf").value headers = {"Idempotency-Key": "mutation-1", "X-CSRF-Token": csrf} first = client.post("/mutate", json={"value": "created"}, headers=headers) second = client.post("/mutate", json={"value": "created"}, headers=headers) assert first.status_code == second.status_code == 201 assert first.get_json() == second.get_json() finally: engine.dispose() def test_idempotent_route_rejects_same_key_for_different_body(): app, engine = make_app() try: client = app.test_client() from app.api.auth import auth_bp app.register_blueprint(auth_bp, url_prefix="/api/auth") assert client.post( "/api/auth/login", json={"email": "idempotent@example.com", "password": "correct horse battery staple"}, ).status_code == 200 csrf = client.get_cookie("crowdsight_csrf").value headers = {"Idempotency-Key": "mutation-2", "X-CSRF-Token": csrf} assert client.post("/mutate", json={"value": "a"}, headers=headers).status_code == 201 conflict = client.post("/mutate", json={"value": "b"}, headers=headers) assert conflict.status_code == 409 assert conflict.get_json()["error_code"] == "idempotency_key_reused" finally: engine.dispose() def test_multipart_file_content_affects_fingerprint(): app = Flask(__name__) boundary = b"FixedBoundary" def fingerprint(content): body = ( b"--" + boundary + b"\r\n" b'Content-Disposition: form-data; name="simulation_requirement"\r\n\r\n' b"req\r\n" b"--" + boundary + b"\r\n" b'Content-Disposition: form-data; name="files"; filename="doc.txt"\r\n' b"Content-Type: text/plain\r\n\r\n" + content + b"\r\n--" + boundary + b"--\r\n" ) with app.test_request_context( "/upload", method="POST", data=body, content_type="multipart/form-data; boundary=FixedBoundary", ): payload = _request_fingerprint_payload() return IdempotencyService._request_hash(payload) assert fingerprint(b"alpha") != fingerprint(b"bravo") def test_all_multipart_files_affect_fingerprint(): app = Flask(__name__) boundary = b"FixedBoundary" def fingerprint(second_content): first_part = ( b'Content-Disposition: form-data; name="files"; filename="first.txt"\r\n' b"Content-Type: text/plain\r\n\r\nalpha" ) second_part = ( b'Content-Disposition: form-data; name="files"; filename="second.txt"\r\n' b"Content-Type: text/plain\r\n\r\n" + second_content ) body = ( b"--" + boundary + b"\r\n" + first_part + b"\r\n--" + boundary + b"\r\n" + second_part + b"\r\n--" + boundary + b"--\r\n" ) with app.test_request_context( "/upload", method="POST", data=body, content_type="multipart/form-data; boundary=FixedBoundary", ): return IdempotencyService._request_hash(_request_fingerprint_payload()) assert fingerprint(b"bravo") != fingerprint(b"charl") def test_idempotency_key_cannot_replay_across_routes(): app, engine = make_app() try: client = app.test_client() from app.api.auth import auth_bp app.register_blueprint(auth_bp, url_prefix="/api/auth") assert client.post( "/api/auth/login", json={"email": "idempotent@example.com", "password": "correct horse battery staple"}, ).status_code == 200 csrf = client.get_cookie("crowdsight_csrf").value headers = {"Idempotency-Key": "cross-route", "X-CSRF-Token": csrf} assert client.post("/mutate", json={"value": "created"}, headers=headers).status_code == 201 conflict = client.post("/mutate-alt", json={"value": "created"}, headers=headers) assert conflict.status_code == 409 assert conflict.get_json()["error_code"] == "idempotency_key_reused" finally: engine.dispose()