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_category(db_session, user_id, name="일상"): return journal_service.create_category(db_session, user_id, JournalCategoryCreate(name=name)) def _make_entry(db_session, user_id, category_id, entry_date=None, content="기록"): data = JournalEntryCreate(category_id=category_id, entry_date=entry_date or date.today(), content=content) return journal_service.create_entry(db_session, user_id, data) def _png_file(): buf = BytesIO() Image.new("RGB", (400, 300), "green").save(buf, format="PNG") buf.seek(0) return buf def test_journal_page_requires_login(client): response = client.get("/journal", follow_redirects=False) assert response.status_code == 303 assert response.headers["location"] == "/login" def test_journal_page_creates_default_category_and_renders(auth_client): response = auth_client.get("/journal") assert response.status_code == 200 assert journal_service.DEFAULT_CATEGORY_NAME in response.text def test_create_entry_via_multipart_form(auth_client, db_session, test_user, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) category = _make_category(db_session, test_user.id, name="투자") today = date.today() response = auth_client.post( "/journal/new", data={ "category_id": str(category.id), "entry_date": today.isoformat(), "title": "첫 기록", "content": "오늘의 투자 회고", "moods": "happy,tired", "tags": "투자, 회고", }, files={"files": ("photo.png", _png_file(), "image/png")}, follow_redirects=False, ) assert response.status_code == 200 assert response.headers["hx-redirect"] == f"/journal?year={today.year}&month={today.month}" entries = journal_service.list_entries(db_session, test_user.id) assert len(entries) == 1 assert entries[0].title == "첫 기록" assert {t.name for t in entries[0].tags} == {"투자", "회고"} assert {m.mood.value for m in entries[0].moods} == {"happy", "tired"} assert len(entries[0].attachments) == 1 def test_create_entry_rejects_blank_content(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id) response = auth_client.post( "/journal/new", data={"category_id": str(category.id), "entry_date": date.today().isoformat(), "content": " "}, ) assert response.status_code == 200 assert "내용을 입력해주세요" in response.text assert journal_service.list_entries(db_session, test_user.id) == [] def test_journal_day_detail_shows_entry(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id) today = date.today() _make_entry(db_session, test_user.id, category.id, entry_date=today, content="오늘의 기록") response = auth_client.get(f"/journal/day/{today.isoformat()}") assert response.status_code == 200 assert "오늘의 기록" in response.text def test_journal_day_detail_requires_login(client): response = client.get(f"/journal/day/{date.today().isoformat()}", follow_redirects=False) assert response.status_code == 303 assert response.headers["location"] == "/login" def test_edit_entry_updates_content(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, content="원래 내용") response = auth_client.post( f"/journal/{entry.id}/edit", data={ "category_id": str(category.id), "entry_date": entry.entry_date.isoformat(), "content": "수정된 내용", "tags": "", }, ) assert response.status_code == 200 assert "수정된 내용" in response.text db_session.refresh(entry) assert entry.content == "수정된 내용" def test_edit_entry_blank_content_keeps_form_open_with_error(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, content="원래 내용") response = auth_client.post( f"/journal/{entry.id}/edit", data={"category_id": str(category.id), "entry_date": entry.entry_date.isoformat(), "content": " "}, ) assert response.status_code == 200 assert "내용을 입력해주세요" in response.text db_session.refresh(entry) assert entry.content == "원래 내용" def test_edit_other_users_entry_returns_404(auth_client, db_session, other_user): category = _make_category(db_session, other_user.id) entry = _make_entry(db_session, other_user.id, category.id) response = auth_client.post( f"/journal/{entry.id}/edit", data={"category_id": str(category.id), "entry_date": entry.entry_date.isoformat(), "content": "해킹 시도"}, ) assert response.status_code == 404 def test_delete_entry_removes_it(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, content="지울 기록") response = auth_client.post(f"/journal/{entry.id}/delete") assert response.status_code == 200 assert "지울 기록" not in response.text assert journal_service.get_entry(db_session, entry.id, test_user.id) is None def test_delete_other_users_entry_returns_404(auth_client, db_session, other_user): category = _make_category(db_session, other_user.id) entry = _make_entry(db_session, other_user.id, category.id) response = auth_client.post(f"/journal/{entry.id}/delete") assert response.status_code == 404 assert journal_service.get_entry(db_session, entry.id, other_user.id) is not None def test_delete_attachment_removes_it(auth_client, db_session, test_user, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id) upload = UploadFile(file=_png_file(), filename="a.png", headers=Headers({"content-type": "image/png"})) attachment = journal_service.save_attachment(db_session, entry, upload) response = auth_client.post(f"/journal/{entry.id}/attachments/{attachment.id}/delete") assert response.status_code == 200 assert journal_service.get_attachment(db_session, attachment.id, test_user.id) is None def test_create_category_and_reject_duplicate(auth_client): response = auth_client.post("/journal/categories/new", data={"name": "투자"}, follow_redirects=False) assert response.status_code == 200 assert response.headers["hx-redirect"] == "/journal?tab=manage" dup_response = auth_client.post("/journal/categories/new", data={"name": "투자"}) assert dup_response.status_code == 200 assert "이미 같은 이름" in dup_response.text def test_journal_page_manage_tab_shows_delete_button_for_categories(auth_client, db_session, test_user): _make_category(db_session, test_user.id, name="투자") response = auth_client.get("/journal?tab=manage") assert response.status_code == 200 assert "/journal/categories/" in response.text assert "/delete" in response.text def test_journal_page_default_tab_hides_category_management(auth_client, db_session, test_user): _make_category(db_session, test_user.id, name="투자") response = auth_client.get("/journal") assert response.status_code == 200 assert "/journal/categories/" not in response.text def test_delete_category_removes_it_and_its_entries(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id, name="지울 카테고리") entry = _make_entry(db_session, test_user.id, category.id, content="딸려서 지워질 기록") response = auth_client.post(f"/journal/categories/{category.id}/delete", follow_redirects=False) assert response.status_code == 200 assert response.headers["hx-redirect"] == "/journal?tab=manage" assert journal_service.get_category(db_session, category.id, test_user.id) is None assert journal_service.get_entry(db_session, entry.id, test_user.id) is None def test_delete_other_users_category_returns_404(auth_client, db_session, other_user): category = _make_category(db_session, other_user.id, name="남의 카테고리") response = auth_client.post(f"/journal/categories/{category.id}/delete") assert response.status_code == 404 assert journal_service.get_category(db_session, category.id, other_user.id) is not None def test_edit_category_updates_name_and_color(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id, name="원래 이름") response = auth_client.post( f"/journal/categories/{category.id}/edit", data={"name": "바뀐 이름", "color": "#123456"}, follow_redirects=False, ) assert response.status_code == 200 assert response.headers["hx-redirect"] == "/journal?tab=manage" db_session.refresh(category) assert category.name == "바뀐 이름" assert category.color == "#123456" def test_edit_category_rejects_duplicate_name(auth_client, db_session, test_user): _make_category(db_session, test_user.id, name="투자") category = _make_category(db_session, test_user.id, name="일기") response = auth_client.post(f"/journal/categories/{category.id}/edit", data={"name": "투자"}) assert response.status_code == 200 assert "이미 같은 이름" in response.text def test_edit_other_users_category_returns_404(auth_client, db_session, other_user): category = _make_category(db_session, other_user.id, name="남의 카테고리") response = auth_client.post(f"/journal/categories/{category.id}/edit", data={"name": "가로채기"}) assert response.status_code == 404 def test_manage_tab_shows_entry_count_per_category(auth_client, db_session, test_user): category = _make_category(db_session, test_user.id, name="투자") _make_entry(db_session, test_user.id, category.id, content="1") _make_entry(db_session, test_user.id, category.id, content="2") response = auth_client.get("/journal?tab=manage") assert "기록 2개" in response.text