"""discord.py 유닛 테스트 — 파싱, 비디오 ID 추출, 임베드 구성.""" from unittest.mock import AsyncMock, patch import pytest from app.discord import _extract_video_id, _parse_summary, send_to_discord # ── _extract_video_id ── @pytest.mark.parametrize( "url, expected", [ ("https://youtu.be/abc123", "abc123"), ("https://www.youtube.com/watch?v=xyz789", "xyz789"), ("https://www.youtube.com/watch?v=xyz789&t=10", "xyz789"), ("https://youtu.be/abc123?si=something", "abc123"), ("https://example.com/no-video", None), ], ) def test_extract_video_id(url: str, expected: str | None): assert _extract_video_id(url) == expected # ── _parse_summary ── SAMPLE_SUMMARY_BOLD = """\ - **한줄 요약**: 한국 경제가 위기에 처했다. - **주요 내용**: - GDP 성장률 하락 - 수출 감소 - 환율 불안정 - **결론/시사점**: 정부의 적극적 대응이 필요하다. """ SAMPLE_SUMMARY_HEADING = """\ ## 한줄 요약 한국 경제가 위기에 처했다. ## 주요 내용 - GDP 성장률 하락 - 수출 감소 - 환율 불안정 ## 결론/시사점 정부의 적극적 대응이 필요하다. """ def test_parse_summary_bold_format(): result = _parse_summary(SAMPLE_SUMMARY_BOLD) assert "한줄요약" in result assert "한국 경제가 위기에 처했다." in result["한줄요약"] assert "주요내용" in result assert "GDP 성장률 하락" in result["주요내용"] assert "결론/시사점" in result or "결론시사점" in result def test_parse_summary_heading_format(): result = _parse_summary(SAMPLE_SUMMARY_HEADING) assert "한줄요약" in result assert "한국 경제가 위기에 처했다." in result["한줄요약"] assert "주요내용" in result assert "GDP 성장률 하락" in result["주요내용"] def test_parse_summary_empty(): result = _parse_summary("이건 파싱 안 되는 텍스트") assert result == {} # ── send_to_discord ── @pytest.mark.asyncio async def test_send_to_discord_embed_structure(): """send_to_discord가 올바른 임베드 구조로 웹훅을 호출하는지 확인.""" mock_response = AsyncMock() mock_response.raise_for_status = lambda: None with patch("app.discord.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client) mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False) await send_to_discord( title="테스트 영상", video_url="https://youtu.be/abc123", summary=SAMPLE_SUMMARY_BOLD, ) mock_client.post.assert_called_once() payload = mock_client.post.call_args[1]["json"] embed = payload["embeds"][0] assert "📰 테스트 영상" == embed["title"] assert embed["url"] == "https://youtu.be/abc123" assert embed["thumbnail"]["url"] == "https://img.youtube.com/vi/abc123/hqdefault.jpg" assert embed["footer"]["text"] == "YouTube 뉴스 요약 봇" assert "timestamp" in embed # 필드 확인 field_names = [f["name"] for f in embed["fields"]] assert "📋 주요 내용" in field_names assert "🎯 결론 / 시사점" in field_names assert "🔗 원본 영상" in field_names # description에 한줄 요약 포함 assert "한국 경제가 위기에 처했다" in embed["description"] @pytest.mark.asyncio async def test_send_to_discord_fallback_on_unparsable(): """파싱 실패 시 전체 summary가 description에 들어가는지 확인.""" mock_response = AsyncMock() mock_response.raise_for_status = lambda: None with patch("app.discord.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client) mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False) raw = "이건 섹션 없는 요약 텍스트입니다." await send_to_discord("테스트", "https://youtu.be/abc", raw) payload = mock_client.post.call_args[1]["json"] embed = payload["embeds"][0] assert embed["description"] == raw assert len(embed["fields"]) == 1 assert embed["fields"][0]["name"] == "🔗 원본 영상"