from datetime import date from io import BytesIO from PIL import Image from starlette.datastructures import Headers, UploadFile from app.schemas.journal import JournalCategoryCreate, JournalEntryCreate from app.services import journal_service def _make_entry_with_attachment(db_session, user_id, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) category = journal_service.create_category(db_session, user_id, JournalCategoryCreate(name="일상")) entry = journal_service.create_entry( db_session, user_id, JournalEntryCreate(category_id=category.id, entry_date=date.today(), content="기록"), ) buf = BytesIO() Image.new("RGB", (800, 600), "blue").save(buf, format="PNG") upload = UploadFile(file=BytesIO(buf.getvalue()), filename="photo.png", headers=Headers({"content-type": "image/png"})) attachment = journal_service.save_attachment(db_session, entry, upload) return attachment def test_get_media_requires_login(client): response = client.get("/api/journal/media/1") assert response.status_code == 401 def test_get_media_returns_file_for_owner(auth_client, db_session, test_user, tmp_path, monkeypatch): attachment = _make_entry_with_attachment(db_session, test_user.id, tmp_path, monkeypatch) response = auth_client.get(f"/api/journal/media/{attachment.id}") assert response.status_code == 200 assert response.content[:8] == b"\x89PNG\r\n\x1a\n" def test_get_media_thumbnail_variant(auth_client, db_session, test_user, tmp_path, monkeypatch): attachment = _make_entry_with_attachment(db_session, test_user.id, tmp_path, monkeypatch) response = auth_client.get(f"/api/journal/media/{attachment.id}?thumbnail=true") assert response.status_code == 200 def test_get_media_returns_404_for_missing_attachment(auth_client): response = auth_client.get("/api/journal/media/9999") assert response.status_code == 404 def test_get_media_returns_404_for_other_users_attachment(auth_client, db_session, other_user, tmp_path, monkeypatch): attachment = _make_entry_with_attachment(db_session, other_user.id, tmp_path, monkeypatch) response = auth_client.get(f"/api/journal/media/{attachment.id}") assert response.status_code == 404 def _png_bytes(): buf = BytesIO() Image.new("RGB", (400, 300), "red").save(buf, format="PNG") return buf.getvalue() def test_paste_image_requires_login(client): response = client.post("/api/journal/paste-image", files={"file": ("a.png", _png_bytes(), "image/png")}) assert response.status_code == 401 def test_paste_image_uploads_and_serves(auth_client, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) response = auth_client.post("/api/journal/paste-image", files={"file": ("a.png", _png_bytes(), "image/png")}) assert response.status_code == 200 url = response.json()["url"] assert url.startswith("/api/journal/pasted-media/") fetched = auth_client.get(url) assert fetched.status_code == 200 assert fetched.content[:8] == b"\x89PNG\r\n\x1a\n" def test_paste_image_rejects_unsupported_type(auth_client, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) response = auth_client.post( "/api/journal/paste-image", files={"file": ("a.pdf", b"%PDF-1.4", "application/pdf")} ) assert response.status_code == 400 def test_paste_image_rejects_oversized_file(auth_client, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) monkeypatch.setattr(journal_service.settings, "journal_max_upload_mb", 0) response = auth_client.post("/api/journal/paste-image", files={"file": ("a.png", _png_bytes(), "image/png")}) assert response.status_code == 400 def test_pasted_media_requires_login(client): response = client.get("/api/journal/pasted-media/anything.png") assert response.status_code == 401 def test_pasted_media_returns_404_for_missing_file(auth_client, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) response = auth_client.get("/api/journal/pasted-media/does-not-exist.png") assert response.status_code == 404 def test_pasted_media_is_scoped_per_user(auth_client, db_session, other_user, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) filename = journal_service.save_pasted_image( other_user.id, UploadFile(file=BytesIO(_png_bytes()), filename="a.png", headers=Headers({"content-type": "image/png"})), ) # 파일명을 정확히 알아도 다른 유저 소유 폴더 안에 있으면 접근할 수 없어야 한다(디렉터리로 스코핑됨). response = auth_client.get(f"/api/journal/pasted-media/{filename}") assert response.status_code == 404 def test_pasted_media_blocks_path_traversal(auth_client, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) # 요청 자체가 상대경로 컴포넌트를 포함하면 라우팅에서 걸러지지만, 인코딩된 경로 구분자로 # 시도해도 Path(...).name이 디렉터리 구분자를 다 제거해서 상위 폴더로 못 나간다. response = auth_client.get("/api/journal/pasted-media/..%2f..%2f..%2fetc%2fpasswd") assert response.status_code == 404 def test_reorder_categories_requires_login(client): response = client.post("/api/journal/categories/reorder", json={"category_ids": [1, 2]}) assert response.status_code == 401 def test_reorder_categories_updates_sort_order(auth_client, db_session, test_user): a = journal_service.create_category(db_session, test_user.id, JournalCategoryCreate(name="A")) b = journal_service.create_category(db_session, test_user.id, JournalCategoryCreate(name="B")) response = auth_client.post("/api/journal/categories/reorder", json={"category_ids": [b.id, a.id]}) assert response.status_code == 200 db_session.refresh(a) db_session.refresh(b) assert b.sort_order == 0 assert a.sort_order == 1 def test_reorder_categories_ignores_other_users_ids(auth_client, db_session, other_user): other_category = journal_service.create_category(db_session, other_user.id, JournalCategoryCreate(name="남의 것")) response = auth_client.post("/api/journal/categories/reorder", json={"category_ids": [other_category.id]}) assert response.status_code == 200 db_session.refresh(other_category) assert other_category.sort_order is None