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
|