add journaling feature: categories, tags, attachments, calendar, and multi-select emotions
Adds an 8th development stage that lets users keep a free-form journal alongside habit tracking, reusing the existing Google OAuth/DB/PWA infrastructure instead of a separate project. Users organize entries into custom categories, filter by a month calendar with day-detail drill-down, attach photos/videos (served via an authenticated route, never /static), tag entries, and pick multiple emotions per entry from a curated 9-option set. Includes a global journal prompt bank for lightweight guided journaling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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 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
|
||||
@@ -0,0 +1,285 @@
|
||||
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
|
||||
@@ -0,0 +1,257 @@
|
||||
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
|
||||
Reference in New Issue
Block a user