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:
2026-07-16 18:06:17 +09:00
co-authored by Claude Sonnet 5
commit cee589bb3e
80 changed files with 4695 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
from pywebpush import WebPushException
from app.models.push_subscription import PushSubscription
from app.schemas.push import PushKeys, PushSubscribeRequest
from app.services import push_service
class _FakeResponse:
def __init__(self, status_code):
self.status_code = status_code
def _subscribe_request(endpoint="https://push.example.com/1"):
return PushSubscribeRequest(endpoint=endpoint, keys=PushKeys(p256dh="p256dh-key", auth="auth-key"))
# ---- save_subscription ----
def test_save_subscription_creates_new_row(db_session, test_user):
sub = push_service.save_subscription(db_session, test_user.id, _subscribe_request())
assert sub.user_id == test_user.id
assert sub.endpoint == "https://push.example.com/1"
def test_save_subscription_reassigns_existing_endpoint_to_new_user(db_session, test_user, other_user):
request = _subscribe_request()
push_service.save_subscription(db_session, other_user.id, request)
# 같은 기기(endpoint)에서 다른 유저(test_user)로 재구독하면 소유자가 갱신되어야 한다.
updated = push_service.save_subscription(db_session, test_user.id, request)
assert updated.user_id == test_user.id
assert db_session.query(PushSubscription).filter_by(endpoint=request.endpoint).count() == 1
def test_delete_subscription_removes_row(db_session, test_user):
sub = push_service.save_subscription(db_session, test_user.id, _subscribe_request())
push_service.delete_subscription(db_session, sub.endpoint)
assert db_session.query(PushSubscription).filter_by(endpoint=sub.endpoint).count() == 0
# ---- send_to_user ----
def test_send_to_user_counts_successful_sends(db_session, test_user, monkeypatch):
push_service.save_subscription(db_session, test_user.id, _subscribe_request("https://push.example.com/a"))
push_service.save_subscription(db_session, test_user.id, _subscribe_request("https://push.example.com/b"))
monkeypatch.setattr(push_service, "webpush", lambda **kwargs: None)
sent = push_service.send_to_user(db_session, test_user.id, title="제목", body="본문")
assert sent == 2
def test_send_to_user_deletes_expired_subscription_on_410(db_session, test_user, monkeypatch):
sub = push_service.save_subscription(db_session, test_user.id, _subscribe_request())
def _raise_gone(**kwargs):
raise WebPushException("gone", response=_FakeResponse(410))
monkeypatch.setattr(push_service, "webpush", _raise_gone)
sent = push_service.send_to_user(db_session, test_user.id, title="제목", body="본문")
assert sent == 0
assert db_session.query(PushSubscription).filter_by(id=sub.id).count() == 0
def test_send_to_user_keeps_subscription_on_other_errors(db_session, test_user, monkeypatch):
sub = push_service.save_subscription(db_session, test_user.id, _subscribe_request())
def _raise_server_error(**kwargs):
raise WebPushException("server error", response=_FakeResponse(500))
monkeypatch.setattr(push_service, "webpush", _raise_server_error)
sent = push_service.send_to_user(db_session, test_user.id, title="제목", body="본문")
assert sent == 0
assert db_session.query(PushSubscription).filter_by(id=sub.id).count() == 1