Add Yaohuo verification-based self-service signup

This commit is contained in:
zeer
2026-04-15 15:36:50 +08:00
parent de130f1052
commit a65b67485e
9 changed files with 568 additions and 20 deletions

View File

@@ -7,7 +7,7 @@ from pathlib import Path
from office365_self_service import create_app, db
from office365_self_service.models import AuditEvent, RedemptionCode
from office365_self_service.services import Office365Service, ServiceConfigurationError, ServiceOperationError
from office365_self_service.services import Office365Service, ServiceConfigurationError, ServiceOperationError, YaohuoVerificationService
from office365_self_service.settings import GRAPH_BASE_URL, GRAPH_SCOPE, Settings, TOKEN_ENDPOINT_TEMPLATE, load_settings
@@ -35,6 +35,10 @@ def build_settings(database_url: str, **overrides) -> Settings:
"token_endpoint": TOKEN_ENDPOINT_TEMPLATE.format(tenant_id=tenant_id),
"scope": GRAPH_SCOPE,
"database_url": database_url,
"yaohuo_cookie": "",
"yaohuo_message_url": "https://www.yaohuo.me/bbs/messagelist_add.aspx",
"yaohuo_verification_enabled": False,
"yaohuo_verification_code_ttl_seconds": 600,
"validation_errors": (),
"warnings": (),
}
@@ -87,6 +91,20 @@ class FakeGraphClient:
self.deleted_users.append(user_id)
class FakeYaohuoService:
def __init__(self):
self.sent = []
def generate_code(self) -> str:
return "123456"
def expires_at(self):
return datetime(2026, 4, 15, 12, 0, 0, tzinfo=timezone.utc)
def send_verification_code(self, target_user_id: str, code: str) -> None:
self.sent.append((target_user_id, code))
class AppRouteTests(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
@@ -99,6 +117,7 @@ class AppRouteTests(unittest.TestCase):
)
self.app.testing = True
self.client = self.app.test_client()
self.app.extensions["yaohuo_verification_service"] = FakeYaohuoService()
with self.app.app_context():
db.drop_all()
@@ -257,6 +276,34 @@ class AppRouteTests(unittest.TestCase):
self.assertEqual(response.status_code, 409)
self.assertFalse(payload["success"])
def test_yaohuo_send_verify_and_provision_without_redemption_code(self):
response = self.client.post("/api/yaohuo/send-code", json={"targetUserId": "12345"})
payload = response.get_json()
self.assertEqual(response.status_code, 200)
self.assertTrue(payload["success"])
self.assertEqual(self.app.extensions["yaohuo_verification_service"].sent, [("12345", "123456")])
verify = self.client.post("/api/yaohuo/verify", json={"code": "123456"})
verify_payload = verify.get_json()
self.assertEqual(verify.status_code, 200)
self.assertTrue(verify_payload["success"])
provision = self.client.post("/api/yaohuo/provision", json={"username": "alice"})
provision_payload = provision.get_json()
self.assertEqual(provision.status_code, 201)
self.assertTrue(provision_payload["success"])
self.assertEqual(provision_payload["data"]["userPrincipalName"], "alice@example.com")
self.assertEqual(self.service.calls, ["alice"])
def test_yaohuo_provision_requires_verified_session(self):
response = self.client.post("/api/yaohuo/provision", json={"username": "alice"})
payload = response.get_json()
self.assertEqual(response.status_code, 403)
self.assertFalse(payload["success"])
self.assertEqual(self.service.calls, [])
class ServiceBehaviorTests(unittest.TestCase):
def test_create_user_accepts_full_upn_without_default_domain(self):
@@ -310,6 +357,17 @@ class ServiceBehaviorTests(unittest.TestCase):
self.assertEqual(fake_client.deleted_users, ["user-1"])
class YaohuoServiceTests(unittest.TestCase):
def test_generate_code_returns_six_digits(self):
settings = build_settings("sqlite:////tmp/unused.db", yaohuo_verification_enabled=True, yaohuo_cookie="cookie")
service = YaohuoVerificationService(settings)
code = service.generate_code()
self.assertEqual(len(code), 6)
self.assertTrue(code.isdigit())
class ModelSerializationTests(unittest.TestCase):
def test_redemption_code_serializes_datetimes_as_utc_z(self):
code = RedemptionCode(