|
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 2m40s
Дефолт не меняет ничего: IDENTITY_STORE="tradein" — это сегодняшний прод, tradein_users/tradein_sessions, соединение с БД auth не открывается вообще. Переключение делается одной переменной окружения ПОСЛЕ того, как на проде появится пароль auth_app и будут скопированы данные. Так сделано намеренно: мерж, который зависит от невыполненного ручного шага, — это мерж, который ломает прод в момент невнимательности. Ядро. app/services/identity_store.py — единственное место, знающее, в какой БД и в каких таблицах живёт реестр. Имена таблиц берутся из фиксированного словаря по значению флага, не конкатенацией с вводом. app/core/auth_db.py — ЛЕНИВЫЙ engine БД auth (core/db.py создаёт свой на импорте; такое же для auth роняло бы старт без DSN). Одно понятие состояния доступа вместо двух. В tradein_users состояние — булев is_active, в auth.users — access_state из трёх значений. Конверсия живёт в одной функции to_access_state(): True→active, False→disabled, а неизвестная строка, NULL или чужой тип → disabled с WARNING. Fail-closed выбран сознательно: если следующая миграция добавит четвёртое состояние, оно по умолчанию НЕ будет пускать. Проверка доступа — свойство can_sign_in, а не сравнение со строкой. Логин в режиме auth. Пароль проверяется ВСЕГДА и ДО ветвления по состоянию — иначе появляется timing-oracle и перечисление логинов. Верный пароль + trial_expired → 403 с машиночитаемым code="access_expired", сессия НЕ создаётся. Верный пароль + disabled → тот же generic 401, что и при неверном пароле. Резолв уже выданной сессии пропускает только active — блокировка обрывает сессию немедленно, а не по истечении sliding-refresh. Старт падает явно, если IDENTITY_STORE=auth, а DSN не задан. Без этого ошибка конфигурации не похожа на аварию: продуктовая БД жива, приложение работает, а rbac_guard ловит исключение резолва вместе с любым другим сбоем и падает в legacy trusted-header ветку — то есть сутками раздаёт права из roles.yaml мимо реестра, включая аккаунты с disabled. Форма входа понимает новый код ответа. Ветвление по detail.code, а не по тексту: текст бэк вправе менять, код — нет. Гранты соблюдены, а не обойдены: auth_app не имеет UPDATE на role/manager_id и не имеет DELETE на users (миграция 004, column-level). Тесты: 2996 passed (+59). Единственный красный — test_search_cache_hit — предсуществующий: проверен контрольным полным прогоном на чистом main (2937 passed, тот же красный). |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| identity_modes.py | ||
| parity.py | ||
| README.md | ||
| test_parity.py | ||
tests/support/ — общие test-инструменты (не сами тесты)
parity.py — legacy → scraper_kit parity harness (issue #2304)
Инструмент для issues #2305-#2310 (миграция неймигрированных importers
app/services/scrapers/* → scraper_kit эквиваленты, см. audit
Scraper_Kit_Legacy_Dependency_Audit_0703 в vault). Каждая такая миграция
должна была доказать, что kit-путь даёт ТОТ ЖЕ результат, что и legacy-путь на
одном и том же входе — для этого использовался assert_parity.
Легаси app/services/scrapers/ каталог полностью удалён (#2397 финальный шаг
E, миграция завершена) — все golden-parity/legacy-vs-kit тесты, построенные на
assert_parity, удалены вместе с ним (kit — единственный живой путь). Harness
(parity.py) остаётся в дереве на случай будущих strangler-миграций
(например legacy-кода за пределами app/services/scrapers/).
Быстрый старт (иллюстративный пример; legacy_fn — гипотетическая функция
из будущего strangler-миграции модуля, не из уже удалённого app/services/scrapers/)
from some_legacy_module import evaluate_via_cian as legacy_fn
from scraper_kit.providers.cian.valuation import evaluate_via_cian as kit_fn
from tests.support.parity import assert_parity
def test_cian_valuation_parity() -> None:
assert_parity(
legacy_fn=legacy_fn,
kit_fn=kit_fn,
fixtures=[
(fixture_html_1, "https://cian.ru/flat/1"),
(fixture_html_2, "https://cian.ru/flat/2"),
],
ignore_fields={"latency_ms", "fetched_at"}, # недетерминированные поля
tolerance=1e-6, # допуск для float-полей (напр. рассчитанные оценки)
)
Как формировать fixtures
Каждый элемент списка — один тестовый вход:
tuple/list→ распаковывается как позиционные аргументы:legacy_fn(*fixture);- любое другое значение (str, dict, ...) → передаётся как единственный
позиционный аргумент:
legacy_fn(fixture).
Начните с 1-2 hardcoded HTML-фикстур/dict'ов (git-история
tests/scrapers/test_avito_detail_kit_parity.py, удалён вместе с legacy
app/services/scrapers/ в #2397, содержит референсный пример).
DB-фикстуры НЕ нужны для чистых parse/compute-функций — используйте их
только если сама legacy/kit-функция реально требует Session.
Почему нельзя просто ==
legacy- и kit-версии одного и того же dataclass (напр. DetailEnrichment,
CianValuationResult) — это РАЗНЫЕ Python-классы (живут в разных модулях),
даже если поля идентичны. Дефолтный dataclass.__eq__ сначала проверяет
other.__class__ is self.__class__ — для двух разных классов это всегда
False, ДАЖЕ когда все значения полей совпадают. assert_parity / compare_outputs
нормализуют оба вывода в dict/list/scalar (через dataclasses.fields() /
.model_dump() рекурсивно) и сравнивают СТРУКТУРНО, по именам полей — эта
проблема класс-идентичности не мешает.
ignore_fields vs tolerance
ignore_fields={"latency_ms", "fetched_at", ...}— поле целиком исключается из сравнения на ЛЮБОМ уровне вложенности. Используйте для полей, у которых даже приблизительное совпадение не гарантировано (timestamps, request-id).tolerance=1e-6— числовой (int/float, НЕbool) допуск черезmath.isclose(rel_tol=tolerance, abs_tol=tolerance). Используйте для float-полей, где legacy/kit могут давать чуть разное значение из-за порядка операций с плавающей точкой (не для timestamps/datetime — там используйтеignore_fields).
При мисматче
assert_parity кидает ParityMismatchError (подкласс AssertionError) со
списком ВСЕХ различающихся полей: путь до поля + значение legacy + значение
kit. Не просто "not equal" — сразу видно, что чинить.
Не входит в scope harness'а
- Он НЕ загружает DB-фикстуры сам — если legacy/kit функция требует
Session, передавайте mock/session в fixture-tuple как обычно. Live network/DB в parity-тестах избегайте — они должны быть detereministic offline unit-тестами. - Он НЕ мигрирует сами importers — это делает каждый sub-issue #2305-#2310 отдельно (тесты для конкретной пары legacy/kit функций пишет тот sub-issue).