170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
|
|
"""Unit tests for vk_sync task logic (price calc, name sanitization, orphan deletion)."""
|
|||
|
|
from datetime import datetime
|
|||
|
|
from decimal import Decimal
|
|||
|
|
from unittest.mock import MagicMock, call, patch
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from web.tasks.vk_sync import (
|
|||
|
|
_build_description,
|
|||
|
|
_calc_price,
|
|||
|
|
_delete_orphans,
|
|||
|
|
_is_weight,
|
|||
|
|
_name_for_vk,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── _is_weight ────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("measure,expected", [
|
|||
|
|
("г", True),
|
|||
|
|
("г.", True),
|
|||
|
|
("гр", True),
|
|||
|
|
("гр.", True),
|
|||
|
|
("грамм", True),
|
|||
|
|
("граммов", True),
|
|||
|
|
(" Г ", True), # case-insensitive, stripped
|
|||
|
|
("кг", False),
|
|||
|
|
("шт", False),
|
|||
|
|
("л", False),
|
|||
|
|
(None, False),
|
|||
|
|
("", False),
|
|||
|
|
])
|
|||
|
|
def test_is_weight(measure, expected):
|
|||
|
|
assert _is_weight(measure) == expected
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── _calc_price ───────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def test_calc_price_normal(monkeypatch):
|
|||
|
|
monkeypatch.setattr("web.tasks.vk_sync.settings.VK_WEIGHT_PRICE_MULTIPLIER", 10)
|
|||
|
|
assert _calc_price(Decimal("150"), "шт") == 15000 # 150 руб * 100 копеек
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_calc_price_weight_multiplier(monkeypatch):
|
|||
|
|
monkeypatch.setattr("web.tasks.vk_sync.settings.VK_WEIGHT_PRICE_MULTIPLIER", 10)
|
|||
|
|
# 50 руб/г → 50 * 10 (multiplier) * 100 (kopecks) = 50000
|
|||
|
|
assert _calc_price(Decimal("50"), "г") == 50000
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_calc_price_none():
|
|||
|
|
assert _calc_price(None, "шт") == 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_calc_price_zero():
|
|||
|
|
assert _calc_price(Decimal("0"), "шт") == 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── _name_for_vk ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def test_name_replaces_semicolons():
|
|||
|
|
assert _name_for_vk("Чай; зелёный; Китай") == "Чай, зелёный, Китай"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_name_no_semicolons():
|
|||
|
|
assert _name_for_vk("Пуэр (выдержанный)") == "Пуэр (выдержанный)"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── _build_description ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def test_build_description_weight(monkeypatch):
|
|||
|
|
monkeypatch.setattr("web.tasks.vk_sync.settings.VK_WEIGHT_PRICE_MULTIPLIER", 10)
|
|||
|
|
desc = _build_description("Чай", "г", None)
|
|||
|
|
assert "10г" in desc
|
|||
|
|
assert "Чай" in desc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_build_description_with_evo_desc(monkeypatch):
|
|||
|
|
monkeypatch.setattr("web.tasks.vk_sync.settings.VK_WEIGHT_PRICE_MULTIPLIER", 10)
|
|||
|
|
desc = _build_description("Чай", "шт", "Вкусный чай из Китая")
|
|||
|
|
assert "Вкусный чай из Китая" in desc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_build_description_no_evo_desc(monkeypatch):
|
|||
|
|
monkeypatch.setattr("web.tasks.vk_sync.settings.VK_WEIGHT_PRICE_MULTIPLIER", 10)
|
|||
|
|
desc = _build_description("Чай", "шт", None)
|
|||
|
|
assert "Чай" in desc
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── _delete_orphans ───────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def test_delete_orphans_removes_stale_vk_products():
|
|||
|
|
from web.models.connections import VkCachedProduct, CachedProduct
|
|||
|
|
|
|||
|
|
# Build fake VK cached products
|
|||
|
|
vk1 = MagicMock(spec=VkCachedProduct)
|
|||
|
|
vk1.vk_product_id = "111"
|
|||
|
|
vk1.name = "Существующий"
|
|||
|
|
|
|||
|
|
vk2 = MagicMock(spec=VkCachedProduct)
|
|||
|
|
vk2.vk_product_id = "222"
|
|||
|
|
vk2.name = "Удалённый из Эвотор"
|
|||
|
|
|
|||
|
|
db = MagicMock()
|
|||
|
|
# owned_ids contains only "111" — "222" is orphan
|
|||
|
|
owned_ids = {"111"}
|
|||
|
|
|
|||
|
|
# query().filter_by().filter().all() chain
|
|||
|
|
query_mock = MagicMock()
|
|||
|
|
query_mock.filter_by.return_value.filter.return_value.all.return_value = [vk2]
|
|||
|
|
# second query for stale cached_products
|
|||
|
|
query_mock.filter.return_value.all.return_value = []
|
|||
|
|
db.query.return_value = query_mock
|
|||
|
|
|
|||
|
|
results = {"deleted": 0, "errors": 0}
|
|||
|
|
mock_post_resp = {"response": 1}
|
|||
|
|
|
|||
|
|
with patch("web.tasks.vk_sync._vk_post", return_value=mock_post_resp):
|
|||
|
|
_delete_orphans(db, user_id=1, vk_group_id="99", owned_ids=owned_ids,
|
|||
|
|
token="tok", results=results)
|
|||
|
|
|
|||
|
|
assert results["deleted"] == 1
|
|||
|
|
db.delete.assert_called_once_with(vk2)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_delete_orphans_vk_api_error_counted():
|
|||
|
|
from web.models.connections import VkCachedProduct
|
|||
|
|
|
|||
|
|
vk1 = MagicMock(spec=VkCachedProduct)
|
|||
|
|
vk1.vk_product_id = "999"
|
|||
|
|
vk1.name = "Сломанный"
|
|||
|
|
|
|||
|
|
db = MagicMock()
|
|||
|
|
query_mock = MagicMock()
|
|||
|
|
query_mock.filter_by.return_value.filter.return_value.all.return_value = [vk1]
|
|||
|
|
query_mock.filter.return_value.all.return_value = []
|
|||
|
|
db.query.return_value = query_mock
|
|||
|
|
|
|||
|
|
results = {"deleted": 0, "errors": 0}
|
|||
|
|
|
|||
|
|
with patch("web.tasks.vk_sync._vk_post", return_value={"error": {"error_code": 15}}):
|
|||
|
|
_delete_orphans(db, user_id=1, vk_group_id="99", owned_ids={"other"},
|
|||
|
|
token="tok", results=results)
|
|||
|
|
|
|||
|
|
assert results["deleted"] == 0
|
|||
|
|
assert results["errors"] == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_delete_orphans_empty_owned_ids_deletes_all():
|
|||
|
|
"""If no Evotor products exist (owned_ids empty), all VK products are orphans."""
|
|||
|
|
from web.models.connections import VkCachedProduct
|
|||
|
|
|
|||
|
|
vk1 = MagicMock(spec=VkCachedProduct)
|
|||
|
|
vk1.vk_product_id = "1"
|
|||
|
|
vk1.name = "Лишний"
|
|||
|
|
|
|||
|
|
db = MagicMock()
|
|||
|
|
query_mock = MagicMock()
|
|||
|
|
# With empty owned_ids, query without .filter() is used
|
|||
|
|
query_mock.filter_by.return_value.all.return_value = [vk1]
|
|||
|
|
query_mock.filter.return_value.all.return_value = []
|
|||
|
|
db.query.return_value = query_mock
|
|||
|
|
|
|||
|
|
results = {"deleted": 0, "errors": 0}
|
|||
|
|
|
|||
|
|
with patch("web.tasks.vk_sync._vk_post", return_value={"response": 1}):
|
|||
|
|
_delete_orphans(db, user_id=1, vk_group_id="99", owned_ids=set(),
|
|||
|
|
token="tok", results=results)
|
|||
|
|
|
|||
|
|
assert results["deleted"] == 1
|