256 lines
7.4 KiB
Python
256 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
import http.client
|
|
import importlib
|
|
import json
|
|
import os
|
|
import socket
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import argparse
|
|
from pathlib import Path
|
|
import traceback
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DB_PATH = ROOT / "var" / "srab.db"
|
|
SERVER_PATH = os.environ.get("SERVER_PATH", ROOT / "build" / "target.exe")
|
|
HOST = "127.0.0.1"
|
|
PORT = 1337
|
|
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
K_FIRST_NAME = "имя"
|
|
K_LAST_NAME = "фамилия"
|
|
K_MIDDLE_NAME = "отчество"
|
|
K_EDUCATION = "образование"
|
|
K_PASSWORD = "пароль"
|
|
K_CONFIRM_PASSWORD = "повтор пароля"
|
|
K_NEW_PASSWORD = "новый пароль"
|
|
K_SNILS = "снилс"
|
|
K_PASSPORT = "паспорт"
|
|
K_CLASS_NUMBER = "номер"
|
|
K_CLASS_LETTER = "буква"
|
|
K_CLASSES = "классы"
|
|
K_IDENTIFIER = "идентификатор"
|
|
K_CREATOR = "создатель"
|
|
K_USERNAME = "имя пользователя"
|
|
|
|
MESSAGE_PASSWORD_MISMATCH = "Пароли не совпадают."
|
|
MESSAGE_LOGIN_OK = "Рады видеть вас снова :3"
|
|
|
|
CASE_PACKAGE = "autotest.cases"
|
|
CASE_MODULES = [
|
|
"case_01_users_create_teacher",
|
|
"case_02_users_create_mismatch",
|
|
"case_03_users_login_success",
|
|
"case_04_users_login_failure",
|
|
"case_05_users_change_password",
|
|
"case_06_users_login_with_new_password",
|
|
"case_07_teachers_auth_required",
|
|
"case_08_teachers_create_class",
|
|
"case_09_teachers_list_classes",
|
|
"case_10_teachers_create_extra_class",
|
|
"case_11_teachers_delete_extra_class",
|
|
"case_12_students_create_student",
|
|
"case_13_students_create_second_student",
|
|
"case_14_teachers_add_student_to_class",
|
|
"case_15_students_list",
|
|
"case_16_students_get",
|
|
"case_17_students_delete",
|
|
"case_18_lessons_teacher_add",
|
|
"case_19_lessons_list",
|
|
"case_20_lessons_teacher_delete",
|
|
"case_21_teachers_remove_student_from_class",
|
|
]
|
|
|
|
|
|
class TestContext:
|
|
def __init__(self) -> None:
|
|
self.teacher_first = "Alice"
|
|
self.teacher_last = "Smith"
|
|
self.teacher_middle = "Ann"
|
|
self.teacher_education = "Math"
|
|
self.teacher_password_initial = "Secret42"
|
|
self.teacher_password_new = "Secret99"
|
|
self.teacher_password = self.teacher_password_initial
|
|
self.teacher_username = f"{self.teacher_first}.{self.teacher_last}"
|
|
|
|
self.class_id = None
|
|
self.class_two_id = None
|
|
self.student_one_id = None
|
|
self.student_two_id = None
|
|
self.teacher_id = None
|
|
self.teacher_user_id = None
|
|
self.lesson_first_id = None
|
|
self.lesson_first_date = None
|
|
self.lesson_first_title = None
|
|
self.lesson_second_id = None
|
|
self.lesson_second_date = None
|
|
self.lesson_second_title = None
|
|
self.student_one_username = None
|
|
self.student_one_password = None
|
|
self.student_two_username = None
|
|
self.student_two_password = None
|
|
|
|
self.K_FIRST_NAME = K_FIRST_NAME
|
|
self.K_LAST_NAME = K_LAST_NAME
|
|
self.K_MIDDLE_NAME = K_MIDDLE_NAME
|
|
self.K_EDUCATION = K_EDUCATION
|
|
self.K_PASSWORD = K_PASSWORD
|
|
self.K_CONFIRM_PASSWORD = K_CONFIRM_PASSWORD
|
|
self.K_NEW_PASSWORD = K_NEW_PASSWORD
|
|
self.K_SNILS = K_SNILS
|
|
self.K_PASSPORT = K_PASSPORT
|
|
self.K_CLASS_NUMBER = K_CLASS_NUMBER
|
|
self.K_CLASS_LETTER = K_CLASS_LETTER
|
|
self.K_CLASSES = K_CLASSES
|
|
self.K_IDENTIFIER = K_IDENTIFIER
|
|
self.K_CREATOR = K_CREATOR
|
|
self.K_USERNAME = K_USERNAME
|
|
self.MESSAGE_PASSWORD_MISMATCH = MESSAGE_PASSWORD_MISMATCH
|
|
self.MESSAGE_LOGIN_OK = MESSAGE_LOGIN_OK
|
|
|
|
def expect(self, condition: bool, message: str) -> None:
|
|
expect(condition, message)
|
|
|
|
def send_request(self, method: str, path: str, *, body=None, headers=None):
|
|
return send_request(method, path, body=body, headers=headers)
|
|
|
|
def query_single_value(self, sql: str, params) -> int:
|
|
return query_single_value(sql, params)
|
|
|
|
def make_auth(self, username: str, password: str) -> dict:
|
|
return make_auth(username, password)
|
|
|
|
|
|
def remove_database() -> None:
|
|
if DB_PATH.exists():
|
|
DB_PATH.unlink()
|
|
|
|
|
|
def start_server(silent) -> subprocess.Popen:
|
|
out = subprocess.DEVNULL if silent else None
|
|
|
|
return subprocess.Popen(
|
|
[str(SERVER_PATH)],
|
|
cwd=str(ROOT),
|
|
stdout=out,
|
|
stderr=out,
|
|
)
|
|
|
|
|
|
def wait_for_server(timeout: float = 10.0) -> None:
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
try:
|
|
with socket.create_connection((HOST, PORT), timeout=0.25):
|
|
return
|
|
except OSError:
|
|
time.sleep(0.05)
|
|
raise RuntimeError("server did not accept connections")
|
|
|
|
|
|
def stop_server(proc: subprocess.Popen) -> None:
|
|
if proc.poll() is not None:
|
|
return
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait(timeout=2)
|
|
|
|
|
|
def expect(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def make_auth(username: str, password: str) -> dict:
|
|
return {"Authorization": f"Basic {username} {password}"}
|
|
|
|
|
|
def send_request(method: str, path: str, *, body=None, headers=None):
|
|
conn = http.client.HTTPConnection(HOST, PORT, timeout=5)
|
|
try:
|
|
data = None
|
|
hdrs = {} if headers is None else dict(headers)
|
|
if body is not None:
|
|
if isinstance(body, (dict, list)):
|
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
hdrs.setdefault(
|
|
"Content-Type",
|
|
"application/json; charset=utf-8",
|
|
)
|
|
elif isinstance(body, str):
|
|
data = body.encode("utf-8")
|
|
else:
|
|
data = body
|
|
conn.request(method, path, body=data, headers=hdrs)
|
|
response = conn.getresponse()
|
|
payload = response.read()
|
|
text = payload.decode("utf-8") if payload else ""
|
|
return response.status, text, dict(response.getheaders())
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def query_single_value(sql: str, params) -> int:
|
|
with sqlite3.connect(DB_PATH) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
row = conn.execute(sql, params).fetchone()
|
|
expect(row is not None, f"query returned no rows: {sql!r} {params!r}")
|
|
return int(row["id"])
|
|
|
|
|
|
def load_cases():
|
|
modules = []
|
|
for name in CASE_MODULES:
|
|
module = importlib.import_module(f"{CASE_PACKAGE}.{name}")
|
|
modules.append(module)
|
|
return modules
|
|
|
|
|
|
def run_tests() -> None:
|
|
context = TestContext()
|
|
|
|
for module in load_cases():
|
|
step_name = getattr(module, "TEST_NAME", module.__name__)
|
|
print(f"[step] {step_name}")
|
|
|
|
try:
|
|
module.run(context)
|
|
except Exception:
|
|
print(f"Test '{step_name}' run failed")
|
|
print(traceback.format_exc())
|
|
sys.exit(1)
|
|
|
|
print("[OK] All steps passed")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="SRAB automatic test suit.")
|
|
parser.add_argument(
|
|
"-s",
|
|
"--suppress",
|
|
default=False,
|
|
action="store_true",
|
|
help="Suppress server output"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
remove_database()
|
|
server = start_server(args.suppress)
|
|
|
|
try:
|
|
wait_for_server()
|
|
run_tests()
|
|
finally:
|
|
stop_server(server)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|