Files
miem_workers/tests/browser_users.py
T
codex 508aae4f8a
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Canceled after 0s
CI / test (pull_request) Canceled after 10m43s
feat: add council user role and restrict crawl runs
2026-11-12 09:35:26 +00:00

300 lines
20 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.
"""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('combobox', name='Роль', exact=True).select_option('council')
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('combobox', name='Роль', exact=True).select_option('user')
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.locator('[data-guide]')
expect(guide).to_be_visible()
expect(guide.get_by_role('heading', name='Познакомьтесь с Workers')).to_be_visible()
restricted = context.new_page()
restricted.add_init_script("""Object.defineProperty(window, 'sessionStorage', {
get() { throw new DOMException('Storage is disabled', 'SecurityError'); },
});""")
restricted.goto(base_url + '/admin/account')
expect(restricted.get_by_role('link', name='Открыть подсказки', exact=True)).to_be_visible()
restricted.get_by_role('link', name='Как пользоваться', exact=True).click()
expect(restricted.get_by_role('heading', name='Как пользоваться сайтом')).to_be_visible()
restricted.close()
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.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()
guide.get_by_role('heading').focus()
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()
go_next = guide.get_by_role('button', name='Далее', exact=True)
go_back = guide.get_by_role('button', name='Назад', exact=True)
tips = page.locator('.guide__tip:visible')
go_next.click()
expect(guide.get_by_role('heading', name='Найдите сотрудника', exact=True)).to_be_focused()
assert page.url.endswith('/admin/directory')
expect(tips).to_have_count(3)
search = page.get_by_role('textbox', name='ФИО или ссылка', exact=True)
expect(search).to_have_accessible_description('Введите фамилию, часть ФИО или ссылку на профиль ВШЭ.')
search.fill('Сотрудник')
page.get_by_role('button', name='Применить фильтры', exact=True).click()
expect(tips).to_have_count(3)
expect(search).to_have_value('Сотрудник')
page.get_by_role('link', name='Сбросить', exact=True).click()
expect(search).to_have_value('')
expect(tips).to_have_count(3)
go_back.click()
expect(guide.get_by_role('heading', name='Познакомьтесь с Workers')).to_be_visible()
for _ in range(3):
go_next.click()
expect(guide.get_by_role('heading', name='Проверьте повышение квалификации')).to_be_visible()
expect(tips).to_have_count(2)
expect(page.get_by_role('combobox', name='Курсы в профиле', exact=True)).to_have_accessible_description(
'Выберите «Есть курсы» или «Нет курсов» по записям в профиле ВШЭ.'
)
expect(page.get_by_role('textbox', name='С какого года включительно', exact=True)).to_have_accessible_description(
'Если указан только год, будут показаны сотрудники с курсами. '
'Например, 2024 включает 2024 год и все последующие.'
)
for width in (1440, 320):
page.set_viewport_size({'width': width, 'height': 900})
assert tips.evaluate_all('tips => tips.every(tip => tip.scrollWidth <= tip.clientWidth)')
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
page.set_viewport_size({'width': 1440, 'height': 900})
for _ in range(3):
go_next.click()
assert page.url.endswith('/admin/council')
expect(tips).to_have_count(3)
for width in (1440, 320):
page.set_viewport_size({'width': width, 'height': 900})
assert page.evaluate('''() => {
const tips = [...document.querySelectorAll('.guide__tip')];
const boxes = tips.map(tip => tip.getBoundingClientRect());
const fields = [...document.querySelectorAll('.council__search input:not([type=hidden]), .council__search select')];
const overlaps = (a, b) => a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
return tips.every(tip => tip.scrollWidth <= tip.clientWidth && tip.scrollHeight <= tip.clientHeight)
&& boxes.every((box, i) => boxes.slice(i + 1).every(other => !overlaps(box, other))
&& fields.every(field => !overlaps(box, field.getBoundingClientRect())))
&& document.documentElement.scrollWidth <= innerWidth;
}''')
guide.evaluate("node => node.scrollIntoView({block: 'start'})")
page.screenshot(path=str(output / f'guide-council-{width}.png'))
go_back.click()
expect(guide.get_by_role('heading', name='Откройте карточку сотрудника')).to_be_visible()
profile = page.get_by_role('link', name='Сотрудник для проверки', exact=True)
expect(profile).to_have_accessible_description('Нажмите на ФИО, чтобы открыть контакты, степени и публикации сотрудника.')
profile.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='Откройте карточку сотрудника')).to_be_visible()
go_next.click()
go_next.click()
go_next.click()
expect(tips).to_have_count(3)
expect(page.locator('[data-guide-count="total"] .guide__tip')).to_contain_text('Уникальные работы')
page.screenshot(path=str(output / 'guide-counts-mobile.png'))
go_next.click()
expect(tips).to_have_count(2)
expect(page.locator('[data-guide-count="hse"] .guide__tip')).to_contain_text('личного кабинета')
go_next.click()
expect(tips).to_have_count(2)
go_next.click()
assert page.url.endswith('/admin/council/publications')
go_next.click()
expect(tips).to_have_count(3)
warning = page.locator('[data-warning] button').first
warning.focus()
expect(warning).to_have_attribute('aria-expanded', 'true')
page.keyboard.press('Escape')
expect(warning).to_have_attribute('aria-expanded', 'false')
expect(guide).to_be_visible()
page.locator('[data-council-article]').click()
page.get_by_role('button', name='Редактировать статью: Статья для проверки', exact=True).click()
guide_editor = page.get_by_role('dialog', name='Редактирование статьи')
expect(guide_editor.locator('.guide__tip')).to_have_count(3)
expect(guide_editor.get_by_role('combobox', name='Квартиль', exact=True)).to_have_accessible_description(
'Правка затронет статьи этого журнала за тот же год. Если год неизвестен, поле недоступно.'
)
page.keyboard.press('Escape')
expect(guide_editor).not_to_be_visible()
expect(guide).to_be_visible()
guide.get_by_role('button', name='Завершить подсказки', exact=True).click()
expect(guide).not_to_be_visible()
expect(tips).to_have_count(0)
page.go_back()
expect(guide).not_to_be_visible()
expect(tips).to_have_count(0)
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(2)
expect(nojs_page.get_by_text('Повышение квалификации', exact=True)).to_be_visible()
expect(nojs_page.get_by_role('link', name='Диссертационный совет', exact=True)).to_have_count(0)
expect(nojs_page.get_by_role('link', name='Запуски', exact=True)).to_have_count(0)
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_role('link', name='Пользователи', exact=True)).to_have_count(0)
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()