diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b942f99..5c135f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,12 +14,11 @@ repos: hooks: - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.4 hooks: - - id: flake8 - args: ["--config=setup.cfg"] - additional_dependencies: [flake8-isort] + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.6.1 # Use the sha / tag you want to point at diff --git a/openapi.yaml b/openapi.yaml index b6268a2..dcec110 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3,52 +3,56 @@ info: title: Case Law Privileged API version: 0.1.2 servers: -- url: "https://{environment}.caselaw.nationalarchives.gov.uk/v1" - variables: - environment: - default: api - enum: - - api - - api.staging + - url: "https://{environment}.caselaw.nationalarchives.gov.uk/v1" + variables: + environment: + default: api + enum: + - api + - api.staging tags: -- description: Verify the operational state of the API - name: Status -- description: Operations for reading document content and metadata - name: Reading -- description: Operations for writing document content and metadata - name: Writing + - description: Verify the operational state of the API + name: Status + - description: Operations for reading document content and metadata + name: Reading + - description: Operations for writing document content and metadata + name: Writing paths: /status: get: - description: "A test endpoint that can be used by clients to verify service\ + description: + "A test endpoint that can be used by clients to verify service\ \ availability, and to verify valid authentication credentials.\nAuthentication\ \ is not required, but if it is provided, it will be checked for validity.\n" responses: "200": - description: "The service is available, and if authentication was provided,\ + description: + "The service is available, and if authentication was provided,\ \ the authentication is valid." "401": - description: "The service is available, but the provided authentication\ + description: + "The service is available, but the provided authentication\ \ was not valid." security: - - {} - - basic: [] + - {} + - basic: [] summary: Health check tags: - - Status + - Status /{judgmentUri}: get: - description: "Unless the client has `read_unpublished_documents` permission,\ + description: + "Unless the client has `read_unpublished_documents` permission,\ \ then only published documents are accessible." operationId: getDocumentByUri parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple responses: "200": description: "A single judgment document, in Akoma Ntoso XML" @@ -61,7 +65,8 @@ paths: type: string style: simple X-Lock-State: - description: Included if the client has the `write_documents` role; + description: + Included if the client has the `write_documents` role; specifies if the document is currently locked for editing. example: true explode: false @@ -69,32 +74,33 @@ paths: type: boolean style: simple security: - - basic: - - read_documents - - read_unpublished_documents + - basic: + - read_documents + - read_unpublished_documents summary: "Read a judgment or decision, given its URI" tags: - - Reading + - Reading put: - description: "Write a complete new version of the document to the database,\ + description: + "Write a complete new version of the document to the database,\ \ and release any client lock." parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple - - description: The last known version number of the document - example: "1" - explode: false - in: header - name: If-Match - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple + - description: The last known version number of the document + example: "1" + explode: false + in: header + name: If-Match + required: true + schema: + type: string + style: simple responses: "204": description: The document was updated successfully and any client lock released @@ -107,54 +113,52 @@ paths: style: simple "400": description: "The request was malformed, and the document was not modified" - "412": - description: "The document was not updated, as it has changed since the\ - \ version number specified If-Match. To avoid this, the client should\ - \ lock the document before making any changes to it." security: - - basic: - - write_documents + - basic: + - write_documents summary: Update a judgment tags: - - Writing + - Writing /{judgmentUri}/lock: get: parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple responses: "204": description: Lock state included in header headers: X-Lock-State: - description: Included if the client has edit permissions; specifies + description: + Included if the client has edit permissions; specifies if the document is currently locked for editing. explode: false schema: type: boolean style: simple security: - - basic: - - write_documents + - basic: + - write_documents summary: Query lock status for a document tags: - - Writing + - Writing put: - description: "Locks edit access for a document for the current client. Returns\ + description: + "Locks edit access for a document for the current client. Returns\ \ the latest version of the locked document, alohg with the new lock state." parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple responses: "201": description: "A single judgment document, in Akoma Ntoso XML" @@ -167,7 +171,8 @@ paths: type: string style: simple X-Lock-State: - description: Included if the client has the `write_documents` role; + description: + Included if the client has the `write_documents` role; specifies if the document is currently locked for editing. example: true explode: false @@ -178,58 +183,60 @@ paths: description: The document was already locked by another client headers: X-Lock-State: - description: Included if the client has edit permissions; specifies + description: + Included if the client has edit permissions; specifies if the document is currently locked for editing. explode: false schema: type: boolean style: simple security: - - basic: - - write_documents + - basic: + - write_documents summary: Lock access to a document tags: - - Writing + - Writing /{judgmentUri}/metadata: get: - description: "Unless the client has `read_unpublished_documents` permission,\ + description: + "Unless the client has `read_unpublished_documents` permission,\ \ then only metadata for published documents are accessible." parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple responses: "200": description: OK security: - - basic: - - read_documents - - read_unpublished_documents + - basic: + - read_documents + - read_unpublished_documents summary: Gets the document's metadata tags: - - Reading + - Reading patch: parameters: - - explode: false - in: path - name: judgmentUri - required: true - schema: - type: string - style: simple + - explode: false + in: path + name: judgmentUri + required: true + schema: + type: string + style: simple responses: "200": description: OK security: - - basic: - - write_documents + - basic: + - write_documents summary: Set document properties tags: - - Writing + - Writing components: parameters: judgmentUri: @@ -262,7 +269,8 @@ components: type: string style: simple X-Lock-State: - description: Included if the client has the `write_documents` role; specifies + description: + Included if the client has the `write_documents` role; specifies if the document is currently locked for editing. example: true explode: false diff --git a/pyproject.toml b/pyproject.toml index 97af1f8..520cec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,5 @@ - +[project] +requires-python = ">=3.12, <4" [tool.black] line-length = 88 @@ -19,13 +20,12 @@ exclude = ''' ) ''' -[tool.isort] -profile = "black" -skip = [ - '.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.tox', - '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv', -] -skip_gitignore = true +[tool.ruff] +ignore = ["S101", "E501", "G004", "PLR2004", "RUF005", "RUF012"] +extend-select = ["W", "B", "Q", "C90", "I", "UP", "YTT", "ASYNC", "S", "BLE", "A", "COM", "C4", "DTZ", "T10", "DJ", "EM", "EXE", "FA", + "ISC", "ICN", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SLOT", "SIM", "TID", "TCH", "INT", "PTH", + "FIX", "PGH", "PL", "TRY", "FLY", "PERF", "RUF"] +unfixable = ["ERA"] [tool.poetry] name = "ds-caselaw-privileged-api" diff --git a/src/openapi_server/apis/reading_api.py b/src/openapi_server/apis/reading_api.py index 435991d..0b055b3 100644 --- a/src/openapi_server/apis/reading_api.py +++ b/src/openapi_server/apis/reading_api.py @@ -1,8 +1,8 @@ -# coding: utf-8 +from typing import Any import lxml.etree -from typing import Dict, List, Any # noqa: F401 from caselawclient.models.documents import DocumentURIString +from caselawclient.search_parameters import SearchParameters from fastapi import ( # noqa: F401 APIRouter, Body, @@ -12,20 +12,21 @@ Header, Path, Query, - Response, Request, + Response, Security, status, ) -from openapi_server.models.extra_models import TokenModel # noqa: F401 -from openapi_server.security_api import get_token_basic -from caselawclient.search_parameters import SearchParameters -from openapi_server.connect import client_for_basic_auth - from requests_toolbelt.multipart import decoder +from openapi_server.connect import client_for_basic_auth +from openapi_server.models.extra_models import TokenModel +from openapi_server.security_api import get_token_basic + from .utils import error_handling +SECURITY_TOKEN_MODEL = Security(get_token_basic) + router = APIRouter() @@ -42,8 +43,7 @@ def unpack_list(xpath_list): ), f"There should only be one response, but there were {len(xpath_list)}: \n {xpath_list}" if xpath_list: return xpath_list[0] - else: - return None + return None @router.get( @@ -59,15 +59,16 @@ def unpack_list(xpath_list): async def get_document_by_uri( response: Response, judgmentUri: DocumentURIString, - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, ): with error_handling(): client = client_for_basic_auth(token_basic) can_view_unpublished = client.user_can_view_unpublished_judgments( - token_basic.username + token_basic.username, ) judgment = client.get_judgment_xml( - judgmentUri, show_unpublished=can_view_unpublished + judgmentUri, + show_unpublished=can_view_unpublished, ) return Response(status_code=200, content=judgment, media_type="application/xml") @@ -84,7 +85,7 @@ async def get_document_by_uri( async def list_unpublished_get_get( request: Request, response: Response, - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, page: int = 1, # should not be 0 ) -> Any: """Unless the client has `read_unpublished_documents` permission, @@ -99,7 +100,7 @@ async def list_unpublished_get_get( page=page, show_unpublished=True, only_unpublished=True, - ) + ), ) xml = decode_multipart_response(response) @@ -108,7 +109,7 @@ async def list_unpublished_get_get( if "application/xml" in content_type: return Response(status_code=200, content=xml, media_type="application/xml") - root = lxml.etree.fromstring(xml) + root = lxml.etree.fromstring(xml) # noqa: S320 namespaces = { "search": "http://marklogic.com/appservices/search", "uk": "https://caselaw.nationalarchives.gov.uk/akn", @@ -123,14 +124,15 @@ async def list_unpublished_get_get( data["uri"] = data["raw_uri"].partition(".xml")[0] data["date"] = unpack_list( result.xpath( - ".//akn:FRBRdate[@name='judgment']/@date", namespaces=namespaces - ) + ".//akn:FRBRdate[@name='judgment']/@date", + namespaces=namespaces, + ), ) data["name"] = unpack_list( - result.xpath(".//akn:FRBRname/@value", namespaces=namespaces) + result.xpath(".//akn:FRBRname/@value", namespaces=namespaces), ) data["neutral"] = unpack_list( - result.xpath(".//uk:cite/text()", namespaces=namespaces) + result.xpath(".//uk:cite/text()", namespaces=namespaces), ) results.append(data) diff --git a/src/openapi_server/apis/status_api.py b/src/openapi_server/apis/status_api.py index b537846..742903d 100644 --- a/src/openapi_server/apis/status_api.py +++ b/src/openapi_server/apis/status_api.py @@ -1,9 +1,6 @@ -# coding: utf-8 - -from typing import Dict, List # noqa: F401 +import contextlib from caselawclient.Client import MarklogicUnauthorizedError - from fastapi import ( # noqa: F401 APIRouter, Body, @@ -19,12 +16,15 @@ status, ) from fastapi.security import HTTPBasicCredentials + from openapi_server.connect import client_for_basic_auth -from openapi_server.models.extra_models import TokenModel # noqa: F401 +from openapi_server.models.extra_models import TokenModel from openapi_server.security_api import get_token_basic from .utils import error_handling +SECURITY_TOKEN_MODEL = Security(get_token_basic) + router = APIRouter() @@ -37,14 +37,12 @@ summary="Health check", response_model_by_alias=True, ) -async def healthcheck_get() -> Dict[str, str]: +async def healthcheck_get() -> dict[str, str]: """A test endpoint that checks Marklogic is present""" client = client_for_basic_auth(HTTPBasicCredentials(username="", password="")) - with error_handling(): - try: - client.user_can_view_unpublished_judgments("") - except MarklogicUnauthorizedError: # expected error - pass + with error_handling(), contextlib.suppress(MarklogicUnauthorizedError): + # MarklogicUnauthorizedError is an expected error + client.user_can_view_unpublished_judgments("") return {"status": "/healthcheck: Marklogic OK"} @@ -53,10 +51,10 @@ async def healthcheck_get() -> Dict[str, str]: responses={ 200: { "description": """The service is available, and if authentication was provided, the authentication is valid. - X-Read-Unpublished will be 1 if the user can read unpublished, 0 otherwise""" + X-Read-Unpublished will be 1 if the user can read unpublished, 0 otherwise""", }, 401: { - "description": "The service is available, but the provided authentication was not valid." + "description": "The service is available, but the provided authentication was not valid.", }, }, tags=["Status"], @@ -65,8 +63,8 @@ async def healthcheck_get() -> Dict[str, str]: ) async def status_get( response: Response, - token_basic: TokenModel = Security(get_token_basic), -) -> Dict[str, str]: + token_basic: TokenModel = SECURITY_TOKEN_MODEL, +) -> dict[str, str]: """A test endpoint that can be used by clients to verify service availability, and to verify valid authentication credentials. Authentication is not required, but if it is provided, it will be checked for validity.""" @@ -83,5 +81,5 @@ async def status_get( can_cannot = f"can{'not' if not view_unpublished else ''}" return { - "status": f"/status: {username} Authorised, and {can_cannot} view unpublished judgments" + "status": f"/status: {username} Authorised, and {can_cannot} view unpublished judgments", } diff --git a/src/openapi_server/apis/utils.py b/src/openapi_server/apis/utils.py index 5c499df..f4f7116 100644 --- a/src/openapi_server/apis/utils.py +++ b/src/openapi_server/apis/utils.py @@ -1,8 +1,9 @@ -from fastapi import HTTPException +import logging from contextlib import contextmanager -from caselawclient.Client import MarklogicValidationFailedError, MarklogicAPIError + import lxml.etree -import logging +from caselawclient.Client import MarklogicAPIError, MarklogicValidationFailedError +from fastapi import HTTPException @contextmanager @@ -10,8 +11,8 @@ def error_handling(): try: yield - except Exception as e: - print(f"EXCEPTION {e}") + except Exception as e: # noqa: BLE001 + print(f"EXCEPTION {e}") # noqa: T201 return error_response(e) @@ -19,19 +20,20 @@ def error_response(e): """provide a uniform error Response""" logging.warning(e) if isinstance(e, MarklogicValidationFailedError): - root = lxml.etree.fromstring(e.response.content) + root = lxml.etree.fromstring(e.response.content) # noqa: S320 error_message = root.xpath( "//mlerror:message/text()", namespaces={"mlerror": "http://marklogic.com/xdmp/error"}, )[0] raise HTTPException(status_code=e.status_code, detail=error_message) - elif isinstance(e, MarklogicAPIError): + + if isinstance(e, MarklogicAPIError): raise HTTPException(status_code=e.status_code, detail=e.default_message) - else: - # presumably a Python error, not a Marklogic one - logging.exception( - "A Python error in the privileged API occurred whilst making a request to Marklogic" - ) - raise HTTPException( - status_code=500, detail="An unknown error occurred outside of Marklogic." - ) + + logging.exception( + "A Python error in the privileged API occurred whilst making a request to Marklogic", + ) + raise HTTPException( + status_code=500, + detail="An unknown error occurred outside of Marklogic.", + ) diff --git a/src/openapi_server/apis/writing_api.py b/src/openapi_server/apis/writing_api.py index 8340937..4743f69 100644 --- a/src/openapi_server/apis/writing_api.py +++ b/src/openapi_server/apis/writing_api.py @@ -1,9 +1,5 @@ -# coding: utf-8 - -from typing import Dict, List, Optional # noqa: F401 - -from caselawclient.models.documents import DocumentURIString from caselawclient.client_helpers import VersionAnnotation, VersionType +from caselawclient.models.documents import DocumentURIString from fastapi import ( # noqa: F401 APIRouter, Body, @@ -11,19 +7,22 @@ Depends, Form, Header, - Request, Path, Query, + Request, Response, Security, status, ) + from openapi_server.connect import client_for_basic_auth from openapi_server.models.extra_models import TokenModel from openapi_server.security_api import get_token_basic from .utils import error_handling +SECURITY_TOKEN_MODEL = Security(get_token_basic) + router = APIRouter() @@ -52,7 +51,7 @@ async def judgment_uri_lock_get( response: Response, judgmentUri: DocumentURIString, - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, ): client = client_for_basic_auth(token_basic) with error_handling(): @@ -84,7 +83,7 @@ async def judgment_uri_lock_get( async def judgment_uri_lock_put( response: Response, judgmentUri: DocumentURIString, - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, expires="0", ): """Locks edit access for a document for the current client. Returns the latest @@ -92,11 +91,13 @@ async def judgment_uri_lock_put( client = client_for_basic_auth(token_basic) annotation = f"Judgment locked for editing by {token_basic.username}" expires = bool( - int(expires) + int(expires), ) # If expires is True then the lock will expire at midnight, otherwise the lock is permanent with error_handling(): - _ml_response = client.checkout_judgment( # noqa: F841 - judgmentUri, annotation, expires + _ml_response = client.checkout_judgment( + judgmentUri, + annotation, + expires, ) judgment = client.get_judgment_xml(judgmentUri, show_unpublished=True) return Response(status_code=201, content=judgment, media_type="application/xml") @@ -115,7 +116,7 @@ async def judgment_uri_lock_put( async def judgment_uri_lock_delete( response: Response, judgmentUri: DocumentURIString, - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, ): client = client_for_basic_auth(token_basic) @@ -130,32 +131,24 @@ async def judgment_uri_lock_delete( "/judgment/{judgmentUri:path}", responses={ 200: { - "description": "The document was updated successfully and the lock released if `unlock` is true" + "description": "The document was updated successfully and the lock released if `unlock` is true", }, 400: { - "description": "The request was malformed, and the document was not modified" - }, - 412: { - "description": """Not yet implemented: The document was not updated, as it has changed since - the version number specified If-Match. To avoid this, the client should - lock the document before making any changes to it.""" + "description": "The request was malformed, and the document was not modified", }, }, tags=["Writing"], summary="Update a judgment", response_model_by_alias=True, ) -async def judgment_uri_patch( +async def judgment_uri_patch( # noqa: PLR0913 request: Request, response: Response, judgmentUri: DocumentURIString, annotation: str = "", - if_match: str = Header( - None, description="The last known version number of the document" - ), - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, unlock: bool = False, -) -> Dict[str, str]: +) -> dict[str, str]: """Write a complete new version of the document to the database, and release any client lock.""" @@ -180,7 +173,7 @@ async def judgment_uri_patch( return {"status": "Uploaded (not unlocked)."} with error_handling(): - _ml_response = client.checkin_judgment(judgment_uri=judgmentUri) # noqa: F841 + _ml_response = client.checkin_judgment(judgment_uri=judgmentUri) response.status_code = 200 return {"status": "Uploaded and unlocked."} diff --git a/src/openapi_server/connect.py b/src/openapi_server/connect.py index 2bbe45a..1fc9e26 100644 --- a/src/openapi_server/connect.py +++ b/src/openapi_server/connect.py @@ -1,18 +1,19 @@ import os import environ -from caselawclient.Client import MarklogicApiClient, DEFAULT_USER_AGENT - +from caselawclient.Client import DEFAULT_USER_AGENT, MarklogicApiClient from fastapi import Security -from openapi_server.models.extra_models import TokenModel # noqa: F401 + +from openapi_server.models.extra_models import TokenModel from openapi_server.security_api import get_token_basic -environ.Env.read_env("../.env") # TODO this is hideous +environ.Env.read_env("../.env") MARKLOGIC_HOST = os.environ["MARKLOGIC_API_CLIENT_HOST"] +SECURITY_TOKEN_MODEL = Security(get_token_basic) def client_for_basic_auth( - token_basic: TokenModel = Security(get_token_basic), + token_basic: TokenModel = SECURITY_TOKEN_MODEL, ) -> MarklogicApiClient: return MarklogicApiClient( host=MARKLOGIC_HOST, diff --git a/src/openapi_server/main.py b/src/openapi_server/main.py index 443107e..df651cc 100644 --- a/src/openapi_server/main.py +++ b/src/openapi_server/main.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """ Case Law Privileged API @@ -11,6 +9,7 @@ from fastapi import FastAPI, Request + from openapi_server.apis.reading_api import router as ReadingApiRouter from openapi_server.apis.status_api import router as StatusApiRouter from openapi_server.apis.writing_api import router as WritingApiRouter diff --git a/src/openapi_server/models/extra_models.py b/src/openapi_server/models/extra_models.py index f0588d2..4c64cf1 100644 --- a/src/openapi_server/models/extra_models.py +++ b/src/openapi_server/models/extra_models.py @@ -1,5 +1,3 @@ -# coding: utf-8 - from pydantic import BaseModel diff --git a/src/openapi_server/security_api.py b/src/openapi_server/security_api.py index 6fa8f87..1eb7345 100644 --- a/src/openapi_server/security_api.py +++ b/src/openapi_server/security_api.py @@ -1,4 +1,3 @@ -# coding: utf-8 from fastapi import Depends, Security # noqa: F401 from fastapi.openapi.models import OAuthFlowImplicit, OAuthFlows # noqa: F401 from fastapi.security import ( # noqa: F401 @@ -16,13 +15,14 @@ APIKeyHeader, APIKeyQuery, ) + from openapi_server.models.extra_models import TokenModel -basic_auth = HTTPBasic() +basic_credentials_auth = Depends(HTTPBasic()) def get_token_basic( - credentials: HTTPBasicCredentials = Depends(basic_auth), + credentials: HTTPBasicCredentials = basic_credentials_auth, ) -> TokenModel: """ Check and retrieve authentication information from basic auth. 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 index 312d38b..628ecb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,16 @@ import pytest - from fastapi import FastAPI from fastapi.testclient import TestClient from openapi_server.main import app as application -@pytest.fixture +@pytest.fixture() def app() -> FastAPI: application.dependency_overrides = {} return application -@pytest.fixture +@pytest.fixture() def client(app) -> TestClient: return TestClient(app) diff --git a/tests/test_reading_api.py b/tests/test_reading_api.py index 826f98c..afee8c2 100644 --- a/tests/test_reading_api.py +++ b/tests/test_reading_api.py @@ -1,18 +1,17 @@ -# coding: utf-8 -from unittest.mock import patch, Mock -from unittest import TestCase +from unittest.mock import Mock, patch +import pytest from caselawclient.Client import MarklogicResourceNotFoundError from caselawclient.search_parameters import SearchParameters from fastapi.testclient import TestClient -from openapi_server.main import app from openapi_server.apis.reading_api import unpack_list +from openapi_server.main import app def test_unpack_list(): assert unpack_list([1]) == 1 assert unpack_list([]) is None - with TestCase().assertRaises(AssertionError): + with pytest.raises(AssertionError): unpack_list([1, 1]) @@ -26,7 +25,8 @@ def test_get_success(mocked_client): mocked_client.return_value.user_can_view_unpublished_judgments.return_value = True response = TestClient(app).request("GET", "/judgment/uri", auth=("user", "pass")) mocked_client.return_value.get_judgment_xml.assert_called_with( - "uri", show_unpublished=True + "uri", + show_unpublished=True, ) assert response.status_code == 200 assert "" in response.text @@ -41,21 +41,25 @@ def test_passes_unpublished_if_true(mocked_client): mocked_client.return_value.user_can_view_unpublished_judgments.return_value = False _ = TestClient(app).request("GET", "/judgment/uri", auth=("user", "pass")) mocked_client.return_value.get_judgment_xml.assert_called_with( - "uri", show_unpublished=False + "uri", + show_unpublished=False, ) @patch("openapi_server.apis.reading_api.client_for_basic_auth") def test_get_not_found(mocked_client): mocked_client.return_value.get_judgment_xml.side_effect = Mock( - side_effect=MarklogicResourceNotFoundError() + side_effect=MarklogicResourceNotFoundError(), ) mocked_client.return_value.user_can_view_unpublished_judgments.return_value = True response = TestClient(app).request( - "GET", "/judgment/bad_uri", auth=("user", "pass") + "GET", + "/judgment/bad_uri", + auth=("user", "pass"), ) mocked_client.return_value.get_judgment_xml.assert_called_with( - "bad_uri", show_unpublished=True + "bad_uri", + show_unpublished=True, ) assert response.status_code == 404 assert "No resource with that name" in response.text @@ -65,12 +69,14 @@ def test_get_not_found(mocked_client): def test_get_list_unpublished_bad_auth(mocked_client): mocked_client.return_value.user_can_view_unpublished_judgments.return_value = False response = TestClient(app).request( - "GET", "/list/unpublished", auth=("user", "pass") + "GET", + "/list/unpublished", + auth=("user", "pass"), ) assert response.status_code == 403 assert "Not allowed" in response.text mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) @@ -80,22 +86,24 @@ def test_get_list_unpublished(mocked_client): advanced_search = mocked_client.return_value.advanced_search.return_value advanced_search.text = "true" advanced_search.headers = { - "content-type": "multipart/mixed; boundary=6bfe89fc4493c0e3" + "content-type": "multipart/mixed; boundary=6bfe89fc4493c0e3", } - advanced_search.content = b'\r\n--6bfe89fc4493c0e3\r\nContent-Type: application/xml\r\nX-Primitive: element()\r\nX-Path: /*:response\r\n\r\n\n \n \n [2000] EWHC 1\n ' # noqa: E501 + advanced_search.content = b'\r\n--6bfe89fc4493c0e3\r\nContent-Type: application/xml\r\nX-Primitive: element()\r\nX-Path: /*:response\r\n\r\n\n \n \n [2000] EWHC 1\n ' response = TestClient(app).request( - "GET", "/list/unpublished", auth=("user", "pass") + "GET", + "/list/unpublished", + auth=("user", "pass"), ) mocked_client.return_value.advanced_search.assert_called_with( SearchParameters( page=1, show_unpublished=True, only_unpublished=True, - ) + ), ) mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) assert response.status_code == 200 assert "ok" in response.text @@ -113,9 +121,9 @@ def test_get_list_unpublished_xml(mocked_client): advanced_search = mocked_client.return_value.advanced_search.return_value advanced_search.text = "true" advanced_search.headers = { - "content-type": "multipart/mixed; boundary=6bfe89fc4493c0e3" + "content-type": "multipart/mixed; boundary=6bfe89fc4493c0e3", } - advanced_search.content = b"\r\n--6bfe89fc4493c0e3\r\nContent-Type: application/xml\r\nX-Primitive: element()\r\nX-Path: /*:response\r\n\r\n" # noqa: E501 + advanced_search.content = b"\r\n--6bfe89fc4493c0e3\r\nContent-Type: application/xml\r\nX-Primitive: element()\r\nX-Path: /*:response\r\n\r\n" response = TestClient(app).request( "GET", @@ -128,11 +136,11 @@ def test_get_list_unpublished_xml(mocked_client): page=6, show_unpublished=True, only_unpublished=True, - ) + ), ) mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) assert response.status_code == 200 assert "whatever" in response.text - assert "application/xml" == response.headers.get("Content-Type") + assert response.headers.get("Content-Type") == "application/xml" diff --git a/tests/test_status_api.py b/tests/test_status_api.py index ecfd7fa..ff956b9 100644 --- a/tests/test_status_api.py +++ b/tests/test_status_api.py @@ -1,9 +1,8 @@ -# coding: utf-8 +from unittest.mock import Mock, patch -from unittest.mock import patch, Mock +from caselawclient.Client import MarklogicUnauthorizedError from fastapi.testclient import TestClient from openapi_server.main import app -from caselawclient.Client import MarklogicUnauthorizedError def test_get_status_no_auth(): @@ -14,7 +13,7 @@ def test_get_status_no_auth(): @patch("openapi_server.apis.status_api.client_for_basic_auth") def test_get_status_no_such_user(mocked_client=None): mocked_client.return_value.user_can_view_unpublished_judgments.side_effect = Mock( - side_effect=MarklogicUnauthorizedError() + side_effect=MarklogicUnauthorizedError(), ) response = TestClient(app).request("GET", "/status", auth=("user", "pass")) assert response.status_code == 401 @@ -23,9 +22,8 @@ def test_get_status_no_such_user(mocked_client=None): == b'{"detail":"Your credentials are not valid, or you did not provide any by basic authentication"}' ) mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) - # TODO: This will break when only_published becomes silently false. @patch("openapi_server.apis.status_api.client_for_basic_auth") @@ -37,7 +35,7 @@ def test_get_status_authorised(mocked_client): assert "/status: user Authorised, and can view" in response.text assert response.headers["X-Read-Unpublished"] == "1" mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) @@ -50,19 +48,19 @@ def test_get_status_less_authorised(mocked_client): assert "/status: user Authorised, and cannot view" in response.text assert response.headers["X-Read-Unpublished"] == "0" mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "user" + "user", ) @patch("openapi_server.apis.status_api.client_for_basic_auth") def test_healthcheck(mocked_client): mocked_client.return_value.user_can_view_unpublished_judgments.side_effect = Mock( - side_effect=MarklogicUnauthorizedError() + side_effect=MarklogicUnauthorizedError(), ) response = TestClient(app).request("GET", "/healthcheck") assert response.status_code == 200 assert "/healthcheck: Marklogic OK" in response.text mocked_client.return_value.user_can_view_unpublished_judgments.assert_called_with( - "" + "", ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9f9865e..d87a87e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,12 @@ -from openapi_server.apis.utils import error_handling -from fastapi.exceptions import HTTPException +from unittest.mock import Mock + +import pytest from caselawclient.Client import ( MarklogicUnauthorizedError, MarklogicValidationFailedError, ) -from unittest.mock import Mock -import pytest +from fastapi.exceptions import HTTPException +from openapi_server.apis.utils import error_handling def test_error_handling_no_exception(): @@ -28,7 +29,7 @@ def test_error_handling_python_error(caplog): def example(): with error_handling(): - 1 / 0 + 1 / 0 # noqa: B018 with pytest.raises(HTTPException) as ex: example() @@ -52,7 +53,7 @@ def test_validation_error(caplog): def example(): e = MarklogicValidationFailedError("error_msg") e.response = Mock() - e.response.content = b'a message from marklogic' # noqa:E501 + e.response.content = b'a message from marklogic' with error_handling(): raise e @@ -79,7 +80,7 @@ def test_non_validation_error(caplog): def example(): e = MarklogicUnauthorizedError("error_msg") e.response = Mock() - e.response.content = b'a message from marklogic' # noqa:E501 + e.response.content = b'a message from marklogic' with error_handling(): raise e diff --git a/tests/test_writing_api.py b/tests/test_writing_api.py index c97e582..9ff1bbb 100644 --- a/tests/test_writing_api.py +++ b/tests/test_writing_api.py @@ -1,14 +1,14 @@ -# coding: utf-8 -from fastapi.testclient import TestClient -from openapi_server.main import app -from unittest.mock import patch, Mock, ANY +from unittest.mock import ANY, Mock, patch + from caselawclient.Client import ( - MarklogicUnauthorizedError, - MarklogicResourceLockedError, MarklogicCheckoutConflictError, + MarklogicResourceLockedError, MarklogicResourceNotCheckedOutError, + MarklogicUnauthorizedError, MarklogicValidationFailedError, ) +from fastapi.testclient import TestClient +from openapi_server.main import app # Read Lock Status (GET /lock/...) @@ -17,10 +17,12 @@ def test_get_lock_no_response(mocked_client): mocked_client.return_value.get_judgment_checkout_status_message.return_value = None response = TestClient(app).request( - "GET", "/lock/judgment/uri", auth=("user", "pass") + "GET", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.get_judgment_checkout_status_message.assert_called_with( - "judgment/uri" + "judgment/uri", ) assert response.status_code == 200 assert response.headers["X-Locked"] == "0" @@ -33,11 +35,13 @@ def test_get_lock_locked(mocked_client): "kitten" ) response = TestClient(app).request( - "GET", "/lock/judgment/uri", auth=("user", "pass") + "GET", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.get_judgment_checkout_status_message.assert_called_with( - "judgment/uri" + "judgment/uri", ) assert response.status_code == 200 assert response.headers["X-Locked"] == "1" @@ -49,13 +53,15 @@ def test_get_lock_locked(mocked_client): def test_get_lock_unauthorised(mocked_client): # More generally testing errors are passed through from the client mocked_client.return_value.get_judgment_checkout_status_message.side_effect = Mock( - side_effect=MarklogicUnauthorizedError() + side_effect=MarklogicUnauthorizedError(), ) response = TestClient(app).request( - "GET", "/lock/judgment/uri", auth=("user", "pass") + "GET", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.get_judgment_checkout_status_message.assert_called_with( - "judgment/uri" + "judgment/uri", ) assert response.status_code == 401 assert "credentials are not valid" in response.text @@ -70,10 +76,14 @@ def test_put_lock_success(mocked_client): mocked_client.return_value.checkout_judgment.return_value = None mocked_client.return_value.get_judgment_xml.return_value = b"" response = TestClient(app).request( - "PUT", "/lock/judgment/uri", auth=("user", "pass") + "PUT", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.checkout_judgment.assert_called_with( - "judgment/uri", "Judgment locked for editing by user", False + "judgment/uri", + "Judgment locked for editing by user", + False, ) assert response.status_code == 201 assert "" in response.text @@ -85,10 +95,15 @@ def test_put_lock_success_temporary(mocked_client): mocked_client.return_value.checkout_judgment.return_value = None mocked_client.return_value.get_judgment_xml.return_value = b"" response = TestClient(app).request( - "PUT", "/lock/judgment/uri", auth=("user", "pass"), params={"expires": "1"} + "PUT", + "/lock/judgment/uri", + auth=("user", "pass"), + params={"expires": "1"}, ) mocked_client.return_value.checkout_judgment.assert_called_with( - "judgment/uri", "Judgment locked for editing by user", True + "judgment/uri", + "Judgment locked for editing by user", + True, ) assert response.status_code == 201 assert "" in response.text @@ -98,14 +113,18 @@ def test_put_lock_success_temporary(mocked_client): def test_put_lock_failure(mocked_client): # Checkout Judgment fails with an error or returns None mocked_client.return_value.checkout_judgment.side_effect = Mock( - side_effect=MarklogicResourceLockedError() + side_effect=MarklogicResourceLockedError(), ) # mocked_client.return_value.get_judgment_xml.return_value = b'' response = TestClient(app).request( - "PUT", "/lock/judgment/uri", auth=("user", "pass") + "PUT", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.checkout_judgment.assert_called_with( - "judgment/uri", "Judgment locked for editing by user", False + "judgment/uri", + "Judgment locked for editing by user", + False, ) assert response.status_code == 409 assert "resource is locked by another user" in response.text @@ -119,10 +138,12 @@ def test_delete_lock_success(mocked_client): # Checkout Judgment fails with an error or returns None mocked_client.return_value.checkin_judgment.return_value = None response = TestClient(app).request( - "DELETE", "/lock/judgment/uri", auth=("user", "pass") + "DELETE", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.checkin_judgment.assert_called_with( - judgment_uri="judgment/uri" + judgment_uri="judgment/uri", ) assert response.status_code == 200 assert "unlocked" in response.text @@ -132,13 +153,15 @@ def test_delete_lock_success(mocked_client): def test_delete_lock_failure(mocked_client): # Checkout Judgment fails with an error or returns None mocked_client.return_value.checkin_judgment.side_effect = Mock( - side_effect=MarklogicCheckoutConflictError() + side_effect=MarklogicCheckoutConflictError(), ) response = TestClient(app).request( - "DELETE", "/lock/judgment/uri", auth=("user", "pass") + "DELETE", + "/lock/judgment/uri", + auth=("user", "pass"), ) mocked_client.return_value.checkin_judgment.assert_called_with( - judgment_uri="judgment/uri" + judgment_uri="judgment/uri", ) assert response.status_code == 409 assert "checked out by another user" in response.text @@ -210,7 +233,7 @@ def test_default_message_in_api_response(mocked_client): """ mocked_client.return_value.save_locked_judgment_xml.side_effect = Mock( - side_effect=MarklogicResourceNotCheckedOutError() + side_effect=MarklogicResourceNotCheckedOutError(), ) response = TestClient(app).request( "PATCH", @@ -246,9 +269,9 @@ def test_validation_error_message_in_api_response(mocked_client): """ error = MarklogicValidationFailedError() error.response = Mock() - error.response.content = b'a message' # noqa:E501 + error.response.content = b'a message' mocked_client.return_value.save_locked_judgment_xml.side_effect = Mock( - side_effect=error + side_effect=error, ) response = TestClient(app).request( "PATCH",