You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
6.2 KiB
153 lines
6.2 KiB
from datetime import time
|
|
from rest_framework import status
|
|
from rest_framework.test import APITestCase
|
|
from crawler.models import CrawlTask
|
|
from .models import Ad, AdEvaluation
|
|
|
|
class AdEvaluationAPITests(APITestCase):
|
|
def setUp(self):
|
|
self.task1 = CrawlTask.objects.create(
|
|
title="Task 1",
|
|
divar_url="https://divar.ir/s/tehran/buy-apartment",
|
|
detection_prompt="Prompt 1",
|
|
interval_minutes=15,
|
|
start_hour=time(8, 0),
|
|
end_hour=time(23, 0)
|
|
)
|
|
self.task2 = CrawlTask.objects.create(
|
|
title="Task 2",
|
|
divar_url="https://divar.ir/s/tehran/rent-apartment",
|
|
detection_prompt="Prompt 2",
|
|
interval_minutes=30,
|
|
start_hour=time(9, 0),
|
|
end_hour=time(22, 0)
|
|
)
|
|
|
|
self.ad1 = Ad.objects.create(
|
|
divar_token="token1",
|
|
title="Apartment in Tehran",
|
|
description="Beautiful apartment",
|
|
price="10,000,000,000",
|
|
category="buy-apartment",
|
|
images=["http://image1.jpg"],
|
|
url="https://divar.ir/v/token1"
|
|
)
|
|
self.ad2 = Ad.objects.create(
|
|
divar_token="token2",
|
|
title="Rent in Tehran",
|
|
description="Nice place",
|
|
price="500,000,000",
|
|
category="rent-apartment",
|
|
images=[],
|
|
url="https://divar.ir/v/token2"
|
|
)
|
|
|
|
self.eval1 = AdEvaluation.objects.create(
|
|
crawl_task=self.task1,
|
|
ad=self.ad1,
|
|
is_flagged=True,
|
|
reason="Matches perfectly",
|
|
confidence=0.95,
|
|
extracted_fields={"urgency": "high"}
|
|
)
|
|
self.eval2 = AdEvaluation.objects.create(
|
|
crawl_task=self.task2,
|
|
ad=self.ad2,
|
|
is_flagged=False,
|
|
reason="Does not match",
|
|
confidence=0.1,
|
|
extracted_fields={}
|
|
)
|
|
|
|
self.list_url = "/api/ads/"
|
|
|
|
def test_get_evaluations_list(self):
|
|
response = self.client.get(self.list_url)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data), 2)
|
|
# Check that ad details are nested
|
|
self.assertEqual(response.data[0]['ad']['divar_token'], self.ad2.divar_token) # Ordered by -evaluated_at
|
|
self.assertEqual(response.data[1]['ad']['divar_token'], self.ad1.divar_token)
|
|
|
|
def test_filter_by_crawl_task(self):
|
|
response = self.client.get(self.list_url, {"crawl_task": self.task1.id})
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data), 1)
|
|
self.assertEqual(response.data[0]['id'], str(self.eval1.id))
|
|
|
|
def test_filter_by_is_flagged_true(self):
|
|
response = self.client.get(self.list_url, {"is_flagged": "true"})
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data), 1)
|
|
self.assertEqual(response.data[0]['id'], str(self.eval1.id))
|
|
self.assertTrue(response.data[0]['is_flagged'])
|
|
|
|
def test_filter_by_is_flagged_false(self):
|
|
response = self.client.get(self.list_url, {"is_flagged": "false"})
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data), 1)
|
|
self.assertEqual(response.data[0]['id'], str(self.eval2.id))
|
|
self.assertFalse(response.data[0]['is_flagged'])
|
|
|
|
def test_get_evaluation_detail(self):
|
|
detail_url = f"{self.list_url}{self.eval1.id}/"
|
|
response = self.client.get(detail_url)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['id'], str(self.eval1.id))
|
|
self.assertEqual(response.data['ad']['title'], self.ad1.title)
|
|
|
|
def test_delete_ad_evaluation(self):
|
|
detail_url = f"{self.list_url}{self.eval1.id}/"
|
|
response = self.client.delete(detail_url)
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
self.assertFalse(AdEvaluation.objects.filter(id=self.eval1.id).exists())
|
|
|
|
|
|
from unittest.mock import MagicMock
|
|
from ads.tasks import extract_and_parse_json, call_llm_with_structured_fallback, AdEvaluationResult
|
|
|
|
class AdTasksHelperTests(APITestCase):
|
|
def test_extract_and_parse_json_raw(self):
|
|
raw = '{"is_flagged": true, "reason": "مناسب است", "confidence": 0.9}'
|
|
res = extract_and_parse_json(raw)
|
|
self.assertTrue(res['is_flagged'])
|
|
self.assertEqual(res['reason'], 'مناسب است')
|
|
|
|
def test_extract_and_parse_json_markdown_fence(self):
|
|
raw = '```json\n{\n "is_flagged": false,\n "reason": "نامناسب",\n "confidence": 0.2\n}\n```'
|
|
res = extract_and_parse_json(raw)
|
|
self.assertFalse(res['is_flagged'])
|
|
|
|
def test_extract_and_parse_json_with_preamble(self):
|
|
raw = 'Here is the response:\n```json\n{"is_flagged": true, "reason": "خوب", "confidence": 0.8}\n```\nHope this helps!'
|
|
res = extract_and_parse_json(raw)
|
|
self.assertTrue(res['is_flagged'])
|
|
|
|
def test_call_llm_fallback_on_markdown(self):
|
|
mock_client = MagicMock()
|
|
# beta parse raises exception (simulating markdown parse failure)
|
|
mock_client.beta.chat.completions.parse.side_effect = Exception("Invalid JSON markdown")
|
|
|
|
# standard completion returns markdown
|
|
mock_message = MagicMock()
|
|
mock_message.content = '```json\n{"is_flagged": true, "reason": "پرچم گذاری شد", "confidence": 0.95}\n```'
|
|
mock_choice = MagicMock()
|
|
mock_choice.message = mock_message
|
|
mock_completion = MagicMock()
|
|
mock_completion.choices = [mock_choice]
|
|
mock_client.chat.completions.create.return_value = mock_completion
|
|
|
|
res = call_llm_with_structured_fallback(
|
|
client=mock_client,
|
|
model_name="openrouter/free",
|
|
system_instruction="sys",
|
|
user_content="user",
|
|
pydantic_cls=AdEvaluationResult,
|
|
max_tokens=2500,
|
|
timeout=35
|
|
)
|
|
self.assertTrue(res.is_flagged)
|
|
self.assertEqual(res.reason, "پرچم گذاری شد")
|
|
self.assertEqual(res.confidence, 0.95)
|
|
|
|
|