Files
evo-sync/tests/test_routes_catalog.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

276 lines
11 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.
"""Integration tests for /catalog routes (stores, groups, products, toggles)."""
from datetime import datetime
import pytest
from web.models.connections import (
CachedGroup, CachedProduct, CachedStore, SyncConfig, SyncFilter,
)
def _now():
return datetime.utcnow()
async def _login(client, user):
await client.post("/login", data={"email": user.email, "password": "testpass123"},
follow_redirects=False)
def _make_store(db, user_id, evotor_id="s1", name="Магазин 1"):
s = CachedStore(user_id=user_id, evotor_id=evotor_id, name=name, fetched_at=_now())
db.add(s)
db.flush()
return s
def _make_group(db, user_id, store_id, evotor_id="g1", name="Группа 1"):
g = CachedGroup(user_id=user_id, store_evotor_id=store_id,
evotor_id=evotor_id, name=name, fetched_at=_now())
db.add(g)
db.flush()
return g
def _make_product(db, user_id, store_id, group_id=None, evotor_id="p1", name="Товар 1",
price=100, allow_to_sell=True):
p = CachedProduct(
user_id=user_id, store_evotor_id=store_id, group_evotor_id=group_id,
evotor_id=evotor_id, name=name, price=price, allow_to_sell=allow_to_sell,
fetched_at=_now(),
)
db.add(p)
db.flush()
return p
# ── auth guards ───────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_catalog_stores_requires_login(client):
resp = await client.get("/catalog/stores", follow_redirects=False)
assert resp.status_code == 303
assert "/login" in resp.headers["location"]
# ── GET /catalog/stores ───────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_catalog_stores_empty(client, active_user):
await _login(client, active_user)
resp = await client.get("/catalog/stores")
assert resp.status_code == 200
assert "не загружены" in resp.text.lower()
@pytest.mark.asyncio
async def test_catalog_stores_lists_stores(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1", "Главный магазин")
_make_store(override_db, active_user.id, "s2", "Второй магазин")
override_db.commit()
resp = await client.get("/catalog/stores")
assert resp.status_code == 200
assert "Главный магазин" in resp.text
assert "Второй магазин" in resp.text
@pytest.mark.asyncio
async def test_catalog_stores_not_shows_other_user(client, active_user, user_factory, override_db):
await _login(client, active_user)
other = user_factory.create()
_make_store(override_db, other.id, "s-other", "Чужой магазин")
override_db.commit()
resp = await client.get("/catalog/stores")
assert "Чужой магазин" not in resp.text
# ── GET /catalog/stores/{id}/groups ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_catalog_groups_shows_product_count(client, active_user, override_db):
await _login(client, active_user)
store = _make_store(override_db, active_user.id, "s1")
group = _make_group(override_db, active_user.id, "s1", "g1", "Чай")
_make_product(override_db, active_user.id, "s1", "g1", "p1", "Пуэр")
_make_product(override_db, active_user.id, "s1", "g1", "p2", "Улун")
override_db.commit()
resp = await client.get(f"/catalog/stores/s1/groups")
assert resp.status_code == 200
assert "Чай" in resp.text
assert "2" in resp.text # product count
@pytest.mark.asyncio
async def test_catalog_groups_zero_count_for_empty_group(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1", "Пустая группа")
override_db.commit()
resp = await client.get("/catalog/stores/s1/groups")
assert resp.status_code == 200
assert "Пустая группа" in resp.text
assert "0" in resp.text
@pytest.mark.asyncio
async def test_catalog_groups_unknown_store_redirects(client, active_user):
await _login(client, active_user)
resp = await client.get("/catalog/stores/no-such-store/groups", follow_redirects=False)
assert resp.status_code == 303
# ── GET /catalog/stores/{id}/products ────────────────────────────────────────
@pytest.mark.asyncio
async def test_catalog_products_shows_all(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1")
_make_product(override_db, active_user.id, "s1", "g1", "p1", "Пуэр")
_make_product(override_db, active_user.id, "s1", "g1", "p2", "Улун")
override_db.commit()
resp = await client.get("/catalog/stores/s1/products")
assert resp.status_code == 200
assert "Пуэр" in resp.text
assert "Улун" in resp.text
@pytest.mark.asyncio
async def test_catalog_products_filtered_by_group(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1", "Группа А")
_make_group(override_db, active_user.id, "s1", "g2", "Группа Б")
_make_product(override_db, active_user.id, "s1", "g1", "p1", "Товар А")
_make_product(override_db, active_user.id, "s1", "g2", "p2", "Товар Б")
override_db.commit()
resp = await client.get("/catalog/stores/s1/products?group=g1")
assert resp.status_code == 200
assert "Товар А" in resp.text
assert "Товар Б" not in resp.text
@pytest.mark.asyncio
async def test_catalog_products_shows_group_column(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1", "МояГруппа")
_make_product(override_db, active_user.id, "s1", "g1", "p1", "Товар")
override_db.commit()
resp = await client.get("/catalog/stores/s1/products")
assert "МояГруппа" in resp.text
@pytest.mark.asyncio
async def test_catalog_products_ungrouped_shown(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_product(override_db, active_user.id, "s1", None, "p1", "Без группы")
override_db.commit()
resp = await client.get("/catalog/stores/s1/products")
assert "Без группы" in resp.text
# ── POST /catalog/stores/{id}/toggle ─────────────────────────────────────────
@pytest.mark.asyncio
async def test_store_toggle_first_disable_seeds_others(client, active_user, override_db):
"""First toggle on a store disables it by seeding include-filters for all other stores."""
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1", "Магазин 1")
_make_store(override_db, active_user.id, "s2", "Магазин 2")
_make_store(override_db, active_user.id, "s3", "Магазин 3")
override_db.commit()
resp = await client.post("/catalog/stores/s1/toggle", follow_redirects=False)
assert resp.status_code == 303
cfg = override_db.query(SyncConfig).filter_by(user_id=active_user.id).first()
filters = override_db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="store", filter_mode="include"
).all()
ids = {f.entity_id for f in filters}
# s1 was toggled off → only s2 and s3 are in include list
assert "s1" not in ids
assert "s2" in ids
assert "s3" in ids
@pytest.mark.asyncio
async def test_store_toggle_re_enable(client, active_user, override_db):
"""Toggling a disabled store re-adds it to the include list."""
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_store(override_db, active_user.id, "s2")
override_db.commit()
# Disable s1 first
await client.post("/catalog/stores/s1/toggle")
# Now re-enable s1
await client.post("/catalog/stores/s1/toggle")
cfg = override_db.query(SyncConfig).filter_by(user_id=active_user.id).first()
filters = override_db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="store", filter_mode="include"
).all()
ids = {f.entity_id for f in filters}
assert "s1" in ids
@pytest.mark.asyncio
async def test_store_toggle_requires_login(client):
resp = await client.post("/catalog/stores/s1/toggle", follow_redirects=False)
assert resp.status_code == 303
assert "/login" in resp.headers["location"]
# ── POST /catalog/stores/{id}/groups/{gid}/toggle ────────────────────────────
@pytest.mark.asyncio
async def test_group_toggle_first_disable(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1", "Группа 1")
_make_group(override_db, active_user.id, "s1", "g2", "Группа 2")
override_db.commit()
resp = await client.post("/catalog/stores/s1/groups/g1/toggle", follow_redirects=False)
assert resp.status_code == 303
cfg = override_db.query(SyncConfig).filter_by(user_id=active_user.id).first()
filters = override_db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="group", filter_mode="include",
parent_entity_id="s1",
).all()
ids = {f.entity_id for f in filters}
assert "g1" not in ids
assert "g2" in ids
@pytest.mark.asyncio
async def test_group_toggle_re_enable(client, active_user, override_db):
await _login(client, active_user)
_make_store(override_db, active_user.id, "s1")
_make_group(override_db, active_user.id, "s1", "g1")
_make_group(override_db, active_user.id, "s1", "g2")
override_db.commit()
await client.post("/catalog/stores/s1/groups/g1/toggle")
await client.post("/catalog/stores/s1/groups/g1/toggle")
cfg = override_db.query(SyncConfig).filter_by(user_id=active_user.id).first()
filters = override_db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="group", parent_entity_id="s1"
).all()
ids = {f.entity_id for f in filters}
assert "g1" in ids