Initial commit: habit tracker PWA with Google OAuth, push notifications
FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine server-rendered frontend. Multi-user via Google OAuth, daily habit tracking, monthly/weekly history views, Web Push reminders via APScheduler, and PWA support (manifest, service worker, offline caching). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
from datetime import date, time
|
||||
|
||||
from app.models.habit import ALL_WEEKDAYS_MASK, Habit, HabitStatus, HabitType
|
||||
from app.models.habit_log import HabitLog
|
||||
from app.schemas.habit import HabitCreate, HabitUpdate
|
||||
from app.services import habit_service
|
||||
|
||||
|
||||
def _make_habit(db_session, user_id, name="아침 일찍 일어나기", **overrides):
|
||||
data = HabitCreate(
|
||||
name=name,
|
||||
habit_type=overrides.pop("habit_type", HabitType.BUILD),
|
||||
weekdays_mask=overrides.pop("weekdays_mask", ALL_WEEKDAYS_MASK),
|
||||
condition_text=overrides.pop("condition_text", None),
|
||||
reminder_time=overrides.pop("reminder_time", None),
|
||||
)
|
||||
return habit_service.create_habit(db_session, user_id, data)
|
||||
|
||||
|
||||
def test_create_habit_defaults_to_active(db_session, test_user):
|
||||
habit = _make_habit(db_session, test_user.id)
|
||||
assert habit.status == HabitStatus.ACTIVE
|
||||
assert habit.user_id == test_user.id
|
||||
assert habit.id is not None
|
||||
|
||||
|
||||
def test_list_habits_scoped_to_user(db_session, test_user, other_user):
|
||||
_make_habit(db_session, test_user.id, name="내 습관")
|
||||
_make_habit(db_session, other_user.id, name="남의 습관")
|
||||
|
||||
mine = habit_service.list_habits(db_session, test_user.id)
|
||||
assert [h.name for h in mine] == ["내 습관"]
|
||||
|
||||
|
||||
def test_get_habit_returns_none_for_other_users_habit(db_session, test_user, other_user):
|
||||
other_habit = _make_habit(db_session, other_user.id, name="남의 습관")
|
||||
|
||||
assert habit_service.get_habit(db_session, other_habit.id, test_user.id) is None
|
||||
assert habit_service.get_habit(db_session, other_habit.id, other_user.id) is not None
|
||||
|
||||
|
||||
def test_list_habits_filters_by_type_and_status(db_session, test_user):
|
||||
build = _make_habit(db_session, test_user.id, name="빌드", habit_type=HabitType.BUILD)
|
||||
quit_ = _make_habit(db_session, test_user.id, name="퀴트", habit_type=HabitType.QUIT)
|
||||
habit_service.complete_habit(db_session, quit_)
|
||||
|
||||
active_builds = habit_service.list_habits(db_session, test_user.id, habit_type=HabitType.BUILD)
|
||||
assert [h.id for h in active_builds] == [build.id]
|
||||
|
||||
completed = habit_service.list_habits(db_session, test_user.id, status=HabitStatus.COMPLETED)
|
||||
assert [h.id for h in completed] == [quit_.id]
|
||||
|
||||
|
||||
def test_update_habit_overwrites_fields(db_session, test_user):
|
||||
habit = _make_habit(db_session, test_user.id, name="원래 이름")
|
||||
updated = habit_service.update_habit(
|
||||
db_session,
|
||||
habit,
|
||||
HabitUpdate(name="바뀐 이름", habit_type=HabitType.BUILD, weekdays_mask=0b0000001, condition_text=" "),
|
||||
)
|
||||
assert updated.name == "바뀐 이름"
|
||||
assert updated.weekdays_mask == 0b0000001
|
||||
assert updated.condition_text is None # blank_condition_to_none 검증기 통과 확인
|
||||
|
||||
|
||||
def test_delete_habit_removes_row_and_cascades_logs(db_session, test_user):
|
||||
habit = _make_habit(db_session, test_user.id)
|
||||
db_session.add(HabitLog(habit_id=habit.id, log_date=date.today()))
|
||||
db_session.commit()
|
||||
|
||||
habit_service.delete_habit(db_session, habit)
|
||||
|
||||
assert db_session.get(Habit, habit.id) is None
|
||||
assert db_session.query(HabitLog).filter_by(habit_id=habit.id).count() == 0
|
||||
|
||||
|
||||
def test_complete_and_reactivate_habit(db_session, test_user):
|
||||
habit = _make_habit(db_session, test_user.id)
|
||||
|
||||
completed = habit_service.complete_habit(db_session, habit)
|
||||
assert completed.status == HabitStatus.COMPLETED
|
||||
assert completed.completed_at is not None
|
||||
|
||||
reactivated = habit_service.reactivate_habit(db_session, habit)
|
||||
assert reactivated.status == HabitStatus.ACTIVE
|
||||
assert reactivated.completed_at is None
|
||||
|
||||
|
||||
def test_reorder_habits_applies_given_order(db_session, test_user):
|
||||
a = _make_habit(db_session, test_user.id, name="A")
|
||||
b = _make_habit(db_session, test_user.id, name="B")
|
||||
c = _make_habit(db_session, test_user.id, name="C")
|
||||
|
||||
habit_service.reorder_habits(db_session, test_user.id, [c.id, a.id, b.id])
|
||||
|
||||
ordered = habit_service.list_habits(db_session, test_user.id)
|
||||
assert [h.name for h in ordered] == ["C", "A", "B"]
|
||||
|
||||
|
||||
def test_reorder_habits_ignores_other_users_ids(db_session, test_user, other_user):
|
||||
mine = _make_habit(db_session, test_user.id, name="내 것")
|
||||
others = _make_habit(db_session, other_user.id, name="남의 것")
|
||||
|
||||
# 남의 habit_id가 섞여 들어와도 그 습관의 sort_order는 바뀌지 않아야 한다 (IDOR 방지).
|
||||
habit_service.reorder_habits(db_session, test_user.id, [others.id, mine.id])
|
||||
|
||||
assert others.sort_order is None
|
||||
assert mine.sort_order is not None
|
||||
|
||||
|
||||
def test_list_active_habits_with_reminders_is_not_user_scoped(db_session, test_user, other_user):
|
||||
with_reminder = _make_habit(db_session, test_user.id, name="알림 있음", reminder_time=time(9, 0))
|
||||
_make_habit(db_session, other_user.id, name="알림 없음")
|
||||
|
||||
result = habit_service.list_active_habits_with_reminders(db_session)
|
||||
assert [h.id for h in result] == [with_reminder.id]
|
||||
|
||||
|
||||
def test_list_active_user_ids_is_not_user_scoped(db_session, test_user, other_user):
|
||||
_make_habit(db_session, test_user.id)
|
||||
_make_habit(db_session, other_user.id)
|
||||
|
||||
result = habit_service.list_active_user_ids(db_session)
|
||||
assert set(result) == {test_user.id, other_user.id}
|
||||
|
||||
|
||||
def test_list_active_user_ids_excludes_completed_only_users(db_session, test_user, other_user):
|
||||
_make_habit(db_session, test_user.id)
|
||||
completed_only = _make_habit(db_session, other_user.id)
|
||||
habit_service.complete_habit(db_session, completed_only)
|
||||
|
||||
result = habit_service.list_active_user_ids(db_session)
|
||||
assert result == [test_user.id]
|
||||
Reference in New Issue
Block a user