Files
evo-sync/tests/test_tasks_vk_sync.py
mguschin 7b4f52b005 feat: VK OAuth flow, catalog sync improvements, and expanded test suite
- 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>
2026-05-12 15:09:47 +03:00

170 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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