from datetime import date, timedelta from io import BytesIO from pathlib import Path from PIL import Image from starlette.datastructures import Headers, UploadFile from app.schemas.journal import JournalCategoryCreate, JournalEntryCreate, JournalEntryUpdate from app.services import journal_service def _make_category(db_session, user_id, name="일상", color=None): return journal_service.create_category(db_session, user_id, JournalCategoryCreate(name=name, color=color)) def _make_entry(db_session, user_id, category_id, entry_date=None, content="오늘의 기록", tags=None, **overrides): data = JournalEntryCreate( category_id=category_id, entry_date=entry_date or date.today(), title=overrides.pop("title", None), content=content, moods=overrides.pop("moods", []), tags=tags or [], ) return journal_service.create_entry(db_session, user_id, data) def _png_bytes(size=(800, 600)) -> bytes: buf = BytesIO() Image.new("RGB", size, "red").save(buf, format="PNG") return buf.getvalue() def _upload(filename: str, content_type: str, data: bytes) -> UploadFile: return UploadFile(file=BytesIO(data), filename=filename, headers=Headers({"content-type": content_type})) # ---- 카테고리 ---- def test_ensure_default_category_creates_once(db_session, test_user): first = journal_service.ensure_default_category(db_session, test_user.id) assert first.name == journal_service.DEFAULT_CATEGORY_NAME second = journal_service.ensure_default_category(db_session, test_user.id) assert second.id == first.id assert len(journal_service.list_categories(db_session, test_user.id)) == 1 def test_list_categories_scoped_to_user(db_session, test_user, other_user): _make_category(db_session, test_user.id, name="일상") _make_category(db_session, other_user.id, name="남의 카테고리") mine = journal_service.list_categories(db_session, test_user.id) assert [c.name for c in mine] == ["일상"] def test_reorder_categories_ignores_foreign_and_unknown_ids(db_session, test_user, other_user): a = _make_category(db_session, test_user.id, name="A") b = _make_category(db_session, test_user.id, name="B") other = _make_category(db_session, other_user.id, name="남의 것") journal_service.reorder_categories(db_session, test_user.id, [b.id, a.id, other.id, 9999]) db_session.refresh(a) db_session.refresh(b) db_session.refresh(other) assert b.sort_order == 0 assert a.sort_order == 1 assert other.sort_order is None def test_count_entries_by_category(db_session, test_user, other_user): a = _make_category(db_session, test_user.id, name="A") b = _make_category(db_session, test_user.id, name="B") other_category = _make_category(db_session, other_user.id, name="남의 카테고리") _make_entry(db_session, test_user.id, a.id, content="1") _make_entry(db_session, test_user.id, a.id, content="2") _make_entry(db_session, test_user.id, b.id, content="3") _make_entry(db_session, other_user.id, other_category.id, content="다른 유저 기록") counts = journal_service.count_entries_by_category(db_session, test_user.id) assert counts == {a.id: 2, b.id: 1} def test_delete_category_cascades_entries_and_attachment_files(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="지울 카테고리") entry = _make_entry(db_session, test_user.id, category.id) attachment = journal_service.save_attachment( db_session, entry, _upload("photo.png", "image/png", _png_bytes()) ) file_path = attachment.file_path journal_service.delete_category(db_session, category) assert journal_service.get_entry(db_session, entry.id, test_user.id) is None assert not Path(file_path).exists() # ---- 태그 ---- def test_get_or_create_tags_reuses_existing_by_name(db_session, test_user): first = journal_service.get_or_create_tags(db_session, test_user.id, ["투자", "재테크"]) second = journal_service.get_or_create_tags(db_session, test_user.id, ["투자", "새태그"]) assert {t.name for t in first} == {"투자", "재테크"} investment_id = next(t.id for t in first if t.name == "투자") assert next(t.id for t in second if t.name == "투자") == investment_id assert {t.name for t in second} == {"투자", "새태그"} # ---- 엔트리 ---- def test_multiple_entries_per_day_same_category_allowed(db_session, test_user): category = _make_category(db_session, test_user.id) today = date.today() morning = _make_entry(db_session, test_user.id, category.id, entry_date=today, content="아침 기록") evening = _make_entry(db_session, test_user.id, category.id, entry_date=today, content="저녁 기록") entries = journal_service.list_entries(db_session, test_user.id, category_id=category.id) assert {e.id for e in entries} == {morning.id, evening.id} def test_get_entry_returns_none_for_other_users_entry(db_session, test_user, other_user): category = _make_category(db_session, other_user.id) other_entry = _make_entry(db_session, other_user.id, category.id) assert journal_service.get_entry(db_session, other_entry.id, test_user.id) is None assert journal_service.get_entry(db_session, other_entry.id, other_user.id) is not None def test_create_entry_attaches_tags(db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, tags=["투자", "회고"]) assert {t.name for t in entry.tags} == {"투자", "회고"} def test_update_entry_replaces_tags(db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, tags=["투자"]) updated = journal_service.update_entry( db_session, entry, JournalEntryUpdate(category_id=category.id, entry_date=entry.entry_date, content="수정된 내용", tags=["새태그"]), ) assert {t.name for t in updated.tags} == {"새태그"} assert updated.content == "수정된 내용" def test_create_entry_supports_multiple_moods(db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, moods=["happy", "tired"]) assert {m.mood.value for m in entry.moods} == {"happy", "tired"} def test_update_entry_replaces_moods(db_session, test_user): category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id, moods=["sad"]) updated = journal_service.update_entry( db_session, entry, JournalEntryUpdate( category_id=category.id, entry_date=entry.entry_date, content="내용", moods=["happy", "calm"] ), ) assert {m.mood.value for m in updated.moods} == {"happy", "calm"} def test_delete_entry_removes_attachment_files(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) attachment = journal_service.save_attachment( db_session, entry, _upload("photo.png", "image/png", _png_bytes()) ) file_path = Path(attachment.file_path) thumb_path = Path(attachment.thumbnail_path) assert file_path.exists() assert thumb_path.exists() journal_service.delete_entry(db_session, entry) assert not file_path.exists() assert not thumb_path.exists() # ---- 첨부파일 ---- def test_save_attachment_generates_thumbnail_for_image(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) attachment = journal_service.save_attachment( db_session, entry, _upload("photo.png", "image/png", _png_bytes()) ) assert attachment.thumbnail_path is not None with Image.open(attachment.thumbnail_path) as thumb: assert thumb.width <= journal_service.THUMBNAIL_WIDTH assert Path(attachment.file_path).exists() def test_save_attachment_rejects_unsupported_type(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) try: journal_service.save_attachment(db_session, entry, _upload("doc.pdf", "application/pdf", b"%PDF-1.4")) assert False, "should have raised" except ValueError as exc: assert "지원하지 않는" in str(exc) def test_save_attachment_rejects_oversized_file(db_session, test_user, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) monkeypatch.setattr(journal_service.settings, "journal_max_upload_mb", 0) category = _make_category(db_session, test_user.id) entry = _make_entry(db_session, test_user.id, category.id) try: journal_service.save_attachment(db_session, entry, _upload("photo.png", "image/png", _png_bytes())) assert False, "should have raised" except ValueError as exc: assert "용량" in str(exc) def test_get_attachment_scoped_to_owner(db_session, test_user, other_user, tmp_path, monkeypatch): monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path)) category = _make_category(db_session, other_user.id) entry = _make_entry(db_session, other_user.id, category.id) attachment = journal_service.save_attachment( db_session, entry, _upload("photo.png", "image/png", _png_bytes()) ) assert journal_service.get_attachment(db_session, attachment.id, test_user.id) is None assert journal_service.get_attachment(db_session, attachment.id, other_user.id) is not None # ---- 캘린더 / day-detail / 회상 ---- def test_get_monthly_journal_summary_counts_entries_per_day(db_session, test_user): category = _make_category(db_session, test_user.id, color="#ff0000") today = date.today() _make_entry(db_session, test_user.id, category.id, entry_date=today, content="1") _make_entry(db_session, test_user.id, category.id, entry_date=today, content="2") summary = journal_service.get_monthly_journal_summary(db_session, test_user.id, today.year, today.month) assert summary[today].total_count == 2 assert summary[today].category_colors == ["#ff0000"] def test_get_day_entries_returns_entries_for_that_date(db_session, test_user): category = _make_category(db_session, test_user.id) today = date.today() yesterday = today - timedelta(days=1) _make_entry(db_session, test_user.id, category.id, entry_date=today, content="오늘 거") _make_entry(db_session, test_user.id, category.id, entry_date=yesterday, content="어제 거") items = journal_service.get_day_entries(db_session, test_user.id, today) assert [i.content for i in items] == ["오늘 거"] def test_get_on_this_day_matches_month_day_different_year(db_session, test_user): category = _make_category(db_session, test_user.id) today = date.today() last_year = today.replace(year=today.year - 1) _make_entry(db_session, test_user.id, category.id, entry_date=last_year, content="작년 오늘") _make_entry(db_session, test_user.id, category.id, entry_date=today, content="올해 오늘") items = journal_service.get_on_this_day(db_session, test_user.id, today) assert [i.content for i in items] == ["작년 오늘"] assert items[0].years_ago == 1 def test_get_random_prompt_returns_none_when_empty(db_session): assert journal_service.get_random_prompt(db_session) is None