feat(notification): add target_mode enum constraint and merge register-notifications script
- Add NotificationTargetMode enum (new_users/exist_users/all_users/user_ids) - Create Alembic migrations: drop duplicate indexes, add target_mode column - Merge register-notifications.sh into dev-migrate.sh sync-notifications subcommand - Shorten notification config path: static/notification/notifications -> static/notifications - Update registration flow to dispatch notifications by target_mode - Add is_first_registration to RegisterBonusResult for first-time user detection - Remove dead code: link_published_notifications_to_user - Update welcome_points.yaml to target new_users only - Add 44 unit tests + 1 integration test, all passing
This commit is contained in:
@@ -10,6 +10,7 @@ from core.config.notification.static_sync import (
|
||||
build_static_notification_content_hash,
|
||||
load_static_notification_documents,
|
||||
)
|
||||
from schemas.enums import NotificationTargetMode
|
||||
|
||||
|
||||
def _write_yaml(path: Path, content: str) -> None:
|
||||
@@ -43,10 +44,86 @@ def test_load_static_notification_file_parses_valid_yaml(tmp_path: Path) -> None
|
||||
|
||||
assert loaded.notification.source_key == "welcome_bonus"
|
||||
assert loaded.notification.payload.action == "open_route"
|
||||
assert loaded.targets.mode == "user_ids"
|
||||
assert loaded.targets.mode == NotificationTargetMode.USER_IDS
|
||||
assert len(loaded.targets.user_ids or []) == 1
|
||||
|
||||
|
||||
def test_load_static_notification_file_parses_new_users(tmp_path: Path) -> None:
|
||||
file_path = tmp_path / "welcome_points.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: welcome_points
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Welcome
|
||||
body: You got points.
|
||||
payload:
|
||||
action: open_route
|
||||
route: /points
|
||||
tab: balance
|
||||
targets:
|
||||
mode: new_users
|
||||
""",
|
||||
)
|
||||
|
||||
loaded = load_static_notification_file(file_path)
|
||||
|
||||
assert loaded.targets.mode == NotificationTargetMode.NEW_USERS
|
||||
assert loaded.targets.user_ids is None
|
||||
|
||||
|
||||
def test_load_static_notification_file_parses_exist_users(tmp_path: Path) -> None:
|
||||
file_path = tmp_path / "promo.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: promo_return
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Come back
|
||||
body: We miss you.
|
||||
payload:
|
||||
action: none
|
||||
targets:
|
||||
mode: exist_users
|
||||
""",
|
||||
)
|
||||
|
||||
loaded = load_static_notification_file(file_path)
|
||||
|
||||
assert loaded.targets.mode == NotificationTargetMode.EXIST_USERS
|
||||
assert loaded.targets.user_ids is None
|
||||
|
||||
|
||||
def test_load_static_notification_file_parses_all_users(tmp_path: Path) -> None:
|
||||
file_path = tmp_path / "announce.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: system_announce
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Announcement
|
||||
body: Maintenance at midnight.
|
||||
payload:
|
||||
action: none
|
||||
targets:
|
||||
mode: all_users
|
||||
""",
|
||||
)
|
||||
|
||||
loaded = load_static_notification_file(file_path)
|
||||
|
||||
assert loaded.targets.mode == NotificationTargetMode.ALL_USERS
|
||||
|
||||
|
||||
def test_load_static_notification_file_rejects_invalid_targets(tmp_path: Path) -> None:
|
||||
file_path = tmp_path / "invalid.yaml"
|
||||
_write_yaml(
|
||||
@@ -72,6 +149,81 @@ def test_load_static_notification_file_rejects_invalid_targets(tmp_path: Path) -
|
||||
load_static_notification_file(file_path)
|
||||
|
||||
|
||||
def test_load_static_notification_file_rejects_unknown_mode(tmp_path: Path) -> None:
|
||||
file_path = tmp_path / "bad_mode.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: bad_mode
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Bad
|
||||
body: Bad mode.
|
||||
payload:
|
||||
action: none
|
||||
targets:
|
||||
mode: non_existent
|
||||
""",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid static notification data"):
|
||||
load_static_notification_file(file_path)
|
||||
|
||||
|
||||
def test_load_static_notification_file_rejects_new_users_with_user_ids(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
file_path = tmp_path / "bad_new_users.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: bad_new
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Bad
|
||||
body: Bad.
|
||||
payload:
|
||||
action: none
|
||||
targets:
|
||||
mode: new_users
|
||||
user_ids:
|
||||
- 11111111-1111-1111-1111-111111111111
|
||||
""",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid static notification data"):
|
||||
load_static_notification_file(file_path)
|
||||
|
||||
|
||||
def test_load_static_notification_file_rejects_user_ids_without_list(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
file_path = tmp_path / "bad_user_ids.yaml"
|
||||
_write_yaml(
|
||||
file_path,
|
||||
"""
|
||||
notification:
|
||||
source_key: bad_uids
|
||||
version: 1
|
||||
type: system
|
||||
status: published
|
||||
title: Bad
|
||||
body: Bad.
|
||||
payload:
|
||||
action: none
|
||||
targets:
|
||||
mode: user_ids
|
||||
""",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid static notification data"):
|
||||
load_static_notification_file(file_path)
|
||||
|
||||
|
||||
def test_load_static_notification_documents_rejects_duplicate_source_key(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user