fix(notes): honor explicit null when clearing optional fields

This commit is contained in:
robyroro
2026-10-07 19:50:03 +03:00
parent a8c147b238
commit 15c4cef36a
2 changed files with 94 additions and 9 deletions
+12 -9
View File
@@ -728,32 +728,35 @@ def setup_note_routes(task_scheduler=None, upload_handler=None):
) )
if body.title is not None: if body.title is not None:
note.title = body.title note.title = body.title
if body.content is not None: # Omitted fields preserve their values; explicit null clears the
# nullable fields used by the editor's removal controls.
supplied = body.model_fields_set
if "content" in supplied:
note.content = body.content note.content = body.content
if body.items is not None: if "items" in supplied:
note.items = json.dumps(body.items) note.items = json.dumps(body.items) if body.items is not None else None
flag_modified(note, "items") flag_modified(note, "items")
if body.note_type is not None: if body.note_type is not None:
note.note_type = body.note_type note.note_type = body.note_type
if body.color is not None: if "color" in supplied:
note.color = body.color note.color = body.color
if body.label is not None: if "label" in supplied:
note.label = body.label note.label = body.label
if body.pinned is not None: if body.pinned is not None:
note.pinned = body.pinned note.pinned = body.pinned
if body.archived is not None: if body.archived is not None:
note.archived = body.archived note.archived = body.archived
if body.due_date is not None: if "due_date" in supplied:
note.due_date = body.due_date note.due_date = body.due_date
if body.image_url is not None: if "image_url" in supplied:
note.image_url = body.image_url note.image_url = body.image_url
if body.gallery_id is not None: if "gallery_id" in supplied:
note.gallery_id = body.gallery_id note.gallery_id = body.gallery_id
if body.repeat is not None: if body.repeat is not None:
note.repeat = body.repeat note.repeat = body.repeat
if body.sort_order is not None: if body.sort_order is not None:
note.sort_order = body.sort_order note.sort_order = body.sort_order
if body.agent_session_id is not None: if "agent_session_id" in supplied:
note.agent_session_id = body.agent_session_id note.agent_session_id = body.agent_session_id
db.commit() db.commit()
+82
View File
@@ -0,0 +1,82 @@
"""Explicit null must clear nullable note fields; omission must preserve them."""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from tests.helpers.database import disposable_database
@pytest.fixture
def client(tmp_path, monkeypatch):
import routes.note.note_routes as routes
with disposable_database(tmp_path) as factory:
monkeypatch.setattr(routes, "SessionLocal", factory)
app = FastAPI()
@app.middleware("http")
async def identity(request, call_next):
request.state.current_user = request.headers.get("x-test-user", "alice")
return await call_next(request)
monkeypatch.setattr(routes, "_scheduler_ref", None)
app.include_router(routes.setup_note_routes())
with TestClient(app) as api:
yield api
@pytest.mark.parametrize("field,value", [
("due_date", "2030-10-08T09:00:00"),
("label", "home"),
("image_url", "https://example.com/note.png"),
("content", "Saved body"),
("items", [{"text": "Saved item", "done": False}]),
("color", "blue"),
("gallery_id", "gallery-1"),
("agent_session_id", "chat-1"),
])
def test_explicit_null_clears_saved_field(client, field, value):
created = client.post("/api/notes", json={"title": "Test note"})
assert created.status_code == 200, created.text
path = f"/api/notes/{created.json()['id']}"
populated = client.put(path, json={field: value})
assert populated.status_code == 200, populated.text
assert client.get(path).json()[field] == value
updated = client.put(path, json={field: None})
assert updated.status_code == 200, updated.text
saved = client.get(path)
assert saved.status_code == 200, saved.text
assert saved.json()[field] is None
assert updated.json()[field] is None
def test_omitted_fields_survive_title_edit(client):
fields = {
"due_date": "2030-10-08T09:00:00",
"label": "home",
"image_url": "https://example.com/note.png",
"content": "Saved body",
"items": [{"text": "Saved item", "done": False}],
"color": "blue",
"gallery_id": "gallery-1",
}
created = client.post("/api/notes", json={"title": "Before", **fields})
assert created.status_code == 200, created.text
path = f"/api/notes/{created.json()['id']}"
updated = client.put(path, json={"title": "After"})
assert updated.status_code == 200, updated.text
saved = client.get(path).json()
assert saved["title"] == "After"
for field, value in fields.items():
assert saved[field] == value
def test_null_update_cannot_clear_another_users_note(client):
created = client.post("/api/notes", json={"title": "Private", "label": "home"})
assert created.status_code == 200, created.text
path = f"/api/notes/{created.json()['id']}"
response = client.put(path, json={"label": None}, headers={"x-test-user": "bob"})
assert response.status_code == 404
assert client.get(path).json()["label"] == "home"