diff --git a/api/controllers/openapi/__init__.py b/api/controllers/openapi/__init__.py new file mode 100644 index 0000000000..d94e9e0140 --- /dev/null +++ b/api/controllers/openapi/__init__.py @@ -0,0 +1,23 @@ +from flask import Blueprint +from flask_restx import Namespace + +from libs.external_api import ExternalApi + +bp = Blueprint("openapi", __name__, url_prefix="/openapi/v1") + +api = ExternalApi( + bp, + version="1.0", + title="OpenAPI", + description="User-scoped programmatic API (bearer auth)", +) + +openapi_ns = Namespace("openapi", description="User-scoped operations", path="/") + +from . import index + +__all__ = [ + "index", +] + +api.add_namespace(openapi_ns) diff --git a/api/controllers/openapi/index.py b/api/controllers/openapi/index.py new file mode 100644 index 0000000000..a6626f9cc6 --- /dev/null +++ b/api/controllers/openapi/index.py @@ -0,0 +1,9 @@ +from flask_restx import Resource + +from controllers.openapi import openapi_ns + + +@openapi_ns.route("/_health") +class HealthApi(Resource): + def get(self): + return {"ok": True} diff --git a/api/extensions/ext_blueprints.py b/api/extensions/ext_blueprints.py index b8da817cc3..690ee1fccf 100644 --- a/api/extensions/ext_blueprints.py +++ b/api/extensions/ext_blueprints.py @@ -29,6 +29,7 @@ def init_app(app: DifyApp): from controllers.files import bp as files_bp from controllers.inner_api import bp as inner_api_bp from controllers.mcp import bp as mcp_bp + from controllers.openapi import bp as openapi_bp from controllers.service_api import bp as service_api_bp from controllers.trigger import bp as trigger_bp from controllers.web import bp as web_bp @@ -41,6 +42,9 @@ def init_app(app: DifyApp): ) app.register_blueprint(service_api_bp) + # User-scoped programmatic API. CORS configured in Phase A step 5. + app.register_blueprint(openapi_bp) + _apply_cors_once( web_bp, resources={ diff --git a/api/tests/unit_tests/controllers/openapi/__init__.py b/api/tests/unit_tests/controllers/openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/controllers/openapi/test_health.py b/api/tests/unit_tests/controllers/openapi/test_health.py new file mode 100644 index 0000000000..f59e4d9a97 --- /dev/null +++ b/api/tests/unit_tests/controllers/openapi/test_health.py @@ -0,0 +1,33 @@ +import builtins + +import pytest +from flask import Flask +from flask.views import MethodView + +from controllers.openapi import bp as openapi_bp + +if not hasattr(builtins, "MethodView"): + builtins.MethodView = MethodView # type: ignore[attr-defined] + + +@pytest.fixture +def app() -> Flask: + app = Flask(__name__) + app.config["TESTING"] = True + app.register_blueprint(openapi_bp) + return app + + +def test_health_returns_ok(app: Flask): + client = app.test_client() + response = client.get("/openapi/v1/_health") + + assert response.status_code == 200 + assert response.get_json() == {"ok": True} + + +def test_health_path_is_under_openapi_v1_prefix(app: Flask): + client = app.test_client() + assert client.get("/_health").status_code == 404 + assert client.get("/v1/_health").status_code == 404 + assert client.get("/openapi/v1/_health").status_code == 200