205 lines
13 KiB
Python
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()
|