diff --git a/docs/api-reference.rst b/docs/api-reference.rst index 85c8fd9c..76ae08ff 100644 --- a/docs/api-reference.rst +++ b/docs/api-reference.rst @@ -60,7 +60,7 @@ Request and response models Pydantic models for the HTTP layer. These are what generate the OpenAPI document served at ``/api/doc/openapi.json``, so they and the schema endpoint never disagree. -.. automodule:: goodmap.api_models +.. automodule:: goodmap.api.api_models :members: :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 65b878de..39fa526a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,7 +62,10 @@ _ANY_PY_ROLE = "py:.*" nitpick_ignore_regex = [ (_ANY_PY_ROLE, r"ConfigDict|callable"), - (_ANY_PY_ROLE, r"(annotated_types|pymongo)\..*"), + (_ANY_PY_ROLE, r"(annotated_types|pymongo|spectree)\..*"), + # RootModel generics: pydantic's inventory has RootModel but not RootModel[...] + # or RootModelRootType, so the parametrised bases autodoc prints cannot resolve. + (_ANY_PY_ROLE, r"pydantic\.root_model\..*"), (_ANY_PY_ROLE, r"[gl]e=-?\d+"), ( _ANY_PY_ROLE, diff --git a/docs/configuration.rst b/docs/configuration.rst index 27d8c986..4562b0d2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -187,10 +187,10 @@ frontend to decide what to render. Both are set the same way. suggest form has no fields — see the note below. * - ``CATEGORIES_HELP`` - both - - Enables the help-tooltip data in ``/api/categories``, ``/api/categories-full`` and - ``/api/category/``, and makes the frontend render the tooltips. Without it - the ``categories_help`` and ``categories_options_help`` keys in your data are - ignored. See :ref:`data-source-help`. + - Enables the help-tooltip data in ``/api/categories-full``, and makes the frontend + render the tooltips. Without it the ``categories_help`` and + ``categories_options_help`` keys in your data are ignored. See + :ref:`data-source-help`. * - ``USE_SERVER_SIDE_CLUSTERING`` - both - The frontend fetches ``/api/locations-clustered`` instead of ``/api/locations``, diff --git a/docs/deployment.rst b/docs/deployment.rst index 2a474f98..dc474f21 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -122,15 +122,6 @@ incoming reports change it while the app runs. Schema keys (``categories``, ``visible_data``, ``location_obligatory_fields``) are read at startup, so changing them means restarting the app. Point data is re-read per request. -Health checks -------------- - -``GET /api/version`` is cheap and needs no data source, returning -``{"backend": ""}``. Point your load balancer at it. - -For a check that also proves the data source is reachable, use ``GET /api/locations`` — -it touches the backend, though on a large map it is not free. - Upgrading --------- diff --git a/docs/http-api.rst b/docs/http-api.rst index 09d203b9..215558f3 100644 --- a/docs/http-api.rst +++ b/docs/http-api.rst @@ -18,8 +18,17 @@ A running instance also serves its own generated OpenAPI schema: * - ``/api/doc/openapi.json`` - raw OpenAPI document -Use this page for the semantics and the schema endpoint for the exact shapes of the -release you are running. +The schema is generated from the code, so it always describes the release you are +running: every endpoint's parameters, status codes and response shapes. **Use it as the +reference.** This page covers what a schema cannot state — what the endpoints mean and +how they behave. + +**The API surface is the same in every deployment**: same paths, same methods, same +response shapes, same status codes. That part is documented here in full. The *values* +moving through it are not — filters, the fields a point may carry, the issues that can be +reported all come from each deployment's own data source. Those are documented by your +running instance rather than by this page; see +`Deployment-specific: what your instance declares`_. Conventions ----------- @@ -29,8 +38,25 @@ Conventions on the ``categories`` in your data source (:doc:`data-source`). **Writes need a CSRF token.** CSRF protection is on for the whole app, so ``POST``, -``PUT`` and ``DELETE`` without a token get ``400 The CSRF token is missing``. Send it as -an ``X-CSRFToken`` header. Server-rendered pages expose one in a meta tag: +``PUT``, ``PATCH`` and ``DELETE`` without a token get ``400 {"message": "The CSRF token +is missing."}``. Send it as an ``X-CSRFToken`` header, from the same session the token was +minted in. There is no endpoint that issues a token on its own — a script needs to fetch +a page first, the same as a browser does (:ref:`api-csrf-scripted`). + +**Errors are ``{"message": "..."}``**, occasionally with an extra ``error`` field. +Messages are deliberately generic — the offending values go to the server log, not the +response. A rejected query parameter names which one it was, without echoing the value: +``{"message": "Invalid request data", "error": "invalid or out of range: zoom"}``. + +**Strings are translated** to the request's language before being returned, so category +keys and field names come back as display text (:ref:`config-translations`). + +.. _api-csrf-scripted: + +Calling writes from a script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A browser gets the token for free, from a meta tag on every server-rendered page: .. code-block:: html @@ -45,11 +71,27 @@ an ``X-CSRFToken`` header. Server-rendered pages expose one in a meta tag: body: JSON.stringify({ id: locationUuid, description: 'has a hole' }), }); -**Errors are ``{"message": "..."}``**, occasionally with an extra ``error`` field. -Messages are deliberately generic — the details go to the server log, not the response. +A scripted client has no meta tag to read, so it needs both pieces the browser gets for +free: the token, and the session cookie it is bound to. **A bare token is not enough** — +without the matching cookie the request fails with a different error, +``400 {"message": "The CSRF session token is missing."}``. Fetch a page first to get +both, keeping cookies in a jar to reuse on the write: -**Strings are translated** to the request's language before being returned, so category -keys and field names come back as display text (:ref:`config-translations`). +.. code-block:: bash + + JAR=$(mktemp) + TOKEN=$(curl -s -c "$JAR" http://localhost:5000/ | grep -oP 'name="csrf-token" content="\K[^"]+') + curl -X POST http://localhost:5000/api/report-location \ + -b "$JAR" \ + -H "Content-Type: application/json" \ + -H "X-CSRFToken: $TOKEN" \ + -d '{"id": "9264286a-5d33-4e38-ab11-c8e179a7754a", "description": "has a hole"}' + +Over https, a matching ``Referer`` header is required too — same-origin defense in +depth, on top of the token. Browsers send this automatically for a same-origin request, +so it is invisible in normal use; a scripted client (``curl``, a backend job) must set it +explicitly, e.g. ``-H "Referer: https://your-host/"``, or the request gets +``400 {"message": "The referrer header is missing."}``. Reading the map --------------- @@ -75,28 +117,23 @@ Query parameters: - Filter value; repeat for several. Combined per :ref:`categories-filter-mode`. * - ``lat``, ``lon`` - Sort results by distance from this coordinate, nearest first. Both required, or - neither applies. + neither applies. Ranges are the usual **−90..90** and **−180..180**. * - ``limit`` - - Return at most this many points. Applied after sorting, so ``lat``/``lon``/``limit`` - together give "the N nearest". + - Return at most this many points, **1 or more**. Applied after sorting, so + ``lat``/``lon``/``limit`` together give "the N nearest". .. code-block:: bash curl 'http://localhost:5000/api/locations?accessible_by=bikes&lat=51.10&lon=17.05&limit=5' -.. code-block:: json - - [ - { - "uuid": "7c3d5e7f-9a1b-4c3d-8e5f-7a9b1c3d5e7f", - "position": [50.0397, 19.906], - "remark": false - } - ] - -``remark`` is a **boolean** — whether the point has a remark, not the remark itself. +Each point comes back as ``uuid``, ``position`` and ``has_remark`` — a **boolean**, whether +the point has a remark, not its text. -Invalid or unknown query parameters are ignored rather than rejected. +A ``lat``, ``lon`` or ``limit`` that cannot mean anything — not a number, or outside the +range above — is a ``400 {"message": "Invalid request data"}`` rather than a silently +different result. Any *other* parameter is passed through to the filters untouched: the +valid filter names come from your own ``categories`` and cannot be checked against a fixed +list, so an unknown one is simply a filter that matches nothing. ``GET /api/locations-clustered`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -106,29 +143,11 @@ level. This is what the frontend calls instead of ``/api/locations`` when ``USE_SERVER_SIDE_CLUSTERING`` is on. Takes every parameter of :ref:`api-locations`, plus ``zoom`` (integer, **0–16**, default -``7``). A ``zoom`` outside that range is a ``400``. - -.. code-block:: json - - [ - { - "type": "cluster", - "position": [50.1026, 19.8240], - "uuid": null, - "cluster_uuid": "34515392-7913-47be-a5b4-0c4b5247ad4c", - "cluster_count": 2 - }, - { - "type": "point", - "position": [50.833, 15.917], - "uuid": "9b1c3d5e-7f9a-4b1c-8d5e-9f1a3b5c7d9e", - "cluster_uuid": null, - "cluster_count": null - } - ] +``7``), and rejects unusable values the same way — a ``zoom`` outside that range, like a +bad ``lat``, is a ``400``. -Both kinds come back in one list, told apart by ``type``. A ``"point"`` carries a real -``uuid`` you can pass to :ref:`api-location-detail`; a ``"cluster"`` carries a +Points and clusters come back in one list, told apart by ``type``. A ``"point"`` carries +a real ``uuid`` you can pass to :ref:`api-location-detail`; a ``"cluster"`` carries a freshly-generated ``cluster_uuid`` (not stable across requests — it is a render key, not an identifier) and the number of points it stands for. ``position`` is ``[latitude, longitude]``, as everywhere else. @@ -163,70 +182,51 @@ in neither list are not returned at all. The path segment must be a valid UUID; anything else fails routing with ``404``. A well-formed UUID that does not exist also gives ``404 {"message": "Location not found"}``. +Deployment-specific: what your instance declares +------------------------------------------------ + +The endpoints above have a fixed shape, but the *values* moving through them do not. +Which filters apply, which fields a point may carry, which issues can be reported — all +of that comes from your own data source (:doc:`data-source`), so it differs between +instances. Rather than enumerating one instance's values here, these endpoints report +what yours actually declares. They are grouped under the ``deployment_specific`` tag in +``/api/doc``, and a running instance is always the authority. + ``GET /api/categories-full`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Every category with its options, defaults and filter mode — everything needed to render -the filter panel in one request. - -.. code-block:: json - - { - "categories": [ - { - "key": "accessible_by", - "name": "accessible_by", - "options": [["bikes", "bikes"], ["cars", "cars"]], - "default_checked": [], - "filter_mode": "or" - } - ] - } +the filter panel in one request, and the way to learn which filter parameters +:ref:`api-locations` accepts on this instance. -``key`` is the query-parameter name to filter by; ``name`` is its translated label. -``options`` are ``[value, translated label]`` pairs — send the *value*. -``filter_mode`` tells you which control to draw: checkboxes for ``or``/``and``, radio +``key`` is the query-parameter name to filter by; ``options`` are +``[value, translated label]`` pairs — send the *value*. ``filter_mode`` is one of the five +fixed modes and tells you which control to draw: checkboxes for ``or``/``and``, radio buttons for ``exclusive``/``threshold``, a single checkbox for ``boolean`` (:ref:`categories-filter-mode`). -With ``CATEGORIES_HELP`` on, each category also carries ``options_help``, and the response -gains a top-level ``categories_help`` — both lists of ``{option: help text}`` objects. - -Prefer this endpoint over the two below, which exist for older clients and cost one -request per category. - -``GET /api/categories`` -~~~~~~~~~~~~~~~~~~~~~~~ +.. _api-location-schema: -Category names only, as ``[key, translated name]`` pairs. With ``CATEGORIES_HELP`` on, -returns ``{"categories": [...], "categories_help": [...]}`` instead — note the response -*type* changes with the flag. - -``GET /api/category/`` +``GET /api/location-schema`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The options for one category, as ``[value, translated label]`` pairs. With -``CATEGORIES_HELP`` on, returns -``{"categories_options": [...], "categories_options_help": [...]}``. +What this instance accepts for a new point: the fields of its location model (all of them +except the server-assigned ``uuid``), the allowed values per category, the reportable +issue types, and the photo limits. This is how a client learns what to put in +``/api/suggest-new-point``'s ``location`` payload rather than assuming — it is the same +schema the built-in suggest form is generated from. ``GET /api/languages`` ~~~~~~~~~~~~~~~~~~~~~~ -The configured interface languages, exactly as given in ``LANGUAGES``: - -.. code-block:: json +The configured interface languages, keyed by language code, exactly as given in +``LANGUAGES``. - {"en": {"name": "English", "flag": "gb", "country": "GB"}} +Fixed everywhere +---------------- -``GET /api/version`` -~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: json - - {"backend": ""} - -The installed package version, normalised to PEP 440 — a release published as -``2.0.0-alpha.5`` reports as ``2.0.0a5``. Useful as a health check. +``GET /api/version`` returns the installed package version normalised to PEP 440, so a +release published as ``2.0.0-alpha.5`` reports as ``2.0.0a5``. Submissions ----------- @@ -244,15 +244,27 @@ the map data. **The request must be ``multipart/form-data``.** The point goes in a single ``location`` form field as a JSON object — not as one form field per property — and the optional photo -goes in a ``photo`` file part. Send the point without a ``uuid``; the server assigns one: +goes in a ``photo`` file part. Send the point without a ``uuid``; the server assigns one. + +That shape is the same everywhere. **What goes inside the JSON object is not** — the +accepted fields are whatever *your* data source declares in ``location_obligatory_fields`` +and ``categories`` (:doc:`data-source`), so there is no universal payload to copy. The +fields below are the ones the :doc:`quickstart` map happens to declare; substitute your +own: .. code-block:: bash curl -X POST http://localhost:5000/api/suggest-new-point \ + -b "$JAR" \ -H "X-CSRFToken: $TOKEN" \ -F 'location={"name": "Nowy", "position": [51.11, 17.03], "type_of_place": "small bridge", "accessible_by": ["bikes"], "is_free": "true"}' \ -F 'photo=@bridge.jpg' +(``$JAR`` and ``$TOKEN`` as obtained above.) + +To find the fields a given instance wants, call :ref:`api-location-schema` — the same +schema the built-in suggest form is generated from. + .. note:: Sending the point as a JSON request body used to work and no longer does — a @@ -320,11 +332,3 @@ description that satisfies neither rule gives ``400``. The report is stored with ``"status": "pending"`` and ``"priority": "medium"`` in the data source, for triage. - -``GET /api/generate-csrf-token`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 1.1.8 - - Deprecated since 1.1.8 and kept only for backward compatibility. Read the token from - the ``csrf-token`` meta tag instead. CSRF protection itself is unaffected. diff --git a/frontend/src/components/Map/components/Markers.jsx b/frontend/src/components/Map/components/Markers.jsx index 2315faf5..82a83024 100644 --- a/frontend/src/components/Map/components/Markers.jsx +++ b/frontend/src/components/Map/components/Markers.jsx @@ -57,7 +57,15 @@ export const Markers = ({ onLoadingChange = null }) => { setAreMarkersLoaded(false); const fetchMarkers = async () => { - const locations = await httpService.getLocations(categories); + let locations; + try { + locations = await httpService.getLocations(categories); + } catch (error) { + console.error('Failed to load locations:', error); + setMarkers([]); + setAreMarkersLoaded(true); + return; + } const markersToAdd = getMarkers(locations); diff --git a/frontend/src/components/MarkerPopup/MarkerPopup.jsx b/frontend/src/components/MarkerPopup/MarkerPopup.jsx index e0c5156f..5c53e582 100644 --- a/frontend/src/components/MarkerPopup/MarkerPopup.jsx +++ b/frontend/src/components/MarkerPopup/MarkerPopup.jsx @@ -81,7 +81,7 @@ const asteriskIcon = new Icon({ * @param {Object} props - Component props * @param {Object} props.place - Location data object * @param {number[]} props.place.position - Coordinates [latitude, longitude] - * @param {boolean} [props.place.remark] - Whether this location has a remark (uses asterisk icon if true) + * @param {boolean} [props.place.has_remark] - Whether this location has a remark (uses asterisk icon if true) * @returns {React.ReactElement} Leaflet Marker component with click-to-show-details functionality */ export const MarkerPopup = ({ place }) => { @@ -113,12 +113,12 @@ export const MarkerPopup = ({ place }) => { eventHandlers: { click: handleMarkerClick, }, - alt: place.remark ? 'Marker-Asterisk' : 'Marker', + alt: place.has_remark ? 'Marker-Asterisk' : 'Marker', }; // Only add icon prop if we have a custom icon (for remarks) // This prevents passing undefined which can cause issues with MarkerClusterGroup - if (place.remark) { + if (place.has_remark) { markerProps.icon = asteriskIcon; } @@ -132,7 +132,7 @@ export const MarkerPopup = ({ place }) => { MarkerPopup.propTypes = { place: PropTypes.shape({ position: PropTypes.arrayOf(PropTypes.number).isRequired, - remark: PropTypes.bool, + has_remark: PropTypes.bool, // eslint-disable-line camelcase -- matches backend API schema property name uuid: PropTypes.string.isRequired, }).isRequired, }; diff --git a/frontend/src/services/http/httpService.js b/frontend/src/services/http/httpService.js index e734563d..620fb6d7 100644 --- a/frontend/src/services/http/httpService.js +++ b/frontend/src/services/http/httpService.js @@ -13,6 +13,27 @@ import { useMapStore } from '../../components/Map/store/map.store'; // sanitizers for request-URL construction, not third-party validators. const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +/** + * Returns the parsed JSON body, or throws if the response was not a success. + * + * Without this a rejected request (e.g. 400 for an out-of-range lat) resolves to the + * API's `{message}` error object, which then reaches the callers as if it were the + * data they asked for and fails later with a confusing shape error. + * + * @param {Response} response - fetch response + * @param {string} what - short description of the request, used in the error message + * @returns {Promise} The parsed JSON body + * @throws {Error} If the response status is not ok + */ +const jsonOrThrow = async (response, what) => { + if (!response.ok) { + const body = await response.json().catch(() => null); + const detail = body?.message ? `: ${body.message}` : ''; + throw new Error(`Failed to fetch ${what} (HTTP ${response.status})${detail}`); + } + return response.json(); +}; + /** * Converts filter object to URL query string parameters. * Also includes map configuration (zoom, bounds) if server-side clustering is enabled. @@ -96,7 +117,7 @@ export const httpService = { 'Content-Type': 'application/json', }, }); - return response.json(); + return jsonOrThrow(response, 'locations'); }, /** @@ -119,7 +140,7 @@ export const httpService = { }, }, ); - return response.json(); + return jsonOrThrow(response, 'nearby locations'); }, /** @@ -143,7 +164,7 @@ export const httpService = { 'Content-Type': 'application/json', }, }); - return response.json(); + return jsonOrThrow(response, 'location details'); }, /** diff --git a/frontend/src/utils/csrf.js b/frontend/src/utils/csrf.js index 0229c95f..a28959fd 100644 --- a/frontend/src/utils/csrf.js +++ b/frontend/src/utils/csrf.js @@ -9,60 +9,32 @@ */ /** - * Gets the CSRF token from the page's meta tag, with fallback to legacy API endpoint. + * Gets the CSRF token from the page's meta tag. * - * Preferred method: The backend sets a meta tag like: + * The backend sets a meta tag like: * * - * Fallback (DEPRECATED): Fetches token from /api/generate-csrf-token endpoint. - * This fallback exists for backward compatibility but will be removed in a future version. - * * This token must be included in the X-CSRFToken header for all * state-changing requests (POST, PUT, PATCH, DELETE). * - * @returns {Promise} The CSRF token - * @throws {Error} If CSRF token cannot be obtained from either source + * @returns {string} The CSRF token + * @throws {Error} If the CSRF token meta tag is missing or empty * * @example - * const csrfToken = await getCsrfToken(); + * const csrfToken = getCsrfToken(); * axios.post('/api/suggest-new-point', data, { * headers: { 'X-CSRFToken': csrfToken } * }); */ -export const getCsrfToken = async () => { +export const getCsrfToken = () => { const metaTag = document.querySelector('meta[name="csrf-token"]'); + const token = metaTag?.getAttribute('content'); - // Try to get token from meta tag first (preferred method) - if (metaTag) { - const token = metaTag.getAttribute('content'); - if (token) { - return token; - } - } - - // Fallback to legacy API endpoint (DEPRECATED) - console.warn( - '⚠️ DEPRECATION WARNING: CSRF token meta tag not found. ' + - 'Falling back to /api/generate-csrf-token endpoint. ' + - 'This fallback is DEPRECATED and will be removed in a future version. ' + - 'Please ensure the backend includes in the page HTML.', - ); - - try { - const response = await fetch('/api/generate-csrf-token'); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - const data = await response.json(); - if (!data.csrf_token) { - throw new Error('API response missing csrf_token field'); - } - return data.csrf_token; - } catch (error) { - console.error('Failed to fetch CSRF token from legacy endpoint:', error); + if (!token) { throw new Error( - 'CSRF token not found. Neither meta tag nor /api/generate-csrf-token endpoint provided a valid token.', + 'CSRF token not found. Ensure the backend includes in the page HTML.', ); } -}; + return token; +}; diff --git a/frontend/tests/Map/components/Markers.test.jsx b/frontend/tests/Map/components/Markers.test.jsx new file mode 100644 index 00000000..3bac069b --- /dev/null +++ b/frontend/tests/Map/components/Markers.test.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, waitFor } from '@testing-library/react'; +import { MapContainer } from 'react-leaflet'; +import { Markers } from '../../../src/components/Map/components/Markers'; +import { CategoriesProvider } from '../../../src/components/Categories/CategoriesContext'; +import { httpService } from '../../../src/services/http/httpService'; + +jest.mock('../../../src/services/http/httpService', () => ({ + httpService: { + getCategoriesData: jest.fn(), + getLocations: jest.fn(), + }, +})); + +const renderMarkers = onLoadingChange => + render( + + + + + , + ); + +beforeEach(() => { + httpService.getCategoriesData.mockResolvedValue({ categories: [], defaultChecked: {} }); + // Server-side clustering settles the loading state directly, rather than waiting on + // a Leaflet cluster event, which keeps these assertions about Markers itself. + globalThis.FEATURE_FLAGS = { USE_SERVER_SIDE_CLUSTERING: true }; +}); + +afterEach(() => { + delete globalThis.FEATURE_FLAGS; +}); + +describe('Markers', () => { + it('reports loading finished once locations arrive', async () => { + httpService.getLocations.mockResolvedValue([]); + const onLoadingChange = jest.fn(); + + renderMarkers(onLoadingChange); + + await waitFor(() => expect(onLoadingChange).toHaveBeenLastCalledWith(false)); + }); + + it('settles the loading state when the locations request is rejected', async () => { + // getLocations rejects on a non-2xx response. Without a failure path the map + // would sit in its loading state forever, with the rejection unhandled. + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + httpService.getLocations.mockRejectedValue(new Error('HTTP 400')); + const onLoadingChange = jest.fn(); + + renderMarkers(onLoadingChange); + + await waitFor(() => expect(onLoadingChange).toHaveBeenLastCalledWith(false)); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to load locations:', + expect.any(Error), + ); + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/frontend/tests/MarkerPopup/MarkerClusterGroupIntegration.test.jsx b/frontend/tests/MarkerPopup/MarkerClusterGroupIntegration.test.jsx index b58d90f6..eadee6d8 100644 --- a/frontend/tests/MarkerPopup/MarkerClusterGroupIntegration.test.jsx +++ b/frontend/tests/MarkerPopup/MarkerClusterGroupIntegration.test.jsx @@ -24,17 +24,17 @@ describe('MarkerPopup integration with MarkerClusterGroup', () => { { position: [51.1095, 17.0525], uuid: 'location-1', - remark: false, + has_remark: false, // eslint-disable-line camelcase }, { position: [51.10655, 17.0555], uuid: 'location-2', - remark: true, + has_remark: true, // eslint-disable-line camelcase }, { position: [51.1085, 17.0535], uuid: 'location-3', - remark: false, + has_remark: false, // eslint-disable-line camelcase }, ]; diff --git a/frontend/tests/MarkerPopup/MarkerPopup.test.jsx b/frontend/tests/MarkerPopup/MarkerPopup.test.jsx index b5b33d8a..02f82fd6 100644 --- a/frontend/tests/MarkerPopup/MarkerPopup.test.jsx +++ b/frontend/tests/MarkerPopup/MarkerPopup.test.jsx @@ -10,7 +10,7 @@ jest.mock('../../src/services/http/httpService'); const location = { position: [51.1095, 17.0525], uuid: '21231', - remark: false, + has_remark: false, // eslint-disable-line camelcase -- matches backend API schema property name }; const locationData = { @@ -106,7 +106,8 @@ describe('MarkerPopup with remark', () => { }); it('should render marker popup with asterisks when remark is true', () => { - const locationWhenRemarkIsTrue = { ...location, remark: true }; + // eslint-disable-next-line camelcase -- matches backend API schema property name + const locationWhenRemarkIsTrue = { ...location, has_remark: true }; act(() => { render( { }); it('should pass custom icon prop when remark is true', () => { - const locationWithRemark = { ...location, remark: true }; + // eslint-disable-next-line camelcase -- matches backend API schema property name + const locationWithRemark = { ...location, has_remark: true }; act(() => { render( list[dict[str, str]]: + """Build the ``[{option: help text}]`` shape the help fields use. + + The help text is looked up under ``_