chore: update 3 api (#35481)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Asuka Minato 2026-04-22 17:53:53 +09:00 committed by GitHub
parent ba924fc97b
commit 8b1533438f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 313 additions and 59 deletions

View File

@ -1,3 +1,11 @@
"""Console workspace endpoint controllers.
This module exposes workspace-scoped plugin endpoint management APIs. The
canonical write routes follow resource-oriented paths, while the historical
verb-based aliases stay available as deprecated resources so OpenAPI metadata
marks only the legacy paths as deprecated.
"""
from typing import Any from typing import Any
from flask import request from flask import request
@ -25,7 +33,12 @@ class EndpointIdPayload(BaseModel):
endpoint_id: str endpoint_id: str
class EndpointUpdatePayload(EndpointIdPayload): class EndpointUpdatePayload(BaseModel):
settings: dict[str, Any]
name: str = Field(min_length=1)
class LegacyEndpointUpdatePayload(EndpointIdPayload):
settings: dict[str, Any] settings: dict[str, Any]
name: str = Field(min_length=1) name: str = Field(min_length=1)
@ -76,6 +89,7 @@ register_schema_models(
EndpointCreatePayload, EndpointCreatePayload,
EndpointIdPayload, EndpointIdPayload,
EndpointUpdatePayload, EndpointUpdatePayload,
LegacyEndpointUpdatePayload,
EndpointListQuery, EndpointListQuery,
EndpointListForPluginQuery, EndpointListForPluginQuery,
EndpointCreateResponse, EndpointCreateResponse,
@ -88,8 +102,60 @@ register_schema_models(
) )
@console_ns.route("/workspaces/current/endpoints/create") def _create_endpoint() -> dict[str, bool]:
class EndpointCreateApi(Resource): """Create a plugin endpoint for the current workspace."""
user, tenant_id = current_account_with_tenant()
args = EndpointCreatePayload.model_validate(console_ns.payload)
try:
return {
"success": EndpointService.create_endpoint(
tenant_id=tenant_id,
user_id=user.id,
plugin_unique_identifier=args.plugin_unique_identifier,
name=args.name,
settings=args.settings,
)
}
except PluginPermissionDeniedError as e:
raise ValueError(e.description) from e
def _update_endpoint(endpoint_id: str) -> dict[str, bool]:
"""Update a plugin endpoint identified by the canonical path parameter."""
user, tenant_id = current_account_with_tenant()
args = EndpointUpdatePayload.model_validate(console_ns.payload)
return {
"success": EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user.id,
endpoint_id=endpoint_id,
name=args.name,
settings=args.settings,
)
}
def _delete_endpoint(endpoint_id: str) -> dict[str, bool]:
"""Delete a plugin endpoint identified by the canonical path parameter."""
user, tenant_id = current_account_with_tenant()
return {
"success": EndpointService.delete_endpoint(
tenant_id=tenant_id,
user_id=user.id,
endpoint_id=endpoint_id,
)
}
@console_ns.route("/workspaces/current/endpoints")
class EndpointCollectionApi(Resource):
"""Canonical collection resource for endpoint creation."""
@console_ns.doc("create_endpoint") @console_ns.doc("create_endpoint")
@console_ns.doc(description="Create a new plugin endpoint") @console_ns.doc(description="Create a new plugin endpoint")
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__]) @console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@ -104,22 +170,33 @@ class EndpointCreateApi(Resource):
@is_admin_or_owner_required @is_admin_or_owner_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
user, tenant_id = current_account_with_tenant() return _create_endpoint()
args = EndpointCreatePayload.model_validate(console_ns.payload)
try: @console_ns.route("/workspaces/current/endpoints/create")
return { class DeprecatedEndpointCreateApi(Resource):
"success": EndpointService.create_endpoint( """Deprecated verb-based alias for endpoint creation."""
tenant_id=tenant_id,
user_id=user.id, @console_ns.doc("create_endpoint_deprecated")
plugin_unique_identifier=args.plugin_unique_identifier, @console_ns.doc(deprecated=True)
name=args.name, @console_ns.doc(
settings=args.settings, description=(
) "Deprecated legacy alias for creating a plugin endpoint. Use POST /workspaces/current/endpoints instead."
} )
except PluginPermissionDeniedError as e: )
raise ValueError(e.description) from e @console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@console_ns.response(
200,
"Endpoint created successfully",
console_ns.models[EndpointCreateResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@account_initialization_required
def post(self):
return _create_endpoint()
@console_ns.route("/workspaces/current/endpoints/list") @console_ns.route("/workspaces/current/endpoints/list")
@ -190,10 +267,56 @@ class EndpointListForSinglePluginApi(Resource):
) )
@console_ns.route("/workspaces/current/endpoints/delete") @console_ns.route("/workspaces/current/endpoints/<string:id>")
class EndpointDeleteApi(Resource): class EndpointItemApi(Resource):
"""Canonical item resource for endpoint updates and deletion."""
@console_ns.doc("delete_endpoint") @console_ns.doc("delete_endpoint")
@console_ns.doc(description="Delete a plugin endpoint") @console_ns.doc(description="Delete a plugin endpoint")
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
"Endpoint deleted successfully",
console_ns.models[EndpointDeleteResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@account_initialization_required
def delete(self, id: str):
return _delete_endpoint(endpoint_id=id)
@console_ns.doc("update_endpoint")
@console_ns.doc(description="Update a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointUpdatePayload.__name__])
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
"Endpoint updated successfully",
console_ns.models[EndpointUpdateResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@account_initialization_required
def patch(self, id: str):
return _update_endpoint(endpoint_id=id)
@console_ns.route("/workspaces/current/endpoints/delete")
class DeprecatedEndpointDeleteApi(Resource):
"""Deprecated verb-based alias for endpoint deletion."""
@console_ns.doc("delete_endpoint_deprecated")
@console_ns.doc(deprecated=True)
@console_ns.doc(
description=(
"Deprecated legacy alias for deleting a plugin endpoint. "
"Use DELETE /workspaces/current/endpoints/{id} instead."
)
)
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__]) @console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response( @console_ns.response(
200, 200,
@ -206,22 +329,23 @@ class EndpointDeleteApi(Resource):
@is_admin_or_owner_required @is_admin_or_owner_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
user, tenant_id = current_account_with_tenant()
args = EndpointIdPayload.model_validate(console_ns.payload) args = EndpointIdPayload.model_validate(console_ns.payload)
return _delete_endpoint(endpoint_id=args.endpoint_id)
return {
"success": EndpointService.delete_endpoint(
tenant_id=tenant_id, user_id=user.id, endpoint_id=args.endpoint_id
)
}
@console_ns.route("/workspaces/current/endpoints/update") @console_ns.route("/workspaces/current/endpoints/update")
class EndpointUpdateApi(Resource): class DeprecatedEndpointUpdateApi(Resource):
@console_ns.doc("update_endpoint") """Deprecated verb-based alias for endpoint updates."""
@console_ns.doc(description="Update a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointUpdatePayload.__name__]) @console_ns.doc("update_endpoint_deprecated")
@console_ns.doc(deprecated=True)
@console_ns.doc(
description=(
"Deprecated legacy alias for updating a plugin endpoint. "
"Use PATCH /workspaces/current/endpoints/{id} instead."
)
)
@console_ns.expect(console_ns.models[LegacyEndpointUpdatePayload.__name__])
@console_ns.response( @console_ns.response(
200, 200,
"Endpoint updated successfully", "Endpoint updated successfully",
@ -233,19 +357,8 @@ class EndpointUpdateApi(Resource):
@is_admin_or_owner_required @is_admin_or_owner_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
user, tenant_id = current_account_with_tenant() args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
return _update_endpoint(endpoint_id=args.endpoint_id)
args = EndpointUpdatePayload.model_validate(console_ns.payload)
return {
"success": EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user.id,
endpoint_id=args.endpoint_id,
name=args.name,
settings=args.settings,
)
}
@console_ns.route("/workspaces/current/endpoints/enable") @console_ns.route("/workspaces/current/endpoints/enable")

View File

@ -2,14 +2,17 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from controllers.console import console_ns
from controllers.console.workspace.endpoint import ( from controllers.console.workspace.endpoint import (
EndpointCreateApi, DeprecatedEndpointCreateApi,
EndpointDeleteApi, DeprecatedEndpointDeleteApi,
DeprecatedEndpointUpdateApi,
EndpointCollectionApi,
EndpointDisableApi, EndpointDisableApi,
EndpointEnableApi, EndpointEnableApi,
EndpointItemApi,
EndpointListApi, EndpointListApi,
EndpointListForSinglePluginApi, EndpointListForSinglePluginApi,
EndpointUpdateApi,
) )
from core.plugin.impl.exc import PluginPermissionDeniedError from core.plugin.impl.exc import PluginPermissionDeniedError
@ -35,9 +38,9 @@ def patch_current_account(user_and_tenant):
@pytest.mark.usefixtures("patch_current_account") @pytest.mark.usefixtures("patch_current_account")
class TestEndpointCreateApi: class TestEndpointCollectionApi:
def test_create_success(self, app): def test_create_success(self, app):
api = EndpointCreateApi() api = EndpointCollectionApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = { payload = {
@ -55,7 +58,7 @@ class TestEndpointCreateApi:
assert result["success"] is True assert result["success"] is True
def test_create_permission_denied(self, app): def test_create_permission_denied(self, app):
api = EndpointCreateApi() api = EndpointCollectionApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = { payload = {
@ -75,7 +78,7 @@ class TestEndpointCreateApi:
method(api) method(api)
def test_create_validation_error(self, app): def test_create_validation_error(self, app):
api = EndpointCreateApi() api = EndpointCollectionApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = { payload = {
@ -91,6 +94,27 @@ class TestEndpointCreateApi:
method(api) method(api)
@pytest.mark.usefixtures("patch_current_account")
class TestDeprecatedEndpointCreateApi:
def test_create_success(self, app):
api = DeprecatedEndpointCreateApi()
method = unwrap(api.post)
payload = {
"plugin_unique_identifier": "plugin-1",
"name": "endpoint",
"settings": {"a": 1},
}
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.endpoint.EndpointService.create_endpoint", return_value=True),
):
result = method(api)
assert result["success"] is True
@pytest.mark.usefixtures("patch_current_account") @pytest.mark.usefixtures("patch_current_account")
class TestEndpointListApi: class TestEndpointListApi:
def test_list_success(self, app): def test_list_success(self, app):
@ -146,9 +170,96 @@ class TestEndpointListForSinglePluginApi:
@pytest.mark.usefixtures("patch_current_account") @pytest.mark.usefixtures("patch_current_account")
class TestEndpointDeleteApi: class TestEndpointItemApi:
def test_delete_success(self, app): def test_delete_success(self, app):
api = EndpointDeleteApi() api = EndpointItemApi()
method = unwrap(api.delete)
with (
app.test_request_context("/", method="DELETE"),
patch(
"controllers.console.workspace.endpoint.EndpointService.delete_endpoint",
return_value=True,
) as mock_delete,
):
result = method(api, "e1")
assert result["success"] is True
mock_delete.assert_called_once_with(tenant_id="t1", user_id="u1", endpoint_id="e1")
def test_delete_service_failure(self, app):
api = EndpointItemApi()
method = unwrap(api.delete)
with (
app.test_request_context("/", method="DELETE"),
patch("controllers.console.workspace.endpoint.EndpointService.delete_endpoint", return_value=False),
):
result = method(api, "e1")
assert result["success"] is False
def test_update_success(self, app):
api = EndpointItemApi()
method = unwrap(api.patch)
payload = {
"name": "new-name",
"settings": {"x": 1},
}
with (
app.test_request_context("/", method="PATCH", json=payload),
patch(
"controllers.console.workspace.endpoint.EndpointService.update_endpoint",
return_value=True,
) as mock_update,
):
result = method(api, "e1")
assert result["success"] is True
mock_update.assert_called_once_with(
tenant_id="t1",
user_id="u1",
endpoint_id="e1",
name="new-name",
settings={"x": 1},
)
def test_update_validation_error(self, app):
api = EndpointItemApi()
method = unwrap(api.patch)
payload = {"settings": {}}
with (
app.test_request_context("/", method="PATCH", json=payload),
):
with pytest.raises(ValueError):
method(api, "e1")
def test_update_service_failure(self, app):
api = EndpointItemApi()
method = unwrap(api.patch)
payload = {
"name": "n",
"settings": {},
}
with (
app.test_request_context("/", method="PATCH", json=payload),
patch("controllers.console.workspace.endpoint.EndpointService.update_endpoint", return_value=False),
):
result = method(api, "e1")
assert result["success"] is False
@pytest.mark.usefixtures("patch_current_account")
class TestDeprecatedEndpointDeleteApi:
def test_delete_success(self, app):
api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = {"endpoint_id": "e1"} payload = {"endpoint_id": "e1"}
@ -162,7 +273,7 @@ class TestEndpointDeleteApi:
assert result["success"] is True assert result["success"] is True
def test_delete_invalid_payload(self, app): def test_delete_invalid_payload(self, app):
api = EndpointDeleteApi() api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post) method = unwrap(api.post)
with ( with (
@ -172,7 +283,7 @@ class TestEndpointDeleteApi:
method(api) method(api)
def test_delete_service_failure(self, app): def test_delete_service_failure(self, app):
api = EndpointDeleteApi() api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = {"endpoint_id": "e1"} payload = {"endpoint_id": "e1"}
@ -187,9 +298,9 @@ class TestEndpointDeleteApi:
@pytest.mark.usefixtures("patch_current_account") @pytest.mark.usefixtures("patch_current_account")
class TestEndpointUpdateApi: class TestDeprecatedEndpointUpdateApi:
def test_update_success(self, app): def test_update_success(self, app):
api = EndpointUpdateApi() api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = { payload = {
@ -207,7 +318,7 @@ class TestEndpointUpdateApi:
assert result["success"] is True assert result["success"] is True
def test_update_validation_error(self, app): def test_update_validation_error(self, app):
api = EndpointUpdateApi() api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = {"endpoint_id": "e1", "settings": {}} payload = {"endpoint_id": "e1", "settings": {}}
@ -219,7 +330,7 @@ class TestEndpointUpdateApi:
method(api) method(api)
def test_update_service_failure(self, app): def test_update_service_failure(self, app):
api = EndpointUpdateApi() api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post) method = unwrap(api.post)
payload = { payload = {
@ -237,6 +348,36 @@ class TestEndpointUpdateApi:
assert result["success"] is False assert result["success"] is False
class TestEndpointRouteMetadata:
def test_legacy_write_routes_are_marked_deprecated(self):
assert DeprecatedEndpointCreateApi.post.__apidoc__["deprecated"] is True
assert DeprecatedEndpointDeleteApi.post.__apidoc__["deprecated"] is True
assert DeprecatedEndpointUpdateApi.post.__apidoc__["deprecated"] is True
assert EndpointCollectionApi.post.__apidoc__.get("deprecated") is not True
assert EndpointItemApi.delete.__apidoc__.get("deprecated") is not True
assert EndpointItemApi.patch.__apidoc__.get("deprecated") is not True
def test_canonical_and_legacy_write_routes_are_registered(self):
route_map = {
resource.__name__: urls
for resource, urls, _route_doc, _kwargs in console_ns.resources
if resource.__name__
in {
"EndpointCollectionApi",
"EndpointItemApi",
"DeprecatedEndpointCreateApi",
"DeprecatedEndpointDeleteApi",
"DeprecatedEndpointUpdateApi",
}
}
assert route_map["EndpointCollectionApi"] == ("/workspaces/current/endpoints",)
assert route_map["EndpointItemApi"] == ("/workspaces/current/endpoints/<string:id>",)
assert route_map["DeprecatedEndpointCreateApi"] == ("/workspaces/current/endpoints/create",)
assert route_map["DeprecatedEndpointDeleteApi"] == ("/workspaces/current/endpoints/delete",)
assert route_map["DeprecatedEndpointUpdateApi"] == ("/workspaces/current/endpoints/update",)
@pytest.mark.usefixtures("patch_current_account") @pytest.mark.usefixtures("patch_current_account")
class TestEndpointEnableApi: class TestEndpointEnableApi:
def test_enable_success(self, app): def test_enable_success(self, app):