diff --git a/pyproject.toml b/pyproject.toml index 9bb9b8e173..cf16ab0bb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,6 @@ module = [ "ldap.*", "moto.*", # https://github.com/spulec/moto/issues/4944 "onelogin.*", - "openapi_core.*", "premailer.*", "pyinotify.*", "pyoembed.*", diff --git a/requirements/common.in b/requirements/common.in index 9a889853e1..1091cc5dc3 100644 --- a/requirements/common.in +++ b/requirements/common.in @@ -161,9 +161,8 @@ requests[security] requests-oauthlib # For OpenAPI schema validation. -openapi-core +https://github.com/p1c2u/openapi-core/archive/ac64879418096c437ad5de9bf7a8c7498959ab87.zip#egg=openapi-core==0.16.0+git # https://github.com/p1c2u/openapi-core/pull/429 openapi-schema-validator -importlib-resources ; python_version < "3.9" # for jsonschema # For reporting errors to sentry.io sentry-sdk diff --git a/requirements/dev.txt b/requirements/dev.txt index 0e9f06cf7c..cac913f1c3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -69,7 +69,7 @@ attrs==21.4.0 \ # automat # glom # jsonschema - # openapi-core + # openapi-schema-validator # semgrep # service-identity # twisted @@ -441,11 +441,6 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via redis -dictpath==0.1.3 \ - --hash=sha256:225248e3c1e7c375495d5da5c390cbf3490f56ee42c151df733e5b2df6b521b5 \ - --hash=sha256:751cde3b76b176d25f961b90c423a11a4d5ede9bd09ab0d64a85abb738c190d8 \ - --hash=sha256:d5212361d1fb93909cff715f6e0404e17752cf7a48df3e140639e529a027c437 - # via openapi-core disposable-email-domains==0.0.80 \ --hash=sha256:97342c8c496ec913168345ed9966fdc9a2ff014f347e7238b776e30059bdf486 \ --hash=sha256:c65f1924126dd5a4b27b037e2125f6dc7e357e35732a50f7261355b94631054e @@ -677,12 +672,12 @@ importlib-metadata==4.12.0 ; python_version < "3.10" \ # markdown # sphinx # zulip-bots -importlib-resources==5.9.0 ; python_version < "3.9" \ +importlib-resources==5.9.0 \ --hash=sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681 \ --hash=sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7 # via - # -r requirements/common.in # jsonschema + # openapi-spec-validator incremental==21.3.0 \ --hash=sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57 \ --hash=sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321 @@ -749,9 +744,16 @@ jsonschema==4.14.0 \ --hash=sha256:15062f4cc6f591400cd528d2c355f2cfa6a57e44c820dc783aee5e23d36a831f \ --hash=sha256:9892b8d630a82990521a9ca630d3446bd316b5ad54dbe981338802787f3e0d2d # via + # jsonschema-spec # openapi-schema-validator # openapi-spec-validator # semgrep +jsonschema-spec==0.1.2 \ + --hash=sha256:1e525177574c23ae0f55cd62382632a083a0339928f0ca846a975a4da9851cec \ + --hash=sha256:780a22d517cdc857d9714a80d8349c546945063f20853ea32ba7f85bc643ec7d + # via + # openapi-core + # openapi-spec-validator jsx-lexer==2.0.0 \ --hash=sha256:bff51c2a2faa2c682cbc9a0f360b8c65e4153eb1df06988e8dad34373d3f9995 \ --hash=sha256:ca22483ced80a92e45fa1855da7cf99309852c0637842a79a759e10ea57b904d @@ -794,7 +796,7 @@ lazy-object-proxy==1.7.1 \ --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \ --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \ --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb - # via openapi-core + # via openapi-spec-validator libcst==0.4.7 \ --hash=sha256:0ca2771ff3cfdf1f148349f89fcae64afa365213ed5c2703a69a89319325d0c8 \ --hash=sha256:1e541ccfeebda1ae5f005fc120a5bf3e8ac9ccfda405ec3efd3df54fc4688ac3 \ @@ -1107,21 +1109,19 @@ oauthlib==3.2.0 \ # via # requests-oauthlib # social-auth-core -openapi-core==0.14.2 \ - --hash=sha256:05e100cfb25b4c68d2f647b3f204da26630d79df02d5e0aacae2bcc1915a2796 \ - --hash=sha256:3426b5ae551a04f7d7a3a625ca600bff157affb4eb691d36412997f6a9ac6898 \ - --hash=sha256:62ad93c8114ce6025f25b004ff0f3674eea8bc4ae920c726e98921fdbe41b4f3 +https://github.com/p1c2u/openapi-core/archive/ac64879418096c437ad5de9bf7a8c7498959ab87.zip#egg=openapi-core==0.16.0+git \ + --hash=sha256:225605fc34214987652f3d7f2f8a23dce91b887833394700abc23ce5e8bded7c # via -r requirements/common.in -openapi-schema-validator==0.2.3 \ - --hash=sha256:2c64907728c3ef78e23711c8840a423f0b241588c9ed929855e4b2d1bb0cf5f2 \ - --hash=sha256:9bae709212a19222892cabcc60cafd903cbf4b220223f48583afa3c0e3cc6fc4 +openapi-schema-validator==0.3.4 \ + --hash=sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a \ + --hash=sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd # via # -r requirements/common.in # openapi-core # openapi-spec-validator -openapi-spec-validator==0.4.0 \ - --hash=sha256:06900ac4d546a1df3642a779da0055be58869c598e3042a2fef067cfd99d04d0 \ - --hash=sha256:97f258850afc97b048f7c2653855e0f88fa66ac103c2be5077c7960aca2ad49a +openapi-spec-validator==0.5.1 \ + --hash=sha256:4a8aee1e45b1ac868e07ab25e18828fe9837baddd29a8e20fdb3d3c61c8eea3d \ + --hash=sha256:8248634bad1f23cac5d5a34e193ab36e23914057ca69e91a1ede5af75552c465 # via openapi-core orjson==3.8.0 \ --hash=sha256:02d638d43951ba346a80f0abd5942a872cc87db443e073f6f6fc530fee81e19b \ @@ -1188,6 +1188,12 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi +pathable==0.4.3 \ + --hash=sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab \ + --hash=sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14 + # via + # jsonschema-spec + # openapi-core pathspec==0.9.0 \ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 @@ -1675,6 +1681,7 @@ pyyaml==6.0 \ --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 # via + # jsonschema-spec # libcst # moto # myst-parser @@ -1884,7 +1891,6 @@ six==1.16.0 \ # ecdsa # html5lib # isodate - # openapi-core # parsel # protego # python-binary-memcached @@ -2222,6 +2228,7 @@ typing-extensions==4.3.0 \ # boto3-stubs # django-stubs # django-stubs-ext + # jsonschema-spec # libcst # mypy # mypy-boto3-s3 @@ -2229,6 +2236,7 @@ typing-extensions==4.3.0 \ # mypy-boto3-sns # mypy-boto3-sqs # myst-parser + # openapi-core # pyre-check # pyre-extensions # semgrep @@ -2526,7 +2534,6 @@ setuptools==65.3.0 \ # via # -r requirements/pip.in # ipython - # openapi-spec-validator # pip-tools # scrapy # zope.interface diff --git a/requirements/prod.txt b/requirements/prod.txt index 4258871fac..d24d59ce23 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -55,7 +55,7 @@ attrs==21.4.0 \ --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd # via # jsonschema - # openapi-core + # openapi-schema-validator backcall==0.2.0 \ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 @@ -279,11 +279,6 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via redis -dictpath==0.1.3 \ - --hash=sha256:225248e3c1e7c375495d5da5c390cbf3490f56ee42c151df733e5b2df6b521b5 \ - --hash=sha256:751cde3b76b176d25f961b90c423a11a4d5ede9bd09ab0d64a85abb738c190d8 \ - --hash=sha256:d5212361d1fb93909cff715f6e0404e17752cf7a48df3e140639e529a027c437 - # via openapi-core disposable-email-domains==0.0.80 \ --hash=sha256:97342c8c496ec913168345ed9966fdc9a2ff014f347e7238b776e30059bdf486 \ --hash=sha256:c65f1924126dd5a4b27b037e2125f6dc7e357e35732a50f7261355b94631054e @@ -469,12 +464,12 @@ importlib-metadata==4.12.0 ; python_version < "3.10" \ # -r requirements/common.in # markdown # zulip-bots -importlib-resources==5.9.0 ; python_version < "3.9" \ +importlib-resources==5.9.0 \ --hash=sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681 \ --hash=sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7 # via - # -r requirements/common.in # jsonschema + # openapi-spec-validator ipython==8.4.0 \ --hash=sha256:7ca74052a38fa25fe9bedf52da0be7d3fdd2fb027c3b778ea78dfe8c212937d1 \ --hash=sha256:f2db3a10254241d9b447232cec8b424847f338d9d36f9a577a6192c332a46abd @@ -511,8 +506,15 @@ jsonschema==4.14.0 \ --hash=sha256:15062f4cc6f591400cd528d2c355f2cfa6a57e44c820dc783aee5e23d36a831f \ --hash=sha256:9892b8d630a82990521a9ca630d3446bd316b5ad54dbe981338802787f3e0d2d # via + # jsonschema-spec # openapi-schema-validator # openapi-spec-validator +jsonschema-spec==0.1.2 \ + --hash=sha256:1e525177574c23ae0f55cd62382632a083a0339928f0ca846a975a4da9851cec \ + --hash=sha256:780a22d517cdc857d9714a80d8349c546945063f20853ea32ba7f85bc643ec7d + # via + # openapi-core + # openapi-spec-validator jsx-lexer==2.0.0 \ --hash=sha256:bff51c2a2faa2c682cbc9a0f360b8c65e4153eb1df06988e8dad34373d3f9995 \ --hash=sha256:ca22483ced80a92e45fa1855da7cf99309852c0637842a79a759e10ea57b904d @@ -555,7 +557,7 @@ lazy-object-proxy==1.7.1 \ --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \ --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \ --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb - # via openapi-core + # via openapi-spec-validator lxml==4.6.5 \ --hash=sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59 \ --hash=sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3 \ @@ -706,21 +708,19 @@ oauthlib==3.2.0 \ # via # requests-oauthlib # social-auth-core -openapi-core==0.14.2 \ - --hash=sha256:05e100cfb25b4c68d2f647b3f204da26630d79df02d5e0aacae2bcc1915a2796 \ - --hash=sha256:3426b5ae551a04f7d7a3a625ca600bff157affb4eb691d36412997f6a9ac6898 \ - --hash=sha256:62ad93c8114ce6025f25b004ff0f3674eea8bc4ae920c726e98921fdbe41b4f3 +https://github.com/p1c2u/openapi-core/archive/ac64879418096c437ad5de9bf7a8c7498959ab87.zip#egg=openapi-core==0.16.0+git \ + --hash=sha256:225605fc34214987652f3d7f2f8a23dce91b887833394700abc23ce5e8bded7c # via -r requirements/common.in -openapi-schema-validator==0.2.3 \ - --hash=sha256:2c64907728c3ef78e23711c8840a423f0b241588c9ed929855e4b2d1bb0cf5f2 \ - --hash=sha256:9bae709212a19222892cabcc60cafd903cbf4b220223f48583afa3c0e3cc6fc4 +openapi-schema-validator==0.3.4 \ + --hash=sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a \ + --hash=sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd # via # -r requirements/common.in # openapi-core # openapi-spec-validator -openapi-spec-validator==0.4.0 \ - --hash=sha256:06900ac4d546a1df3642a779da0055be58869c598e3042a2fef067cfd99d04d0 \ - --hash=sha256:97f258850afc97b048f7c2653855e0f88fa66ac103c2be5077c7960aca2ad49a +openapi-spec-validator==0.5.1 \ + --hash=sha256:4a8aee1e45b1ac868e07ab25e18828fe9837baddd29a8e20fdb3d3c61c8eea3d \ + --hash=sha256:8248634bad1f23cac5d5a34e193ab36e23914057ca69e91a1ede5af75552c465 # via openapi-core orjson==3.8.0 \ --hash=sha256:02d638d43951ba346a80f0abd5942a872cc87db443e073f6f6fc530fee81e19b \ @@ -777,6 +777,12 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi +pathable==0.4.3 \ + --hash=sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab \ + --hash=sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14 + # via + # jsonschema-spec + # openapi-core pexpect==4.8.0 \ --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c @@ -1148,7 +1154,9 @@ pyyaml==6.0 \ --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 - # via openapi-spec-validator + # via + # jsonschema-spec + # openapi-spec-validator qrcode==7.3.1 \ --hash=sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578 # via django-two-factor-auth @@ -1279,7 +1287,6 @@ six==1.16.0 \ # ecdsa # html5lib # isodate - # openapi-core # python-binary-memcached # python-dateutil # talon-core @@ -1396,10 +1403,12 @@ typing-extensions==4.3.0 \ # via # -r requirements/common.in # django-stubs-ext + # jsonschema-spec # mypy-boto3-s3 # mypy-boto3-ses # mypy-boto3-sns # mypy-boto3-sqs + # openapi-core # zulip # zulip-bots uhashring==2.1 \ @@ -1539,6 +1548,4 @@ pip==20.3.4 \ setuptools==65.3.0 \ --hash=sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82 \ --hash=sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57 - # via - # ipython - # openapi-spec-validator + # via ipython diff --git a/version.py b/version.py index a71df0414a..c34c3c6ebb 100644 --- a/version.py +++ b/version.py @@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 151 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (204, 0) +PROVISION_VERSION = (205, 0) diff --git a/zerver/openapi/openapi.py b/zerver/openapi/openapi.py index 9c4ff81be2..66f3a22fde 100644 --- a/zerver/openapi/openapi.py +++ b/zerver/openapi/openapi.py @@ -12,11 +12,11 @@ from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union import orjson from jsonschema.exceptions import ValidationError as JsonSchemaValidationError -from openapi_core import create_spec +from openapi_core import Spec from openapi_core.testing import MockRequest, MockResponse from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue -from openapi_core.validation.request.validators import RequestValidator -from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.validation.request import openapi_request_validator +from openapi_core.validation.response import openapi_response_validator OPENAPI_SPEC_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "../openapi/zulip.yaml") @@ -79,8 +79,7 @@ class OpenAPISpec: self.mtime: Optional[float] = None self._openapi: Dict[str, Any] = {} self._endpoints_dict: Dict[str, str] = {} - self._request_validator: Optional[RequestValidator] = None - self._response_validator: Optional[ResponseValidator] = None + self._spec: Optional[Spec] = None def check_reload(self) -> None: # Because importing yaml takes significant time, and we only @@ -105,9 +104,8 @@ class OpenAPISpec: openapi = yaml.load(f, Loader=yaml.CSafeLoader) - spec = create_spec(openapi) - self._request_validator = RequestValidator(spec) - self._response_validator = ResponseValidator(spec) + spec = Spec.create(openapi) + self._spec = spec self._openapi = naively_merge_allOf_dict(JsonRef.replace_refs(openapi)) self.create_endpoints_dict() self.mtime = mtime @@ -165,23 +163,14 @@ class OpenAPISpec: assert len(self._endpoints_dict) > 0 return self._endpoints_dict - def request_validator(self) -> RequestValidator: + def spec(self) -> Spec: """Reload the OpenAPI file if it has been modified after the last time it was read, and then return the openapi_core validator object. Similar to preceding functions. Used for proper access to OpenAPI objects. """ self.check_reload() - assert self._request_validator is not None - return self._request_validator - - def response_validator(self) -> RequestValidator: - """Reload the OpenAPI file if it has been modified after the last time - it was read, and then return the openapi_core validator object. Similar - to preceding functions. Used for proper access to OpenAPI objects. - """ - self.check_reload() - assert self._response_validator is not None - return self._response_validator + assert self._spec is not None + return self._spec class SchemaError(Exception): @@ -443,16 +432,17 @@ def validate_against_openapi_schema( mock_request = MockRequest("http://localhost:9991/", method, "/api/v1" + path) mock_response = MockResponse( # TODO: Use original response content instead of re-serializing it. - orjson.dumps(content), - status_code=status_code, + orjson.dumps(content).decode(), + status_code=int(status_code), ) - result = openapi_spec.response_validator().validate(mock_request, mock_response) + result = openapi_response_validator.validate(openapi_spec.spec(), mock_request, mock_response) try: result.raise_for_errors() except InvalidSchemaValue as isv: - message = f"{len(isv.schema_errors)} response validation error(s) at {method} /api/v1{path} ({status_code}):" - for error in isv.schema_errors: - if display_brief_error: + schema_errors = list(isv.schema_errors) + message = f"{len(schema_errors)} response validation error(s) at {method} /api/v1{path} ({status_code}):" + for error in schema_errors: + if display_brief_error and isinstance(error, JsonSchemaValidationError): # display_brief_error is designed to avoid printing 1000 lines # of output when the schema to validate is extremely large # (E.g. the several dozen format variants for individual @@ -557,22 +547,24 @@ def validate_request( # Now using the openapi_core APIs, validate the request schema # against the OpenAPI documentation. + assert isinstance(data, dict) mock_request = MockRequest( "http://localhost:9991/", method, "/api/v1" + url, headers=http_headers, args=data ) - result = openapi_spec.request_validator().validate(mock_request) - if len(result.errors) != 0: - # Requests that do not validate against the OpenAPI spec must either: - # * Have returned a 400 (bad request) error - # * Have returned a 200 (success) with this request marked as intentionally - # undocumented behavior. - if status_code.startswith("4"): - return - if status_code.startswith("2") and intentionally_undocumented: - return + result = openapi_request_validator.validate(openapi_spec.spec(), mock_request) + errors = list(result.errors) # If no errors are raised, then validation is successful - if len(result.errors) == 0: + if not errors: + return + + # Requests that do not validate against the OpenAPI spec must either: + # * Have returned a 400 (bad request) error + # * Have returned a 200 (success) with this request marked as intentionally + # undocumented behavior. + if status_code.startswith("4"): + return + if status_code.startswith("2") and intentionally_undocumented: return # Show a block error message explaining the options for fixing it. @@ -590,6 +582,6 @@ with the parameters passed in this HTTP request. Consider: See https://zulip.readthedocs.io/en/latest/documentation/api.html for help. The errors logged by the OpenAPI validator are below:\n""" - for error in result.errors: + for error in errors: msg += f"* {str(error)}\n" raise SchemaError(msg) diff --git a/zerver/tests/test_openapi.py b/zerver/tests/test_openapi.py index 9e697ba0de..49818a6bb1 100644 --- a/zerver/tests/test_openapi.py +++ b/zerver/tests/test_openapi.py @@ -107,7 +107,7 @@ class OpenAPIToolsTest(ZulipTestCase): bad_content, TEST_ENDPOINT, TEST_METHOD, TEST_RESPONSE_SUCCESS ) - with self.assertRaisesRegex(SchemaError, r"42 is not of type string"): + with self.assertRaisesRegex(SchemaError, r"42 is not of type 'string'"): bad_content = { "msg": 42, "result": "success",