DocDoeAI / tests /test_documents_delete.py
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
3bcdb36
Raw
History Blame Contribute Delete
12.7 kB
from __future__ import annotations
import os
from app.core.database import SessionLocal
from app.models.chat_session import ChatMessageRecord, ChatSession
from app.models.document import Document
from app.models.document_chunk import DocumentChunk
from app.models.flashcard import FlashcardSet
from app.models.generation import Generation
from app.models.generation_cache import GenerationCache
from app.models.learning_state import GeneratedResource
from app.models.previous_paper import PreviousPaper
from app.models.previous_question import PreviousQuestion
from app.models.quiz import Quiz
from app.models.study_profile import StudyProfile
from app.models.syllabus_item import SyllabusItem
from app.models.video_render_job import VideoRenderJob
def _signup(client, *, email: str, password: str = "Pass123!beta", name: str = "Test User") -> str:
resp = client.post("/auth/signup", json={"name": name, "email": email, "password": password})
assert resp.status_code == 201, f"signup failed: {resp.status_code} {resp.text}"
return resp.json()["access_token"]
def _auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
def _create_document(client, token: str, title: str = "User Notes") -> str:
resp = client.post(
"/documents/upload",
headers=_auth(token),
data={"title": title, "subject": "Physics", "chapter": "Sound Waves"},
files={
"file": (
"physics_notes.txt",
b"Sound travels as a longitudinal wave through a medium.",
"text/plain",
),
},
)
assert resp.status_code == 201, f"upload failed: {resp.status_code} {resp.text}"
return resp.json()["id"]
def _assert_standard_not_found(resp) -> None:
assert resp.status_code == 404, f"expected 404, got {resp.status_code}: {resp.text}"
data = resp.json()
assert data["success"] is False
assert data["error"]["code"] == "RESOURCE_NOT_FOUND"
class TestDocumentDeletion:
def test_delete_requires_authentication(self, auth_client) -> None:
resp = auth_client.delete("/documents/doc_does_not_matter")
assert resp.status_code in (401, 403)
def test_delete_nonexistent_document_returns_standard_404(self, auth_client) -> None:
token = _signup(auth_client, email="delete_missing@docs.test", name="Missing User")
resp = auth_client.delete("/documents/doc_not_real_id", headers=_auth(token))
_assert_standard_not_found(resp)
def test_delete_requires_ownership(self, auth_client) -> None:
token_a = _signup(auth_client, email="alice_delete@docs.test", name="Alice")
token_b = _signup(auth_client, email="bob_delete@docs.test", name="Bob")
doc_id = _create_document(auth_client, token_a, "Alice Notes")
resp = auth_client.delete(f"/documents/{doc_id}", headers=_auth(token_b))
_assert_standard_not_found(resp)
# Alice's document must still be reachable by Alice — Bob's attempt did nothing.
still_there = auth_client.get(f"/documents/{doc_id}", headers=_auth(token_a))
assert still_there.status_code == 200
def test_delete_removes_document_chunks_and_file_from_disk(self, auth_client) -> None:
token = _signup(auth_client, email="delete_full@docs.test", name="Delete User")
doc_id = _create_document(auth_client, token, "Full Delete Notes")
with SessionLocal() as db:
doc = db.get(Document, doc_id)
assert doc is not None
file_path = doc.file_path
assert os.path.exists(file_path)
chunk_count_before = db.query(DocumentChunk).filter(DocumentChunk.document_id == doc_id).count()
assert chunk_count_before > 0
resp = auth_client.delete(f"/documents/{doc_id}", headers=_auth(token))
assert resp.status_code == 204, f"expected 204, got {resp.status_code}: {resp.text}"
# Row, chunks, and the physical file must all be gone.
with SessionLocal() as db:
assert db.get(Document, doc_id) is None
chunk_count_after = db.query(DocumentChunk).filter(DocumentChunk.document_id == doc_id).count()
assert chunk_count_after == 0
assert not os.path.exists(file_path)
# A second delete (or any subsequent access) is an honest 404, not a 500.
_assert_standard_not_found(auth_client.delete(f"/documents/{doc_id}", headers=_auth(token)))
_assert_standard_not_found(auth_client.get(f"/documents/{doc_id}", headers=_auth(token)))
def test_delete_removes_derived_generations_quizzes_and_flashcards(self, auth_client) -> None:
token = _signup(auth_client, email="delete_derived@docs.test", name="Derived User")
doc_id = _create_document(auth_client, token, "Derived Notes")
with SessionLocal() as db:
user_id = db.query(Document).filter(Document.id == doc_id).one().user_id
db.add(Generation(
user_id=user_id,
document_id=doc_id,
type="notes",
output_json={"notes": ["a"]},
model_used="test-model",
))
db.add(Quiz(user_id=user_id, document_id=doc_id, questions_json={"questions": []}))
db.add(FlashcardSet(user_id=user_id, document_id=doc_id, cards_json={"cards": []}))
db.commit()
assert db.query(Generation).filter(Generation.document_id == doc_id).count() == 1
assert db.query(Quiz).filter(Quiz.document_id == doc_id).count() == 1
assert db.query(FlashcardSet).filter(FlashcardSet.document_id == doc_id).count() == 1
resp = auth_client.delete(f"/documents/{doc_id}", headers=_auth(token))
assert resp.status_code == 204
with SessionLocal() as db:
assert db.query(Generation).filter(Generation.document_id == doc_id).count() == 0
assert db.query(Quiz).filter(Quiz.document_id == doc_id).count() == 0
assert db.query(FlashcardSet).filter(FlashcardSet.document_id == doc_id).count() == 0
def test_delete_detaches_video_render_job_and_study_profile_without_deleting_them(self, auth_client) -> None:
token = _signup(auth_client, email="delete_detach@docs.test", name="Detach User")
doc_id = _create_document(auth_client, token, "Detach Notes")
with SessionLocal() as db:
user_id = db.query(Document).filter(Document.id == doc_id).one().user_id
job = VideoRenderJob(user_id=user_id, title="A rendered video", source_document_id=doc_id)
db.add(job)
profile = db.query(StudyProfile).filter(StudyProfile.user_id == user_id).one_or_none()
if profile is None:
profile = StudyProfile(user_id=user_id, uploaded_document_id=doc_id)
db.add(profile)
else:
profile.uploaded_document_id = doc_id
db.commit()
job_id = job.id
profile_id = profile.id
resp = auth_client.delete(f"/documents/{doc_id}", headers=_auth(token))
assert resp.status_code == 204
with SessionLocal() as db:
kept_job = db.get(VideoRenderJob, job_id)
kept_profile = db.get(StudyProfile, profile_id)
# The video and the profile survive deletion — only the dangling
# reference to the removed document is cleared.
assert kept_job is not None
assert kept_job.source_document_id is None
assert kept_profile is not None
assert kept_profile.uploaded_document_id is None
def test_delete_removes_all_source_derived_account_data(self, auth_client) -> None:
token = _signup(auth_client, email="delete_all_derived@docs.test", name="Privacy User")
doc_id = _create_document(auth_client, token, "Private Sound Waves Notes")
with SessionLocal() as db:
document = db.get(Document, doc_id)
assert document is not None
user_id = document.user_id
paper = PreviousPaper(
user_id=user_id,
title="Derived Sound Waves PYQ",
subject="Physics",
file_name=document.file_name,
file_type=document.file_type,
file_path=document.file_path,
status="ready",
verification_status="verified",
)
paper.questions.append(
PreviousQuestion(
question_number="1",
question_text="Define frequency.",
subject="Physics",
source_origin="user_uploaded",
),
)
db.add(paper)
db.add(
SyllabusItem(
user_id=user_id,
source_id=doc_id,
board="Kerala State Board",
class_level="SSLC / 10th",
subject="Physics",
chapter="Sound Waves",
topic="Frequency",
),
)
db.add(
GeneratedResource(
user_id=user_id,
source_id=doc_id,
resource_type="notes",
title="Generated Sound Waves notes",
resource_data={"body": "private derived content"},
),
)
session = ChatSession(
user_id=user_id,
source_id=doc_id,
subject="Physics",
title="Chat from private notes",
)
session.messages.append(
ChatMessageRecord(
role="assistant",
content="Answer grounded in the private notes.",
),
)
db.add(session)
db.add(
GenerationCache(
cache_key=f"legacy:{doc_id}",
task_type="notes",
provider="test",
input_hash="private-source-hash",
output_text="cached private output",
metadata_json={"document_id": doc_id},
),
)
db.commit()
paper_id = paper.id
session_id = session.id
assert db.query(PreviousQuestion).filter(
PreviousQuestion.previous_paper_id == paper_id,
).count() == 1
assert db.query(ChatMessageRecord).filter(
ChatMessageRecord.session_id == session_id,
).count() == 1
resp = auth_client.delete(f"/documents/{doc_id}", headers=_auth(token))
assert resp.status_code == 204
with SessionLocal() as db:
assert db.get(PreviousPaper, paper_id) is None
assert db.query(PreviousQuestion).filter(
PreviousQuestion.previous_paper_id == paper_id,
).count() == 0
assert db.query(SyllabusItem).filter(
SyllabusItem.source_id == doc_id,
).count() == 0
assert db.query(GeneratedResource).filter(
GeneratedResource.source_id == doc_id,
).count() == 0
assert db.get(ChatSession, session_id) is None
assert db.query(ChatMessageRecord).filter(
ChatMessageRecord.session_id == session_id,
).count() == 0
assert db.get(GenerationCache, f"legacy:{doc_id}") is None
def test_legacy_source_delete_uses_the_same_complete_cleanup(self, auth_client) -> None:
token = _signup(auth_client, email="delete_source_alias@docs.test", name="Source User")
doc_id = _create_document(auth_client, token, "Legacy Source Delete")
with SessionLocal() as db:
document = db.get(Document, doc_id)
assert document is not None
file_path = document.file_path
db.add(
GeneratedResource(
user_id=document.user_id,
source_id=doc_id,
resource_type="quiz",
title="Source-linked quiz",
resource_data={},
),
)
db.commit()
resp = auth_client.delete(f"/sources/{doc_id}", headers=_auth(token))
assert resp.status_code == 204
with SessionLocal() as db:
assert db.get(Document, doc_id) is None
assert db.query(GeneratedResource).filter(
GeneratedResource.source_id == doc_id,
).count() == 0
assert not os.path.exists(file_path)