From e5c75702e89e73d0d48f654fdbd247d339545f3c Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 29 Oct 2026 23:07:54 +0100 Subject: [PATCH] fix(views,tags): a rename onto a taken name names the taken one, not the row's old name The rollback after the conflict puts the old name back on the row, so the refusal read "you already have a view named 'hh'" when the taken name was 'gaming'. --- backend/app/routes/saved_views.py | 3 +- backend/app/routes/tags.py | 3 +- backend/tests/test_name_taken_detail.py | 46 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_name_taken_detail.py diff --git a/backend/app/routes/saved_views.py b/backend/app/routes/saved_views.py index f258b230..2af427ea 100644 --- a/backend/app/routes/saved_views.py +++ b/backend/app/routes/saved_views.py @@ -128,7 +128,8 @@ def update_view( db.commit() except IntegrityError: db.rollback() - raise refused(409, "view_name_taken", name=view.name) from None + # The name asked for: the rollback has put the view's old name back on `view`. + raise refused(409, "view_name_taken", name=name) from None return _serialize(view) diff --git a/backend/app/routes/tags.py b/backend/app/routes/tags.py index 47e7e8cc..250206ec 100644 --- a/backend/app/routes/tags.py +++ b/backend/app/routes/tags.py @@ -107,7 +107,8 @@ def update_tag( db.commit() except IntegrityError: db.rollback() - raise refused(409, "tag_name_taken", name=tag.name) from None + # The name asked for: the rollback has put the tag's old name back on `tag`. + raise refused(409, "tag_name_taken", name=name) from None return { "ok": True, "id": tag.id, diff --git a/backend/tests/test_name_taken_detail.py b/backend/tests/test_name_taken_detail.py new file mode 100644 index 00000000..e1f75acb --- /dev/null +++ b/backend/tests/test_name_taken_detail.py @@ -0,0 +1,46 @@ +"""A rename onto a name already taken says WHICH name is taken: the one asked for, not the row's +old name (the rollback that follows the conflict puts the old name back on the row). DB lane. +""" + +from fastapi.testclient import TestClient + +import app.state as state +from app.auth import current_user +from app.db import get_db +from app.main import app +from app.models import User + + +def _client(db, monkeypatch, override_dep): + u = User(email="taken@example.com") + db.add(u) + db.commit() + monkeypatch.setattr( + state, "_configured_cache", True + ) # the request gate 503s an unconfigured app + override_dep(get_db, db.info["request"]) + override_dep(current_user, lambda: u) + return TestClient(app) + + +def test_renaming_a_view_onto_a_taken_name_names_the_taken_one(db, monkeypatch, override_dep): + c = _client(db, monkeypatch, override_dep) + gaming = c.post("/api/saved-views", json={"name": "gaming", "filters": {}, "source": "feed"}) + assert gaming.status_code == 200 + hh = c.post("/api/saved-views", json={"name": "hh", "filters": {}, "source": "feed"}).json() + + r = c.patch(f"/api/saved-views/{hh['id']}", json={"name": "gaming"}) + + assert r.status_code == 409 + assert r.json()["detail"] == {"code": "refused", "reason": "view_name_taken", "name": "gaming"} + + +def test_renaming_a_tag_onto_a_taken_name_names_the_taken_one(db, monkeypatch, override_dep): + c = _client(db, monkeypatch, override_dep) + assert c.post("/api/tags", json={"name": "music"}).status_code == 200 + news = c.post("/api/tags", json={"name": "news"}).json() + + r = c.patch(f"/api/tags/{news['id']}", json={"name": "music"}) + + assert r.status_code == 409 + assert r.json()["detail"] == {"code": "refused", "reason": "tag_name_taken", "name": "music"}