Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions mpcontribs-api/src/mpcontribs_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
consumer_username_scheme,
)
from mpcontribs_api.config import Settings, get_settings
from mpcontribs_api.domains._redirects.router import router as redirects_router
from mpcontribs_api.domains.attachments.models import Attachment
from mpcontribs_api.domains.consumers.models import Consumer
from mpcontribs_api.domains.contributions.models import Contribution
Expand Down Expand Up @@ -147,9 +146,6 @@ def create_app(settings: Settings | None = None) -> FastAPI:
register_exception_handlers(app)
app.include_router(healthcheck_router, prefix="/healthcheck")
app.include_router(v1_router, prefix="/api/v1")
# Legacy (root-path) endpoints: 308-redirect to /api/v1 where a counterpart
# exists, else 410 Gone. Registered last so it never shadows live routes.
app.include_router(redirects_router)

return app

Expand Down
161 changes: 0 additions & 161 deletions mpcontribs-api/src/mpcontribs_api/domains/_redirects/router.py

This file was deleted.

8 changes: 6 additions & 2 deletions mpcontribs-api/src/mpcontribs_api/domains/_shared/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,13 @@ def _serialize_frame(data: pl.DataFrame) -> dict:
def _nfkc_casefold(value: str) -> str:
"""NFKC + casefold: the case-insensitive, compatibility-folded form used for search/matching.

Surrounding whitespace is stripped by :func:`nfkc_normalize` before casefolding.
An idempotent nfkc + casefold operation. A single NFKC-then-casefold is not stable: casefold can
expand a character (``ß`` -> ``ss``) sitting before a combining mark, leaving an NFKC-unstable
sequence that re-composes (``s`` + circumflex -> ``ŝ``) on a second fold; and NFKC can compose a
decomposed form (``t`` + diaeresis -> ``ẗ``) that then casefolds back to the decomposed form.
Folding twice reaches the fixed point either way, so the output is both NFKC-stable and casefold-stable.
"""
return nfkc_normalize(value).casefold()
return unicodedata.normalize("NFKC", nfkc_normalize(value).casefold()).casefold()


def nfkc_normalize(value: str) -> str:
Expand Down
4 changes: 0 additions & 4 deletions mpcontribs-api/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,8 @@ async def _noop_lifespan(app: FastAPI):
register_exception_handlers(app)

from mpcontribs_api.api.v1.router import router as v1_router
from mpcontribs_api.domains._redirects.router import router as redirects_router

app.include_router(v1_router, prefix="/api/v1")
# Mounted last (root path), exactly as in the real app, so legacy-endpoint
# redirect/deprecation behaviour is exercised by integration tests.
app.include_router(redirects_router)
return app


Expand Down
143 changes: 0 additions & 143 deletions mpcontribs-api/tests/integration/test_redirects.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ def _make_service(
struct_repo = structures or AsyncMock()
table_repo = tables or AsyncMock()
attach_repo = attachments or AsyncMock()
# ``coerce_identifiers`` is a *sync* repo method (see MongoDbRepository), but a bare AsyncMock
# would turn it into a coroutine factory: the service passes its result straight into get_one/
# patch_one without awaiting, leaking un-awaited coroutines. Make it a sync passthrough on every
# repo so it behaves like the real thing (returns the identifiers dict unchanged).
for repo in (contrib_repo, struct_repo, table_repo, attach_repo):
repo.coerce_identifiers = MagicMock(side_effect=lambda identifiers: identifiers)
# Default identity resolution: every referenced project reports its ``unique_column`` (None by
# default -> identity is the fixed-field triple), and no identity exists yet, so the common path
# resolves with no conflict. Tests exercising duplicates override ``existing_identities``.
Expand Down
18 changes: 7 additions & 11 deletions mpcontribs-api/tests/unit/domains/test_search_str_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,15 @@ def test_searchstr_normalized_across_models(extract):
assert extract(_MESSY_TAG) == ["file"]


@pytest.mark.xfail(
strict=True,
reason="_nfkc_casefold is not idempotent when casefold expands a char sitting before a combining mark",
)
def test_searchstr_casefold_expansion_breaks_idempotency():
"""Documents a real edge: a casefold-expanding char (ß -> ss) followed by a combining mark.
def test_searchstr_casefold_expansion_is_idempotent():
"""Covers the tricky edge: a casefold-expanding char (ß -> ss) followed by a combining mark.

NFKC runs before casefold, so ``ß`` + combining circumflex stays decomposed through the first
fold (-> ``ss`` + circumflex). Re-folding then NFKC-composes ``s`` + circumflex into ``ŝ``, so
the value is not stable under a second pass. Because ``ProjectOut.tags`` is also
``list[SearchStr]``, a stored tag re-normalizes on read and can round-trip to a different
string. xfail(strict) so this flips to a failure the moment the normalizer is made idempotent
(e.g. a trailing NFKC pass after casefold).
fold (-> ``ss`` + circumflex). A naive fold would stop there and leave an NFKC-unstable value:
re-folding NFKC-composes ``s`` + circumflex into ``ŝ``, so it would round-trip to a different
string. ``_nfkc_casefold`` runs a trailing NFKC pass after casefold to collapse this now, so the
value is stable under a second pass. This matters because ``ProjectOut.tags`` is also
``list[SearchStr]`` and a stored tag re-normalizes on read.
"""
once = _search.validate_python("ß̂") # eszett + COMBINING CIRCUMFLEX ACCENT
assert _search.validate_python(once) == once
Loading