Files
miem_workers/tests/browser_users.py
T
codex 5128fac6e5
CI / test (pull_request) Successful in 10m22s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped
feat: add first-login usage guide
2026-10-12 11:05:04 +03:00

205 lines
13 KiB
Python

"""Offline users/roles/password/audit browser scenario; synthetic SQLite data only."""
import os
from pathlib import Path
import sys
import socket
import threading
import time
import uvicorn
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from playwright.sync_api import expect, sync_playwright
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.config import Settings, get_settings
from app.db import Base, get_db
from app.main import app
from app.models import Employee, EmployeePublication, utcnow
from app.security import ensure_bootstrap_admin
from app.services import council
def main():
engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine)
settings = Settings(_env_file=None, admin_username='admin', admin_password='Browser initial admin', session_secret='browser-users-secret')
with factory() as db:
admin = ensure_bootstrap_admin(db, settings)
admin.first_login_at = utcnow()
employee = Employee(profile_key='browser-users', full_name='Сотрудник для проверки', canonical_url='https://www.hse.ru/staff/test',
current_data={'sections': [{'title': 'Учёные степени', 'items': ['Доктор технических наук']}]})
db.add(employee)
db.flush()
db.add(EmployeePublication(employee_id=employee.id, title='Статья для проверки', year=2024, publication_type='ARTICLE',
source_hash='browser-users', raw_data={'journal': 'Журнал для проверки'}))
db.commit()
def database():
with factory() as db:
yield db
original = council._articles_by_author
council._articles_by_author = lambda: {}
app.dependency_overrides[get_db] = database
app.dependency_overrides[get_settings] = lambda: settings
output = Path('.impeccable/review/users')
output.mkdir(parents=True, exist_ok=True)
listener = socket.socket()
listener.bind(('127.0.0.1', 0))
server = uvicorn.Server(uvicorn.Config(app, lifespan='off', access_log=False, log_level='critical'))
thread = threading.Thread(target=server.run, kwargs={'sockets': [listener]}, daemon=True)
thread.start()
for _ in range(1000):
if server.started:
break
time.sleep(.01)
assert server.started
base_url = f'http://127.0.0.1:{listener.getsockname()[1]}'
try:
with sync_playwright() as playwright:
browser = playwright.chromium.launch(executable_path=os.getenv('BROWSER_EXECUTABLE'))
context = browser.new_context(viewport={'width':1440,'height':1000})
page = context.new_page()
errors = []
page.on('pageerror', lambda error: errors.append(str(error)))
page.goto(base_url + '/admin/login')
page.get_by_label('Логин', exact=True).fill('admin')
page.get_by_label('Пароль', exact=True).fill('Browser initial admin')
page.get_by_role('button', name='Войти', exact=True).click()
expect(page.get_by_role('link', name='Пользователи', exact=True)).to_be_visible()
page.get_by_role('link', name='Пользователи', exact=True).click()
creation = page.locator('form[action="/admin/users"]')
creation.get_by_label('Логин', exact=True).fill('reader')
creation.get_by_label('Имя', exact=True).fill('Тестовый читатель')
creation.get_by_role('button', name='Создать пользователя', exact=True).click()
temporary = page.locator('[data-temporary-password]')
expect(temporary).to_be_visible()
password = temporary.input_value()
page.screenshot(path=str(output/'users-desktop.png'), mask=[temporary], full_page=True)
page.set_viewport_size({'width':320,'height':844})
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
page.screenshot(path=str(output/'users-mobile.png'), mask=[temporary], full_page=True)
creation.get_by_label('Логин', exact=True).fill('nojs.reader')
creation.get_by_role('button', name='Создать пользователя', exact=True).click()
nojs_password = page.locator('[data-temporary-password]').input_value()
page.get_by_role('button', name='Выйти', exact=True).click()
page.goto(base_url + '/admin/login')
page.get_by_label('Логин', exact=True).fill('reader')
page.get_by_label('Пароль', exact=True).fill(password)
page.get_by_role('button', name='Войти', exact=True).click()
modal = page.get_by_role('dialog', name='Смените временный пароль')
expect(modal).to_be_visible()
page.keyboard.press('Escape')
expect(modal).to_be_visible()
expect(modal.get_by_label('Текущий пароль', exact=True)).to_be_focused()
assert modal.evaluate('node => node.scrollWidth <= node.clientWidth')
page.screenshot(path=str(output/'required-mobile.png'))
page.set_viewport_size({'width':1440,'height':1000})
page.screenshot(path=str(output/'required-desktop.png'))
modal.get_by_label('Текущий пароль', exact=True).fill(password)
modal.get_by_label('Новый пароль', exact=True).fill('Reader personal passphrase 123')
modal.get_by_label('Повторите новый пароль', exact=True).fill('Reader personal passphrase 123')
modal.get_by_role('button', name='Сохранить новый пароль', exact=True).click()
guide = page.get_by_role('dialog', name='Как пользоваться сайтом')
expect(guide).to_be_visible()
expect(guide.get_by_role('heading', name='Выберите задачу', exact=True)).to_be_focused()
for width in (1440, 320):
page.set_viewport_size({'width': width, 'height': 900})
assert guide.evaluate('node => node.scrollWidth <= node.clientWidth')
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
page.screenshot(path=str(output / f'guide-first-{width}.png'))
page.keyboard.press('Tab')
assert page.evaluate('document.querySelector("[data-guide-dialog]").contains(document.activeElement)')
page.route('**/admin/guide/dismiss', lambda route: route.fulfill(status=503, body='Temporary failure'))
guide.get_by_role('button', name='Закрыть подсказки', exact=True).click()
expect(guide.locator('[data-guide-error]')).to_be_visible()
guide.get_by_role('button', name='Продолжить без сохранения', exact=True).click()
expect(guide).not_to_be_visible()
expect(page.get_by_role('link', name='Как пользоваться', exact=True)).to_be_focused()
page.unroute('**/admin/guide/dismiss')
page.reload()
expect(guide).to_be_visible()
page.keyboard.press('Escape')
expect(guide).not_to_be_visible()
expect(page.get_by_role('link', name='Как пользоваться', exact=True)).to_be_focused()
page.reload()
expect(guide).not_to_be_visible()
page.get_by_role('link', name='Как пользоваться', exact=True).click()
expect(guide).to_be_visible()
guide.get_by_role('button', name='Далее', exact=True).click()
expect(guide.get_by_role('heading', name='Найдите сотрудника и откройте профиль')).to_be_visible()
guide.get_by_role('button', name='Назад', exact=True).click()
expect(guide.get_by_role('heading', name='Выберите задачу', exact=True)).to_be_visible()
for number in range(1, 7):
expect(guide.locator('[data-guide-progress]')).to_have_text(f'Шаг {number} из 6')
expect(guide.locator('[data-guide-step]:visible')).to_have_count(1)
if number == 4:
expect(guide).to_contain_text('ВСЕГО (предв.)')
page.screenshot(path=str(output / 'guide-counts-mobile.png'))
if number < 6:
guide.get_by_role('button', name='Далее', exact=True).click()
guide.get_by_role('button', name='Завершить подсказки', exact=True).click()
expect(guide).not_to_be_visible()
no_js = browser.new_context(java_script_enabled=False)
nojs_page = no_js.new_page()
nojs_page.goto(base_url + '/admin/login')
nojs_page.get_by_label('Логин', exact=True).fill('nojs.reader')
nojs_page.get_by_label('Пароль', exact=True).fill(nojs_password)
nojs_page.get_by_role('button', name='Войти', exact=True).click()
nojs_page.get_by_label('Текущий пароль', exact=True).fill(nojs_password)
nojs_page.get_by_label('Новый пароль', exact=True).fill('No JS personal passphrase 123')
nojs_page.get_by_label('Повторите новый пароль', exact=True).fill('No JS personal passphrase 123')
nojs_page.get_by_role('button', name='Сохранить новый пароль', exact=True).click()
nojs_page.get_by_role('link', name='Открыть подсказки', exact=True).click()
expect(nojs_page.locator('[data-guide-step]')).to_have_count(6)
expect(nojs_page.get_by_role('heading', name='Проверьте строку публикации')).to_be_visible()
nojs_page.get_by_role('button', name='Завершить подсказки', exact=True).click()
expect(nojs_page.get_by_role('link', name='Открыть подсказки', exact=True)).to_have_count(0)
no_js.close()
expect(page.get_by_text('Пароль изменён. Другие сессии завершены.', exact=True)).to_be_visible()
expect(page.get_by_role('link', name='Пользователи', exact=True)).to_have_count(0)
page.get_by_role('link', name='Обзор', exact=True).click()
expect(page.get_by_role('button', name='Запустить парсинг', exact=True)).to_have_count(0)
page.get_by_role('link', name='Диссертационный совет', exact=True).click()
page.locator('.council__candidate-summary').click()
article = page.locator('[data-council-article]')
article.focus()
page.keyboard.press('Enter')
page.keyboard.press('Enter')
editor = page.get_by_role('dialog', name='Редактирование статьи')
editor.get_by_label('Название статьи', exact=True).fill('Статья изменена читателем')
editor.get_by_role('button', name='Сохранить', exact=True).click()
expect(article.locator('strong')).to_have_text('Статья изменена читателем')
page.get_by_role('link', name='Личный кабинет', exact=True).click()
expect(page.get_by_text('Редактирование статьи', exact=True)).to_be_visible()
expect(page.get_by_text('Статья изменена читателем', exact=False)).to_be_visible()
page.screenshot(path=str(output/'account-desktop.png'), full_page=True)
page.set_viewport_size({'width':320,'height':844})
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
page.screenshot(path=str(output/'account-mobile.png'), full_page=True)
page.get_by_role('button', name='Выйти', exact=True).click()
page.goto(base_url + '/admin/login')
page.get_by_label('Логин', exact=True).fill('admin')
page.get_by_label('Пароль', exact=True).fill('Browser initial admin')
page.get_by_role('button', name='Войти', exact=True).click()
page.get_by_role('link', name='Пользователи', exact=True).click()
page.once('dialog', lambda dialog: dialog.accept())
page.get_by_role('button', name='Сбросить пароль reader', exact=True).click()
expect(page.locator('[data-temporary-password]')).to_be_visible()
page.locator('[data-temporary-password]').evaluate("input => input.value = '[скрыто]'")
expect(page.locator('.table__cell').filter(has_text='Требуется смена пароля')).to_be_visible()
assert not errors, errors
browser.close()
finally:
server.should_exit = True
thread.join(timeout=5)
listener.close()
council._articles_by_author = original
app.dependency_overrides.clear()
engine.dispose()
print(f'Users browser scenario passed; screenshots: {output}')
if __name__ == '__main__':
main()