- Add VK OAuth implicit flow: /vk-auth redirect, /vk-callback JS page, /vk-callback/save endpoint with state validation - Add VK_CLIENT_ID/VK_CLIENT_SECRET to config - Add refresh_token/token_expires_at columns to vk_connections (migration 0006) - Fix vk_catalog task: handle price/thumb_photo as string or dict (VK API v5.199) - Fix connections/vk/test: use groups.getById instead of market.getAlbums (works with both user and group tokens) - Add orphan deletion to mirror_to_vk: VK products not in Evotor are removed - Handle ungrouped Evotor products: push to "Без категории" VK album - Respect SyncConfig.is_enabled in mirror_to_vk - Add product count column to catalog groups page - Add group name column to catalog products page - Expand test suite: 73 new tests covering connections routes, catalog routes, vk_sync task logic, and catalog task helpers (138 total, all passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
|