mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
feat(email): add admin notification settings
This commit is contained in:
@@ -13,6 +13,9 @@ from backend.migrations import (
|
||||
run_pending_migrations,
|
||||
)
|
||||
from backend.migration_steps.account_lockout import apply as apply_account_lockout
|
||||
from backend.migration_steps.email_notification_settings import (
|
||||
apply as apply_email_notification_settings,
|
||||
)
|
||||
from backend.migration_steps.task_thread_id import apply as apply_task_thread_id
|
||||
|
||||
|
||||
@@ -75,13 +78,36 @@ def test_existing_migrations_are_registered_in_order() -> None:
|
||||
assert [migration.id for migration in MIGRATIONS] == [
|
||||
"2026050401_add_account_lockout",
|
||||
"2026050402_add_task_thread_id",
|
||||
"2026050501_add_email_notification_settings",
|
||||
]
|
||||
assert [migration.apply.__module__ for migration in MIGRATIONS] == [
|
||||
"backend.migration_steps.account_lockout",
|
||||
"backend.migration_steps.task_thread_id",
|
||||
"backend.migration_steps.email_notification_settings",
|
||||
]
|
||||
|
||||
|
||||
def test_email_notification_settings_migration_creates_settings_table() -> None:
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
|
||||
with engine.connect() as conn:
|
||||
apply_email_notification_settings(conn)
|
||||
|
||||
columns = {
|
||||
row[1] for row in conn.execute(text("PRAGMA table_info(email_notification_settings)"))
|
||||
}
|
||||
|
||||
assert {
|
||||
"smtp_server",
|
||||
"smtp_port",
|
||||
"smtp_sender_email",
|
||||
"smtp_sender_password",
|
||||
"smtp_use_ssl",
|
||||
"notify_token_expiring",
|
||||
"notify_check_in_success",
|
||||
} <= columns
|
||||
|
||||
|
||||
def test_account_lockout_migration_adds_missing_user_fields() -> None:
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from backend.api import admin as admin_api
|
||||
from backend.dependencies import get_current_user, get_db
|
||||
from backend.models import Base, User
|
||||
from backend.schemas.email_settings import EmailNotificationSettingsUpdate
|
||||
from backend.services import email_settings_service
|
||||
from backend.services.email_service import EmailService
|
||||
from backend.services.email_settings_service import EmailSettingsService
|
||||
|
||||
|
||||
def make_session():
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session_factory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
session = session_factory()
|
||||
return engine, session_factory, session
|
||||
|
||||
|
||||
def test_email_settings_default_row_uses_environment_defaults_and_masks_password(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
engine, _, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SERVER", "smtp.example.test")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_PORT", 2525)
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SENDER_EMAIL", "mailer@example.com")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SENDER_PASSWORD", "secret")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_USE_SSL", False)
|
||||
|
||||
snapshot = EmailSettingsService.get_snapshot(session)
|
||||
|
||||
assert snapshot.smtp_server == "smtp.example.test"
|
||||
assert snapshot.smtp_port == 2525
|
||||
assert snapshot.smtp_sender_email == "mailer@example.com"
|
||||
assert snapshot.smtp_sender_password == "secret"
|
||||
assert snapshot.has_smtp_sender_password is True
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_email_settings_update_preserves_and_clears_password(monkeypatch) -> None:
|
||||
engine, _, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SERVER", "smtp.example.test")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_PORT", 2525)
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SENDER_EMAIL", "mailer@example.com")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_SENDER_PASSWORD", "")
|
||||
monkeypatch.setattr(email_settings_service.settings, "SMTP_USE_SSL", False)
|
||||
|
||||
EmailSettingsService.get_snapshot(session)
|
||||
EmailSettingsService.update_settings(
|
||||
session,
|
||||
EmailNotificationSettingsUpdate(
|
||||
smtp_server="smtp.changed.test",
|
||||
smtp_port=587,
|
||||
smtp_sender_email="ops@example.com",
|
||||
smtp_use_ssl=True,
|
||||
notify_token_expiring=False,
|
||||
notify_check_in_success=False,
|
||||
smtp_sender_password="new-secret",
|
||||
),
|
||||
)
|
||||
|
||||
updated = EmailSettingsService.get_snapshot(session)
|
||||
assert updated.smtp_server == "smtp.changed.test"
|
||||
assert updated.smtp_port == 587
|
||||
assert updated.smtp_sender_email == "ops@example.com"
|
||||
assert updated.smtp_sender_password == "new-secret"
|
||||
assert updated.notify_token_expiring is False
|
||||
assert updated.notify_check_in_success is False
|
||||
|
||||
EmailSettingsService.update_settings(
|
||||
session,
|
||||
EmailNotificationSettingsUpdate(
|
||||
smtp_server="smtp.changed.test",
|
||||
smtp_port=587,
|
||||
smtp_sender_email="ops@example.com",
|
||||
smtp_use_ssl=True,
|
||||
notify_token_expiring=False,
|
||||
notify_check_in_success=False,
|
||||
clear_smtp_sender_password=True,
|
||||
),
|
||||
)
|
||||
|
||||
cleared = EmailSettingsService.get_snapshot(session)
|
||||
assert cleared.smtp_sender_password in ("", None)
|
||||
assert cleared.has_smtp_sender_password is False
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_admin_email_settings_route_rejects_non_admin() -> None:
|
||||
engine, session_factory, session = make_session()
|
||||
app = FastAPI()
|
||||
app.include_router(admin_api.router, prefix="/api/admin")
|
||||
|
||||
def override_get_db():
|
||||
yield session
|
||||
|
||||
async def override_get_current_user() -> User:
|
||||
return User(id=1, alias="user", role="user", is_approved=True)
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
app.dependency_overrides[get_current_user] = override_get_current_user
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/admin/email_settings")
|
||||
|
||||
assert response.status_code == 403
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_token_expiring_notification_can_be_disabled(monkeypatch) -> None:
|
||||
engine, session_factory, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service, "SessionLocal", session_factory)
|
||||
EmailSettingsService.update_settings(
|
||||
session,
|
||||
EmailNotificationSettingsUpdate(
|
||||
smtp_server="smtp.example.test",
|
||||
smtp_port=465,
|
||||
smtp_sender_email="mailer@example.com",
|
||||
smtp_use_ssl=True,
|
||||
notify_token_expiring=False,
|
||||
notify_check_in_success=True,
|
||||
),
|
||||
)
|
||||
|
||||
sent: dict[str, object] = {}
|
||||
|
||||
def fake_send(*args, **kwargs) -> bool:
|
||||
sent["called"] = True
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(EmailService, "send_email", fake_send)
|
||||
|
||||
user = User(alias="Alice", email="alice@example.com")
|
||||
assert EmailService.notify_token_expiring(user, "1000") is False
|
||||
assert sent == {}
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_success_check_in_notification_can_be_disabled(monkeypatch) -> None:
|
||||
engine, session_factory, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service, "SessionLocal", session_factory)
|
||||
EmailSettingsService.update_settings(
|
||||
session,
|
||||
EmailNotificationSettingsUpdate(
|
||||
smtp_server="smtp.example.test",
|
||||
smtp_port=465,
|
||||
smtp_sender_email="mailer@example.com",
|
||||
smtp_use_ssl=True,
|
||||
notify_token_expiring=True,
|
||||
notify_check_in_success=False,
|
||||
),
|
||||
)
|
||||
|
||||
sent: dict[str, object] = {}
|
||||
|
||||
def fake_send(*args, **kwargs) -> bool:
|
||||
sent["called"] = True
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(EmailService, "send_email", fake_send)
|
||||
|
||||
user = User(alias="Alice", email="alice@example.com")
|
||||
assert (
|
||||
EmailService.notify_check_in_result(
|
||||
user,
|
||||
{"thread_id": "thread-1"},
|
||||
True,
|
||||
"打卡成功",
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert sent == {}
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_email_settings_update_validates_sender_email() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
EmailNotificationSettingsUpdate(
|
||||
smtp_server="smtp.example.test",
|
||||
smtp_port=465,
|
||||
smtp_sender_email="not-an-email",
|
||||
smtp_use_ssl=True,
|
||||
notify_token_expiring=True,
|
||||
notify_check_in_success=True,
|
||||
)
|
||||
@@ -29,6 +29,7 @@ def test_frontend_has_business_app_structure() -> None:
|
||||
"views/admin/AdminRecordsView.vue",
|
||||
"views/admin/AdminLogsView.vue",
|
||||
"views/admin/AdminStatsView.vue",
|
||||
"views/admin/AdminEmailSettingsView.vue",
|
||||
"utils/format.ts",
|
||||
]
|
||||
|
||||
@@ -53,10 +54,17 @@ def test_frontend_routes_cover_user_and_admin_workflows() -> None:
|
||||
"/admin/records",
|
||||
"/admin/logs",
|
||||
"/admin/stats",
|
||||
"/admin/email-settings",
|
||||
]:
|
||||
assert path in router
|
||||
|
||||
|
||||
def test_frontend_admin_api_covers_email_settings() -> None:
|
||||
api = (SRC_ROOT / "api" / "index.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "/api/admin/email_settings" in api
|
||||
|
||||
|
||||
def test_frontend_replaces_starter_component() -> None:
|
||||
app = (SRC_ROOT / "App.vue").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user