"""MSTeams audio / credentials / probe / oauth / sso 单元测试。""" from __future__ import annotations import json import time import pytest from yuxi.channels.adapters.msteams.audio import ( build_voice_activity, build_tts_activity, parse_voice_attachment, ) from yuxi.channels.adapters.msteams.credentials import ( FederatedCredentialError, DelegatedAuthStore, ) from yuxi.channels.adapters.msteams.oauth_flow import ( _generate_pkce_code_verifier, _generate_pkce_code_challenge, _generate_state, detect_wsl2, detect_ssh_session, needs_manual_oauth, OAuthPKCEFlow, ) from yuxi.channels.adapters.msteams.sso import ( is_signin_invoke, is_token_exchange, is_verify_state, extract_signin_token, build_oauth_card, build_token_exchange_response, build_sso_auth_activity, SSOHandler, SSOTokenStore, ) class TestAudioBuildVoiceActivity: def test_basic(self): activity = build_voice_activity(b"fake-audio-data") assert activity["type"] == "message" assert len(activity["attachments"]) == 1 assert activity["attachments"][0]["contentType"] == "audio/wav" assert activity["attachments"][0]["contentUrl"].startswith("data:audio/wav;base64,") def test_with_reply(self): activity = build_voice_activity(b"data", reply_to_id="msg-1") assert activity["replyToId"] == "msg-1" def test_custom_mime(self): activity = build_voice_activity(b"data", mime_type="audio/mp3", filename="voice.mp3") assert activity["attachments"][0]["contentType"] == "audio/mp3" assert activity["attachments"][0]["name"] == "voice.mp3" class TestAudioBuildTTSActivity: def test_basic(self): activity = build_tts_activity("hello world") assert activity["type"] == "message" assert activity["text"] == "hello world" assert activity["channelData"]["speak"] == "hello world" def test_voice_param(self): activity = build_tts_activity("hello", voice="en-US-JennyNeural") assert activity["channelData"]["voice"] == "en-US-JennyNeural" def test_with_reply(self): activity = build_tts_activity("hello", reply_to_id="msg-1") assert activity["replyToId"] == "msg-1" class TestAudioParseVoiceAttachment: def test_audio_attachment(self): result = parse_voice_attachment({ "contentType": "audio/wav", "contentUrl": "https://example.com/audio.wav", "name": "recording.wav", }) assert result is not None assert result["type"] == "audio" assert result["url"] == "https://example.com/audio.wav" def test_non_audio_attachment(self): result = parse_voice_attachment({"contentType": "image/png"}) assert result is None def test_missing_fields(self): result = parse_voice_attachment({"contentType": "audio/mp3"}) assert result is not None assert result["name"] == "audio" class TestFederatedCredentialError: def test_is_exception(self): with pytest.raises(FederatedCredentialError, match="Certificate file not found"): raise FederatedCredentialError("Certificate file not found") class TestDelegatedAuthStore: @pytest.fixture def store(self, tmp_path, monkeypatch): monkeypatch.delenv("MSTEAMS_DELEGATED_STORE_KEY", raising=False) return DelegatedAuthStore(storage_dir=str(tmp_path)) def test_store_and_get_token(self, store): store.store_token("user-1", "access-token", "refresh-token", expires_in=3600) entry = store.get_token("user-1") assert entry is not None assert entry["access_token"] == "access-token" assert entry["refresh_token"] == "refresh-token" def test_get_nonexistent(self, store): assert store.get_token("nonexistent") is None def test_is_expired_new_token(self, store): store.store_token("user-1", "access", "refresh", expires_in=3600) assert store.is_expired("user-1") is False def test_is_expired_nonexistent(self, store): assert store.is_expired("nonexistent") is True def test_persistence(self, tmp_path, monkeypatch): monkeypatch.delenv("MSTEAMS_DELEGATED_STORE_KEY", raising=False) store1 = DelegatedAuthStore(storage_dir=str(tmp_path)) store1.store_token("user-1", "at", "rt") store2 = DelegatedAuthStore(storage_dir=str(tmp_path)) entry = store2.get_token("user-1") assert entry is not None assert entry["access_token"] == "at" class TestOAuthPKCE: def test_generate_verifier(self): verifier = _generate_pkce_code_verifier() assert len(verifier) >= 43 def test_generate_challenge(self): verifier = "test-verifier" challenge = _generate_pkce_code_challenge(verifier) assert len(challenge) >= 43 def test_verifier_challenge_consistency(self): verifier = _generate_pkce_code_verifier() challenge1 = _generate_pkce_code_challenge(verifier) challenge2 = _generate_pkce_code_challenge(verifier) assert challenge1 == challenge2 def test_generate_state(self): state = _generate_state() assert len(state) == 64 def test_detect_wsl2(self): result = detect_wsl2() assert isinstance(result, bool) def test_detect_ssh_session(self): result = detect_ssh_session() assert isinstance(result, bool) def test_needs_manual_oauth(self): result = needs_manual_oauth() assert isinstance(result, bool) class TestOAuthPKCEFlow: def test_build_authorize_url(self): flow = OAuthPKCEFlow(client_id="test-client-id", tenant_id="common") url = flow.build_authorize_url() assert "client_id=test-client-id" in url assert "code_challenge=" in url assert "code_challenge_method=S256" in url assert "response_type=code" in url def test_build_authorize_url_with_custom_tenant(self): flow = OAuthPKCEFlow(client_id="test-id", tenant_id="my-tenant") url = flow.build_authorize_url() assert "my-tenant" in url def test_build_authorize_url_with_custom_scopes(self): flow = OAuthPKCEFlow(client_id="test-id", scopes=["scope1", "scope2"]) url = flow.build_authorize_url() assert "scope1%20scope2" in url or "scope1+scope2" in url def test_build_authorize_url_with_custom_port(self): flow = OAuthPKCEFlow(client_id="test-id", redirect_port=8080) url = flow.build_authorize_url() assert "localhost%3A8080" in url or "localhost:8080" in url class TestSSO: def test_is_signin_invoke(self): assert is_signin_invoke({"name": "signin/tokenExchange"}) is True assert is_signin_invoke({"name": "signin/verifyState"}) is True assert is_signin_invoke({"name": "other"}) is False def test_is_token_exchange(self): assert is_token_exchange({"name": "signin/tokenExchange"}) is True assert is_token_exchange({"name": "signin/verifyState"}) is False def test_is_verify_state(self): assert is_verify_state({"name": "signin/verifyState"}) is True assert is_verify_state({"name": "signin/tokenExchange"}) is False def test_extract_signin_token(self): activity = {"value": {"token": "abc123", "id": "id-1", "state": "state-1"}} result = extract_signin_token(activity) assert result["token"] == "abc123" assert result["id"] == "id-1" assert result["state"] == "state-1" def test_extract_signin_token_empty(self): result = extract_signin_token({}) assert result["token"] == "" def test_build_oauth_card(self): card = build_oauth_card("conn-1", title="Login", text="Please login") assert card["type"] == "AdaptiveCard" assert len(card["actions"]) == 1 assert "conn-1" in card["actions"][0]["url"] def test_build_token_exchange_response(self): resp = build_token_exchange_response("activity-1", 200) assert resp["id"] == "activity-1" assert resp["status"] == 200 def test_build_sso_auth_activity(self): activity = build_sso_auth_activity("success", connection_name="conn-1") assert activity["text"] == "success" assert activity["channelData"]["oauthConnectionName"] == "conn-1" class TestSSOHandler: @pytest.fixture def handler(self): return SSOHandler(connection_name="test-conn", enabled=True) def test_default_disabled(self): handler = SSOHandler() assert handler.enabled is False def test_add_and_check_verified_state(self, handler): handler.add_verified_state("state-123") assert handler.is_state_verified("state-123") is True assert handler.is_state_verified("state-456") is False def test_check_sso_authorization_direct(self, handler): assert handler.check_sso_authorization("direct", "conv-1", dm_policy="open") is True assert handler.check_sso_authorization("direct", "conv-1", dm_policy="disabled") is False def test_check_sso_authorization_group(self, handler): assert handler.check_sso_authorization("group", "conv-1", group_policy="open") is True assert handler.check_sso_authorization("group", "conv-1", group_policy="disabled") is False def test_check_sso_authorization_channel(self, handler): assert handler.check_sso_authorization("channel", "conv-1", group_policy="open") is True def test_check_sso_authorization_unknown(self, handler): assert handler.check_sso_authorization("unknown", "conv-1") is False @pytest.mark.asyncio async def test_handle_signin_disabled(self): handler = SSOHandler(enabled=False) result = await handler.handle_signin({"name": "signin/tokenExchange"}) assert result is None @pytest.mark.asyncio async def test_handle_signin_token_exchange(self, handler): activity = { "id": "activity-1", "name": "signin/tokenExchange", "from": {"aadObjectId": "aad-1", "name": "Test", "id": "user-1"}, "value": {"token": "abc123", "state": "s1"}, } result = await handler.handle_signin(activity) assert result is not None assert result["user_id"] == "aad-1" @pytest.mark.asyncio async def test_handle_signin_verify_state(self, handler): activity = { "id": "activity-1", "name": "signin/verifyState", "value": {"state": "state-abc"}, } result = await handler.handle_signin(activity) assert result is not None assert result["state"] == "state-abc" assert handler.is_state_verified("state-abc") is True @pytest.mark.asyncio async def test_handle_signin_unknown(self, handler): result = await handler.handle_signin({"name": "unknown"}) assert result is None