test(messages): add unit tests for conversation models and link preview
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5841116ff1
commit
bca1decf97
242
tests/unit/test_conversation_models.py
Normal file
242
tests/unit/test_conversation_models.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
"""
|
||||||
|
Unit Tests — Conversation Models
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Tests for the new SQLAlchemy conversation models (mock-based, no DB required):
|
||||||
|
- Conversation.display_name
|
||||||
|
- Conversation.member_count
|
||||||
|
- ConversationMember.is_owner
|
||||||
|
- ConvMessage.__repr__
|
||||||
|
- MessageReaction unique constraint
|
||||||
|
- Model imports
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Import Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestModelImports:
|
||||||
|
"""Verify all 5 conversation models can be imported from database."""
|
||||||
|
|
||||||
|
def test_import_conversation(self):
|
||||||
|
from database import Conversation
|
||||||
|
assert Conversation is not None
|
||||||
|
|
||||||
|
def test_import_conversation_member(self):
|
||||||
|
from database import ConversationMember
|
||||||
|
assert ConversationMember is not None
|
||||||
|
|
||||||
|
def test_import_conv_message(self):
|
||||||
|
from database import ConvMessage
|
||||||
|
assert ConvMessage is not None
|
||||||
|
|
||||||
|
def test_import_message_reaction(self):
|
||||||
|
from database import MessageReaction
|
||||||
|
assert MessageReaction is not None
|
||||||
|
|
||||||
|
def test_import_message_attachment(self):
|
||||||
|
from database import MessageAttachment
|
||||||
|
assert MessageAttachment is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Helpers
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _make_mock_member(name=None, email='user@example.com'):
|
||||||
|
"""Create a MagicMock simulating a ConversationMember with a User."""
|
||||||
|
member = MagicMock()
|
||||||
|
user = MagicMock()
|
||||||
|
user.name = name
|
||||||
|
user.email = email
|
||||||
|
member.user = user
|
||||||
|
return member
|
||||||
|
|
||||||
|
|
||||||
|
def _make_conversation(name=None, members=None):
|
||||||
|
"""Create a MagicMock simulating a Conversation instance."""
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock(spec=Conversation)
|
||||||
|
# Attach the real property logic by calling it on the mock
|
||||||
|
conv.name = name
|
||||||
|
conv.members = members or []
|
||||||
|
# Bind the real display_name property to this mock
|
||||||
|
conv.display_name = Conversation.display_name.fget(conv)
|
||||||
|
conv.member_count = Conversation.member_count.fget(conv)
|
||||||
|
return conv
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Conversation.display_name Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestConversationDisplayName:
|
||||||
|
"""Test Conversation.display_name property."""
|
||||||
|
|
||||||
|
def test_display_name_returns_name_when_set(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.name = 'Projekt Alpha'
|
||||||
|
conv.members = []
|
||||||
|
result = Conversation.display_name.fget(conv)
|
||||||
|
assert result == 'Projekt Alpha'
|
||||||
|
|
||||||
|
def test_display_name_joins_member_names_when_no_name(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.name = None
|
||||||
|
conv.members = [
|
||||||
|
_make_mock_member(name='Anna'),
|
||||||
|
_make_mock_member(name='Bob'),
|
||||||
|
]
|
||||||
|
result = Conversation.display_name.fget(conv)
|
||||||
|
assert 'Anna' in result
|
||||||
|
assert 'Bob' in result
|
||||||
|
|
||||||
|
def test_display_name_uses_email_prefix_when_user_has_no_name(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.name = None
|
||||||
|
conv.members = [
|
||||||
|
_make_mock_member(name=None, email='jan.kowalski@example.com'),
|
||||||
|
]
|
||||||
|
result = Conversation.display_name.fget(conv)
|
||||||
|
assert 'jan.kowalski' in result
|
||||||
|
|
||||||
|
def test_display_name_truncates_beyond_four_members(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.name = None
|
||||||
|
conv.members = [
|
||||||
|
_make_mock_member(name=f'User{i}', email=f'user{i}@example.com')
|
||||||
|
for i in range(6)
|
||||||
|
]
|
||||||
|
result = Conversation.display_name.fget(conv)
|
||||||
|
assert '+2' in result
|
||||||
|
|
||||||
|
def test_display_name_no_suffix_for_four_or_fewer_members(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.name = None
|
||||||
|
conv.members = [
|
||||||
|
_make_mock_member(name=f'User{i}', email=f'user{i}@example.com')
|
||||||
|
for i in range(4)
|
||||||
|
]
|
||||||
|
result = Conversation.display_name.fget(conv)
|
||||||
|
assert '+' not in result
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Conversation.member_count Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestConversationMemberCount:
|
||||||
|
"""Test Conversation.member_count property."""
|
||||||
|
|
||||||
|
def test_member_count_returns_length_of_members(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.members = [MagicMock(), MagicMock(), MagicMock()]
|
||||||
|
assert Conversation.member_count.fget(conv) == 3
|
||||||
|
|
||||||
|
def test_member_count_zero_when_no_members(self):
|
||||||
|
from database import Conversation
|
||||||
|
conv = MagicMock()
|
||||||
|
conv.members = []
|
||||||
|
assert Conversation.member_count.fget(conv) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ConversationMember.is_owner Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestConversationMemberIsOwner:
|
||||||
|
"""Test ConversationMember.is_owner property."""
|
||||||
|
|
||||||
|
def test_is_owner_true_when_role_is_owner(self):
|
||||||
|
from database import ConversationMember
|
||||||
|
cm = MagicMock()
|
||||||
|
cm.role = 'owner'
|
||||||
|
assert ConversationMember.is_owner.fget(cm) is True
|
||||||
|
|
||||||
|
def test_is_owner_false_when_role_is_member(self):
|
||||||
|
from database import ConversationMember
|
||||||
|
cm = MagicMock()
|
||||||
|
cm.role = 'member'
|
||||||
|
assert ConversationMember.is_owner.fget(cm) is False
|
||||||
|
|
||||||
|
def test_is_owner_false_when_role_is_admin(self):
|
||||||
|
from database import ConversationMember
|
||||||
|
cm = MagicMock()
|
||||||
|
cm.role = 'admin'
|
||||||
|
assert ConversationMember.is_owner.fget(cm) is False
|
||||||
|
|
||||||
|
def test_is_owner_false_when_role_is_empty(self):
|
||||||
|
from database import ConversationMember
|
||||||
|
cm = MagicMock()
|
||||||
|
cm.role = ''
|
||||||
|
assert ConversationMember.is_owner.fget(cm) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ConvMessage.__repr__ Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestConvMessageRepr:
|
||||||
|
"""Test ConvMessage.__repr__ returns expected format."""
|
||||||
|
|
||||||
|
def test_repr_format(self):
|
||||||
|
from database import ConvMessage
|
||||||
|
msg = MagicMock()
|
||||||
|
msg.id = 42
|
||||||
|
msg.conversation_id = 7
|
||||||
|
msg.sender_id = 13
|
||||||
|
# Call the actual __repr__ method on the mock instance
|
||||||
|
result = ConvMessage.__repr__(msg)
|
||||||
|
assert '42' in result
|
||||||
|
assert '7' in result
|
||||||
|
assert '13' in result
|
||||||
|
|
||||||
|
def test_repr_contains_class_indicator(self):
|
||||||
|
from database import ConvMessage
|
||||||
|
msg = MagicMock()
|
||||||
|
msg.id = 1
|
||||||
|
msg.conversation_id = 2
|
||||||
|
msg.sender_id = 3
|
||||||
|
assert 'ConvMessage' in ConvMessage.__repr__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MessageReaction UniqueConstraint Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestMessageReactionUniqueConstraint:
|
||||||
|
"""Verify MessageReaction __table_args__ contains UniqueConstraint."""
|
||||||
|
|
||||||
|
def test_table_args_contains_unique_constraint(self):
|
||||||
|
from database import MessageReaction
|
||||||
|
table_args = MessageReaction.__table_args__
|
||||||
|
assert table_args is not None
|
||||||
|
constraint_types = [type(a) for a in table_args]
|
||||||
|
assert UniqueConstraint in constraint_types
|
||||||
|
|
||||||
|
def test_unique_constraint_covers_message_user_emoji(self):
|
||||||
|
from database import MessageReaction
|
||||||
|
table_args = MessageReaction.__table_args__
|
||||||
|
unique_constraints = [a for a in table_args if isinstance(a, UniqueConstraint)]
|
||||||
|
assert len(unique_constraints) >= 1
|
||||||
|
constraint = unique_constraints[0]
|
||||||
|
col_names = [col.key for col in constraint.columns]
|
||||||
|
assert 'message_id' in col_names
|
||||||
|
assert 'user_id' in col_names
|
||||||
|
assert 'emoji' in col_names
|
||||||
262
tests/unit/test_link_preview.py
Normal file
262
tests/unit/test_link_preview.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
"""
|
||||||
|
Unit Tests — Link Preview
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Tests for blueprints/messages/link_preview.py:
|
||||||
|
- OGParser with og: meta tags
|
||||||
|
- OGParser fallback to <title> and meta description
|
||||||
|
- OGParser with no meta tags
|
||||||
|
- fetch_link_preview with no URL
|
||||||
|
- fetch_link_preview skips internal URLs
|
||||||
|
- fetch_link_preview success (mocked HTTP)
|
||||||
|
- fetch_link_preview timeout handling
|
||||||
|
- fetch_link_preview non-HTML content-type
|
||||||
|
- URL extraction from HTML anchor tags
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from requests.exceptions import Timeout
|
||||||
|
|
||||||
|
from blueprints.messages.link_preview import fetch_link_preview, OGParser
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# OGParser Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestOGParser:
|
||||||
|
"""Test OGParser HTML parsing."""
|
||||||
|
|
||||||
|
def test_parses_og_title_description_image(self):
|
||||||
|
html = """
|
||||||
|
<html><head>
|
||||||
|
<meta property="og:title" content="Test Title">
|
||||||
|
<meta property="og:description" content="Test Description">
|
||||||
|
<meta property="og:image" content="https://example.com/image.jpg">
|
||||||
|
</head></html>
|
||||||
|
"""
|
||||||
|
parser = OGParser()
|
||||||
|
parser.feed(html)
|
||||||
|
assert parser.og['title'] == 'Test Title'
|
||||||
|
assert parser.og['description'] == 'Test Description'
|
||||||
|
assert parser.og['image'] == 'https://example.com/image.jpg'
|
||||||
|
|
||||||
|
def test_fallback_to_title_tag_and_meta_description(self):
|
||||||
|
html = """
|
||||||
|
<html><head>
|
||||||
|
<title>Fallback Title</title>
|
||||||
|
<meta name="description" content="Fallback Description">
|
||||||
|
</head></html>
|
||||||
|
"""
|
||||||
|
parser = OGParser()
|
||||||
|
parser.feed(html)
|
||||||
|
assert parser.title == 'Fallback Title'
|
||||||
|
assert parser.og.get('description') == 'Fallback Description'
|
||||||
|
assert 'title' not in parser.og # og:title not set
|
||||||
|
|
||||||
|
def test_empty_html_returns_title_from_title_tag(self):
|
||||||
|
html = "<html><head><title>Only Title</title></head></html>"
|
||||||
|
parser = OGParser()
|
||||||
|
parser.feed(html)
|
||||||
|
assert parser.title == 'Only Title'
|
||||||
|
assert parser.og.get('description') is None
|
||||||
|
assert parser.og.get('image') is None
|
||||||
|
|
||||||
|
def test_no_meta_tags_empty_og(self):
|
||||||
|
html = "<html><head></head><body>No meta here</body></html>"
|
||||||
|
parser = OGParser()
|
||||||
|
parser.feed(html)
|
||||||
|
assert parser.og == {}
|
||||||
|
assert parser.title is None
|
||||||
|
|
||||||
|
def test_og_description_takes_precedence_over_meta_description(self):
|
||||||
|
html = """
|
||||||
|
<html><head>
|
||||||
|
<meta property="og:description" content="OG Desc">
|
||||||
|
<meta name="description" content="Meta Desc">
|
||||||
|
</head></html>
|
||||||
|
"""
|
||||||
|
parser = OGParser()
|
||||||
|
parser.feed(html)
|
||||||
|
assert parser.og['description'] == 'OG Desc'
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# fetch_link_preview Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestFetchLinkPreview:
|
||||||
|
"""Test fetch_link_preview function."""
|
||||||
|
|
||||||
|
def test_returns_none_for_none_text(self):
|
||||||
|
result = fetch_link_preview(None)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_for_empty_text(self):
|
||||||
|
result = fetch_link_preview('')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_when_no_url_in_text(self):
|
||||||
|
result = fetch_link_preview('Cześć, jak się masz?')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_for_internal_nordabiznes_url(self):
|
||||||
|
result = fetch_link_preview('Sprawdź https://nordabiznes.pl/company/test')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_for_staging_internal_url(self):
|
||||||
|
result = fetch_link_preview('Link: https://staging.nordabiznes.pl/company/foo')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_for_localhost_url(self):
|
||||||
|
result = fetch_link_preview('Dev: http://localhost:5000/test')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_success_returns_dict_with_og_data(self):
|
||||||
|
html = """<html><head>
|
||||||
|
<meta property="og:title" content="Example Title">
|
||||||
|
<meta property="og:description" content="Example Description">
|
||||||
|
<meta property="og:image" content="https://example.com/img.jpg">
|
||||||
|
</head></html>"""
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html; charset=utf-8'}
|
||||||
|
mock_resp.text = html
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('Check out https://example.com')
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result['url'] == 'https://example.com'
|
||||||
|
assert result['title'] == 'Example Title'
|
||||||
|
assert result['description'] == 'Example Description'
|
||||||
|
assert result['image'] == 'https://example.com/img.jpg'
|
||||||
|
|
||||||
|
def test_success_uses_title_tag_fallback(self):
|
||||||
|
html = "<html><head><title>Page Title</title></head></html>"
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = html
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('See https://example.com for details')
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result['title'] == 'Page Title'
|
||||||
|
|
||||||
|
def test_returns_none_on_timeout(self):
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', side_effect=Timeout):
|
||||||
|
result = fetch_link_preview('Visit https://slow-site.example.com')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_for_non_html_content_type(self):
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'application/pdf'}
|
||||||
|
mock_resp.text = '%PDF-1.4 binary content'
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('Download https://example.com/doc.pdf')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_returns_none_when_page_has_no_title(self):
|
||||||
|
html = "<html><head><meta name='robots' content='noindex'></head></html>"
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = html
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('Visit https://example.com')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_title_truncated_to_200_chars(self):
|
||||||
|
long_title = 'A' * 300
|
||||||
|
html = f"<html><head><title>{long_title}</title></head></html>"
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = html
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('https://example.com')
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert len(result['title']) <= 200
|
||||||
|
|
||||||
|
def test_description_truncated_to_300_chars(self):
|
||||||
|
long_desc = 'B' * 400
|
||||||
|
html = f"""<html><head>
|
||||||
|
<title>Title</title>
|
||||||
|
<meta name="description" content="{long_desc}">
|
||||||
|
</head></html>"""
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = html
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview('https://example.com')
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert len(result['description']) <= 300
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# URL Extraction from HTML Content Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class TestURLExtractionFromHTML:
|
||||||
|
"""Test that URLs inside HTML anchor tags are correctly found."""
|
||||||
|
|
||||||
|
def test_extracts_url_from_anchor_tag(self):
|
||||||
|
"""URL inside <a href> is extracted after stripping HTML tags."""
|
||||||
|
text = '<a href="https://external-site.com/page">Visit site</a>'
|
||||||
|
# The function strips HTML tags before extracting URLs,
|
||||||
|
# so href URL is not extracted — only bare URLs in text are.
|
||||||
|
# This test verifies the stripping behavior: no URL in visible text → None.
|
||||||
|
result = fetch_link_preview(text)
|
||||||
|
# After stripping tags, text is "Visit site" — no URL → None
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_extracts_bare_url_from_mixed_html(self):
|
||||||
|
"""Bare URL in text alongside HTML is extracted correctly."""
|
||||||
|
text = '<p>Check out https://example.com/news for more</p>'
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = '<html><head><title>News</title></head></html>'
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview(text)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result['url'] == 'https://example.com/news'
|
||||||
|
|
||||||
|
def test_first_url_is_used_when_multiple_urls_present(self):
|
||||||
|
"""When text contains multiple URLs, the first one is used."""
|
||||||
|
text = 'First: https://first.example.com and second: https://second.example.com'
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.headers = {'content-type': 'text/html'}
|
||||||
|
mock_resp.text = '<html><head><title>First</title></head></html>'
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch('blueprints.messages.link_preview.requests.get', return_value=mock_resp):
|
||||||
|
result = fetch_link_preview(text)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result['url'] == 'https://first.example.com'
|
||||||
Loading…
Reference in New Issue
Block a user