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_abandon_and_reactivate_habit(db_session, test_user): habit = _make_habit(db_session, test_user.id) abandoned = habit_service.abandon_habit(db_session, habit) assert abandoned.status == HabitStatus.ABANDONED assert abandoned.abandoned_at is not None reactivated = habit_service.reactivate_habit(db_session, habit) assert reactivated.status == HabitStatus.ACTIVE assert reactivated.abandoned_at is None def test_list_habits_filters_by_abandoned_status(db_session, test_user): build = _make_habit(db_session, test_user.id, name="빌드") quit_ = _make_habit(db_session, test_user.id, name="퀴트", habit_type=HabitType.QUIT) habit_service.abandon_habit(db_session, quit_) active = habit_service.list_habits(db_session, test_user.id, status=HabitStatus.ACTIVE) assert [h.id for h in active] == [build.id] abandoned = habit_service.list_habits(db_session, test_user.id, status=HabitStatus.ABANDONED) assert [h.id for h in abandoned] == [quit_.id] 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]