DELETE /library/api/libraries/{uid}/ called bare lib.delete(), orphaning
Collections/Items/Chunks/Images and skipping Concept GC — the only delete
path not using the shared cascade. It now delegates to
delete_library_cascade like the HTML and workspace delete paths.
Name-conflict checks on both create endpoints are now case-insensitive
via find_library_by_name_ci (parameterised Cypher toLower comparison —
not neomodel iexact, which embeds the value in a regex and breaks on
names like "C++ Notes"). The Neo4j unique index is case-sensitive, so
"amazon connect" previously coexisted silently with "Amazon Connect",
confusing every name-matching client (Spelunker matches names
case-insensitively). The 409 reports the existing spelling. The plain
create also gains a UniqueProperty catch for the pre-check/save race,
mirroring the workspace path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Tests for the plain library REST endpoints beyond create.
|
|
|
|
Currently covers the DELETE cascade: ``DELETE /library/api/libraries/{uid}/``
|
|
must go through ``delete_library_cascade`` (shared with the HTML and
|
|
workspace delete paths) — a bare ``lib.delete()`` leaks Collections, Items,
|
|
Chunks, and Images and skips orphan-Concept GC. Neo4j is stubbed via
|
|
``sys.modules``, same style as ``test_managed_by.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from rest_framework.test import APIClient
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class LibraryApiDeleteTests(TestCase):
|
|
"""DELETE on the plain library endpoint cascades."""
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="op", password="pw")
|
|
self.client = APIClient()
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
def _fake_models_module(self, lib):
|
|
fake_nodes = MagicMock()
|
|
if lib is None:
|
|
class DoesNotExist(Exception):
|
|
pass
|
|
|
|
fake_library = SimpleNamespace(nodes=fake_nodes, DoesNotExist=DoesNotExist)
|
|
fake_nodes.get.side_effect = DoesNotExist()
|
|
else:
|
|
fake_library = SimpleNamespace(nodes=fake_nodes, DoesNotExist=Exception)
|
|
fake_nodes.get.return_value = lib
|
|
return SimpleNamespace(Library=fake_library)
|
|
|
|
def test_delete_uses_shared_cascade(self):
|
|
lib = SimpleNamespace(uid="lib-1", name="Docs")
|
|
cascade_result = {
|
|
"library_uid": "lib-1",
|
|
"name": "Docs",
|
|
"item_count": 3,
|
|
"item_s3_keys": [],
|
|
"orphans_deleted": 1,
|
|
}
|
|
with patch.dict(
|
|
"sys.modules", {"library.models": self._fake_models_module(lib)}
|
|
), patch(
|
|
"library.api.views.delete_library_cascade",
|
|
return_value=cascade_result,
|
|
) as mock_cascade:
|
|
response = self.client.delete("/library/api/libraries/lib-1/")
|
|
|
|
self.assertEqual(response.status_code, 204)
|
|
mock_cascade.assert_called_once_with(lib)
|
|
|
|
def test_delete_missing_library_returns_404(self):
|
|
with patch.dict(
|
|
"sys.modules", {"library.models": self._fake_models_module(None)}
|
|
), patch("library.api.views.delete_library_cascade") as mock_cascade:
|
|
response = self.client.delete("/library/api/libraries/nope/")
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
mock_cascade.assert_not_called()
|