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

353 lines
13 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 /connections routes."""
import secrets
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from web.models.connections import EvotorConnection, VkConnection
def _login(client, user):
client.cookies.clear()
return client.post("/login", data={"email": user.email, "password": "testpass123"},
follow_redirects=False)
# ── auth guard ────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_requires_login(client):
resp = await client.get("/connections", follow_redirects=False)
assert resp.status_code == 303
assert "/login" in resp.headers["location"]
# ── GET /connections ──────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_get_no_connections(client, active_user):
await _login(client, active_user)
resp = await client.get("/connections")
assert resp.status_code == 200
assert "Эвотор" in resp.text
assert "ВКонтакте" in resp.text
assert "Не подключено" in resp.text
@pytest.mark.asyncio
async def test_connections_get_shows_connected(client, active_user, override_db):
await _login(client, active_user)
conn = EvotorConnection(
user_id=active_user.id,
evotor_user_id="evo-123",
access_token="tok-abc",
api_token="api-tok",
connected_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
resp = await client.get("/connections")
assert resp.status_code == 200
assert "Подключено" in resp.text
assert "tok-abc"[:8] in resp.text
# ── POST /connections/evotor ──────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_evotor_post_creates(client, active_user, override_db):
await _login(client, active_user)
resp = await client.post("/connections/evotor", data={
"access_token": "new-evotor-token",
"evotor_user_id": "",
}, follow_redirects=False)
assert resp.status_code == 303
assert "success=1" in resp.headers["location"]
conn = override_db.query(EvotorConnection).filter_by(user_id=active_user.id).first()
assert conn is not None
assert conn.access_token == "new-evotor-token"
assert conn.api_token is not None
@pytest.mark.asyncio
async def test_connections_evotor_post_updates(client, active_user, override_db):
await _login(client, active_user)
conn = EvotorConnection(
user_id=active_user.id, evotor_user_id="evo-upd",
access_token="old-token", api_token="api",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
await client.post("/connections/evotor", data={"access_token": "updated-token"})
override_db.refresh(conn)
assert conn.access_token == "updated-token"
@pytest.mark.asyncio
async def test_connections_evotor_post_empty_token(client, active_user):
await _login(client, active_user)
resp = await client.post("/connections/evotor", data={"access_token": ""})
assert resp.status_code == 200
assert "обязателен" in resp.text.lower()
# ── POST /connections/evotor/disconnect ───────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_evotor_disconnect(client, active_user, override_db):
await _login(client, active_user)
conn = EvotorConnection(
user_id=active_user.id, evotor_user_id="evo-del",
access_token="tok", api_token="api",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
resp = await client.post("/connections/evotor/disconnect", follow_redirects=False)
assert resp.status_code == 303
assert override_db.query(EvotorConnection).filter_by(user_id=active_user.id).first() is None
# ── POST /connections/vk (manual token) ──────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_vk_post_creates(client, active_user, override_db):
await _login(client, active_user)
resp = await client.post("/connections/vk", data={
"access_token": "vk1.a.testtoken",
"vk_group_id": "123456789",
}, follow_redirects=False)
assert resp.status_code == 303
assert "success=1" in resp.headers["location"]
conn = override_db.query(VkConnection).filter_by(user_id=active_user.id).first()
assert conn is not None
assert conn.access_token == "vk1.a.testtoken"
assert conn.vk_user_id == "123456789"
@pytest.mark.asyncio
async def test_connections_vk_post_empty_token(client, active_user):
await _login(client, active_user)
resp = await client.post("/connections/vk", data={"access_token": "", "vk_group_id": ""})
assert resp.status_code == 200
assert "обязателен" in resp.text.lower()
# ── POST /connections/vk/disconnect ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_connections_vk_disconnect(client, active_user, override_db):
await _login(client, active_user)
conn = VkConnection(
user_id=active_user.id, access_token="vk-tok",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
resp = await client.post("/connections/vk/disconnect", follow_redirects=False)
assert resp.status_code == 303
assert override_db.query(VkConnection).filter_by(user_id=active_user.id).first() is None
# ── GET /vk-auth ──────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_vk_auth_redirects_to_vk(client, active_user, monkeypatch):
monkeypatch.setattr("web.routes.connections.settings.VK_CLIENT_ID", "53265827")
monkeypatch.setattr("web.routes.connections.settings.BASE_URL", "http://test")
await _login(client, active_user)
resp = await client.get("/vk-auth", follow_redirects=False)
assert resp.status_code == 302
assert "oauth.vk.com/authorize" in resp.headers["location"]
assert "client_id=53265827" in resp.headers["location"]
assert "response_type=token" in resp.headers["location"]
@pytest.mark.asyncio
async def test_vk_auth_no_client_id(client, active_user, monkeypatch):
monkeypatch.setattr("web.routes.connections.settings.VK_CLIENT_ID", "")
await _login(client, active_user)
resp = await client.get("/vk-auth", follow_redirects=False)
assert resp.status_code == 303
assert "error=vk_not_configured" in resp.headers["location"]
# ── GET /vk-callback ──────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_vk_callback_page_returns_html(client, active_user):
await _login(client, active_user)
resp = await client.get("/vk-callback")
assert resp.status_code == 200
assert "access_token" in resp.text
assert "fetch" in resp.text
# ── POST /vk-callback/save ────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_vk_callback_save_valid(client, active_user, override_db):
await _login(client, active_user)
# Seed state into session via /vk-auth call
monkeypatch_state = "test-state-xyz"
# Manually set expected state in session by calling the save endpoint
# with a pre-seeded state — we bypass the session by mocking get_current_user
# Instead: call /vk-auth to seed the session state, then intercept
# Since we can't easily inspect session, test save with wrong state first
resp = await client.post("/vk-callback/save", json={
"access_token": "vk1.a.token",
"state": "wrong-state",
"user_id": "12345",
"expires_in": "86400",
})
assert resp.status_code == 200
data = resp.json()
assert data["ok"] is False
assert "state" in data["message"].lower()
@pytest.mark.asyncio
async def test_vk_callback_save_no_token(client, active_user):
await _login(client, active_user)
resp = await client.post("/vk-callback/save", json={
"access_token": "",
"state": "",
})
assert resp.status_code == 200
assert resp.json()["ok"] is False
@pytest.mark.asyncio
async def test_vk_callback_save_unauthenticated(client):
resp = await client.post("/vk-callback/save", json={
"access_token": "tok", "state": "s",
})
assert resp.status_code == 401
# ── POST /connections/evotor/test ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_evotor_test_no_connection(client, active_user):
await _login(client, active_user)
resp = await client.post("/connections/evotor/test")
assert resp.status_code == 200
assert resp.json()["ok"] is False
assert "не настроено" in resp.json()["message"]
@pytest.mark.asyncio
async def test_evotor_test_success(client, active_user, override_db):
await _login(client, active_user)
conn = EvotorConnection(
user_id=active_user.id, evotor_user_id="evo-t",
access_token="tok", api_token="api",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"items": [{"id": "s1"}, {"id": "s2"}]}
with patch("web.routes.connections.httpx.get", return_value=mock_resp):
resp = await client.post("/connections/evotor/test")
assert resp.status_code == 200
data = resp.json()
assert data["ok"] is True
assert "2" in data["message"]
@pytest.mark.asyncio
async def test_evotor_test_invalid_token(client, active_user, override_db):
await _login(client, active_user)
conn = EvotorConnection(
user_id=active_user.id, evotor_user_id="evo-inv",
access_token="bad-tok", api_token="api",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
mock_resp = MagicMock()
mock_resp.status_code = 401
with patch("web.routes.connections.httpx.get", return_value=mock_resp):
resp = await client.post("/connections/evotor/test")
data = resp.json()
assert data["ok"] is False
assert "401" in data["message"]
# ── POST /connections/vk/test ─────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_vk_test_no_group_id(client, active_user, override_db):
await _login(client, active_user)
conn = VkConnection(
user_id=active_user.id, access_token="vk-tok",
vk_user_id=None,
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
resp = await client.post("/connections/vk/test")
assert resp.json()["ok"] is False
assert "сообщества" in resp.json()["message"].lower()
@pytest.mark.asyncio
async def test_vk_test_success(client, active_user, override_db):
await _login(client, active_user)
conn = VkConnection(
user_id=active_user.id, access_token="vk-tok",
vk_user_id="229744980",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
mock_resp = MagicMock()
mock_resp.json.return_value = {"response": {"groups": [
{"name": "Тестовая чайная", "market": {"enabled": True}}
]}}
with patch("web.routes.connections.httpx.get", return_value=mock_resp):
resp = await client.post("/connections/vk/test")
data = resp.json()
assert data["ok"] is True
assert "Тестовая чайная" in data["message"]
assert "включён" in data["message"]
@pytest.mark.asyncio
async def test_vk_test_api_error(client, active_user, override_db):
await _login(client, active_user)
conn = VkConnection(
user_id=active_user.id, access_token="vk-tok",
vk_user_id="229744980",
connected_at=datetime.utcnow(), updated_at=datetime.utcnow(),
)
override_db.add(conn)
override_db.commit()
mock_resp = MagicMock()
mock_resp.json.return_value = {"error": {"error_code": 5, "error_msg": "User authorization failed"}}
with patch("web.routes.connections.httpx.get", return_value=mock_resp):
resp = await client.post("/connections/vk/test")
data = resp.json()
assert data["ok"] is False
assert "5" in data["message"]