- 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>
276 lines
11 KiB
Python
276 lines
11 KiB
Python
"""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
|