feat(ldd): реестр утверждений, evidence gate и runtime-проверки
This commit is contained in:
195
scripts/ldd-migration-upgrade.sh
Executable file
195
scripts/ldd-migration-upgrade.sh
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
# LDD migration evidence: master-схема + данные -> миграции ветки.
|
||||
# Одноразовая БД, рабочие данные не затрагиваются.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DB="lms_ldd_mig_$(date -u +%H%M%S)_$$"
|
||||
EVIDENCE="build/evidence/migration.json"
|
||||
POSTGRES_WAS_RUNNING=0
|
||||
FAILED=0
|
||||
|
||||
log() { echo ">> $*"; }
|
||||
|
||||
cleanup() {
|
||||
local code=$?
|
||||
docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "DROP DATABASE IF EXISTS $DB;" >/dev/null 2>&1 || true
|
||||
if ((POSTGRES_WAS_RUNNING == 0)); then
|
||||
docker compose stop postgres >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit $code
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
command -v docker >/dev/null 2>&1 || { echo "Нужен docker" >&2; exit 1; }
|
||||
git rev-parse --verify master >/dev/null 2>&1 || { echo "Нужна ветка master в git" >&2; exit 1; }
|
||||
|
||||
if docker compose ps --status running --services 2>/dev/null | grep -qx postgres; then
|
||||
POSTGRES_WAS_RUNNING=1
|
||||
fi
|
||||
docker compose up -d postgres >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null
|
||||
docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "CREATE DATABASE $DB;" >/dev/null
|
||||
|
||||
mkdir -p build/evidence
|
||||
|
||||
apply() {
|
||||
docker exec -i lms-postgres psql -U lms -d "$DB" -v ON_ERROR_STOP=1 >/tmp/lms-ldd-migration.log
|
||||
}
|
||||
|
||||
log "Разворачиваю master-схему и синтетические данные..."
|
||||
{
|
||||
git show master:services/identity-service/src/main/resources/db/migration/V1__init.sql
|
||||
echo 'SET search_path TO identity;'
|
||||
git show master:services/identity-service/src/main/resources/db/migration/V2__accounts.sql
|
||||
git show master:services/identity-service/src/main/resources/db/migration/V3__login_attempts.sql
|
||||
cat <<'SQL'
|
||||
INSERT INTO accounts (id, login, password_hash, role, must_change_password) VALUES
|
||||
('00000000-0000-7000-8000-000000001001','admin','x','ADMIN',false),
|
||||
('00000000-0000-7000-8000-000000001002','s1','x','STUDENT',false);
|
||||
INSERT INTO students (account_id, student_no, last_name, first_name) VALUES
|
||||
('00000000-0000-7000-8000-000000001002','S1','Иванов','Иван');
|
||||
INSERT INTO groups (id, name) VALUES ('00000000-0000-7000-8000-000000002001','ПИ-11БО');
|
||||
INSERT INTO enrollments (group_id, student_id) VALUES
|
||||
('00000000-0000-7000-8000-000000002001','00000000-0000-7000-8000-000000001002');
|
||||
SQL
|
||||
echo 'SET search_path TO academic;'
|
||||
git show master:services/academic-service/src/main/resources/db/migration/V1__init.sql
|
||||
git show master:services/academic-service/src/main/resources/db/migration/V2__schema.sql
|
||||
git show master:services/academic-service/src/main/resources/db/migration/V3__seed.sql
|
||||
git show master:services/academic-service/src/main/resources/db/migration/V4__content.sql
|
||||
git show master:services/academic-service/src/main/resources/db/migration/V5__content_published.sql
|
||||
cat <<'SQL'
|
||||
INSERT INTO labs (id, number, title, semester_id) VALUES
|
||||
('00000000-0000-7000-8000-000000003001',1,'ЛР 1','00000000-0000-7000-8000-000000000001');
|
||||
INSERT INTO chapters (id, title, slug) VALUES
|
||||
('00000000-0000-7000-8000-000000003002','Глава','osnovy-c');
|
||||
SQL
|
||||
echo 'SET search_path TO progress;'
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V1__init.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V2__schema.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V3__grades.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V4__submissions.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V5__defense_sessions.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V6__grade_comments.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V7__grade_publications.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V8__grade_publication_students.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V9__grade_general_comment.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V11__attestations.sql
|
||||
git show master:services/progress-service/src/main/resources/db/migration/V12__attestation_semester_and_publication.sql
|
||||
cat <<'SQL'
|
||||
INSERT INTO lab_grades (student_id, lab_id, score) VALUES
|
||||
('00000000-0000-7000-8000-000000001002','00000000-0000-7000-8000-000000003001',4.5);
|
||||
INSERT INTO grade_comments (student_id, lab_id, text) VALUES
|
||||
('00000000-0000-7000-8000-000000001002','00000000-0000-7000-8000-000000003001','ok');
|
||||
SQL
|
||||
} | apply
|
||||
|
||||
log "Применяю миграции ветки..."
|
||||
{
|
||||
echo 'SET search_path TO identity;'
|
||||
cat services/identity-service/src/main/resources/db/migration/V4__roles.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V5__groups_cohorts.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V6__student_statuses.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V7__promotion.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V8__membership_history.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V9__account_statuses.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V10__audit_events.sql
|
||||
echo 'SET search_path TO academic;'
|
||||
cat services/academic-service/src/main/resources/db/migration/V6__courses.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V7__labs_course.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V8__chapters_course.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V9__article_assets_indexes.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V10__course_title_unique.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V11__global_audience_all.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V12__asset_links_backfill.sql
|
||||
echo 'SET search_path TO progress;'
|
||||
cat services/progress-service/src/main/resources/db/migration/V13__profile_authors.sql
|
||||
} | apply
|
||||
|
||||
log "Проверяю postflight..."
|
||||
CHECK_SQL=$(cat <<'SQL'
|
||||
SET search_path TO identity;
|
||||
SELECT 'audit_events=' || count(*) FROM information_schema.tables
|
||||
WHERE table_schema='identity' AND table_name='audit_events';
|
||||
SELECT 'students=' || count(*) FROM students;
|
||||
SELECT 'groups=' || count(*) FROM groups;
|
||||
SELECT 'history=' || count(*) FROM membership_history;
|
||||
SET search_path TO academic;
|
||||
SELECT 'labs_course_id=' || count(*) FROM information_schema.columns
|
||||
WHERE table_schema='academic' AND table_name='labs' AND column_name='course_id';
|
||||
SELECT 'chapters_course_id=' || count(*) FROM information_schema.columns
|
||||
WHERE table_schema='academic' AND table_name='chapters' AND column_name='course_id';
|
||||
SELECT 'article_assets=' || count(*) FROM information_schema.tables
|
||||
WHERE table_schema='academic' AND table_name='article_assets';
|
||||
SELECT 'labs=' || count(*) FROM labs;
|
||||
SELECT 'chapters=' || count(*) FROM chapters;
|
||||
SET search_path TO progress;
|
||||
SELECT 'grade_author=' || count(*) FROM information_schema.columns
|
||||
WHERE table_schema='progress' AND table_name='grade_comments' AND column_name='author_account_id';
|
||||
SELECT 'grade_updated_by=' || count(*) FROM information_schema.columns
|
||||
WHERE table_schema='progress' AND table_name='lab_grades' AND column_name='updated_by_account_id';
|
||||
SELECT 'grades=' || count(*) FROM lab_grades;
|
||||
SELECT 'comments=' || count(*) FROM grade_comments;
|
||||
SQL
|
||||
)
|
||||
RESULT=$(docker exec -i lms-postgres psql -U lms -d "$DB" -At -v ON_ERROR_STOP=1 -c "$CHECK_SQL")
|
||||
|
||||
# V13 повторно: идемпотентность.
|
||||
if { echo 'SET search_path TO progress;'; cat services/progress-service/src/main/resources/db/migration/V13__profile_authors.sql; } | apply; then
|
||||
V13_TWICE="passed"
|
||||
else
|
||||
V13_TWICE="failed"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$RESULT" | sed 's/^/ /'
|
||||
|
||||
python3 - "$EVIDENCE" "$RESULT" "$V13_TWICE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
out, result, v13 = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
required = {
|
||||
"audit_events=1",
|
||||
"students=1",
|
||||
"groups=1",
|
||||
"history=1",
|
||||
"labs_course_id=1",
|
||||
"chapters_course_id=1",
|
||||
"article_assets=1",
|
||||
"labs=1",
|
||||
"chapters=1",
|
||||
"grade_author=1",
|
||||
"grade_updated_by=1",
|
||||
"grades=1",
|
||||
"comments=1",
|
||||
}
|
||||
lines = {line.strip() for line in result.splitlines() if line.strip()}
|
||||
missing = sorted(required - lines)
|
||||
failed = bool(missing) or v13 != "passed"
|
||||
if missing:
|
||||
status = "failed"
|
||||
detail = "postflight не подтвердил: " + ", ".join(missing)
|
||||
elif v13 != "passed":
|
||||
status = "failed"
|
||||
detail = "V13 не применилась повторно"
|
||||
else:
|
||||
status = "passed"
|
||||
detail = "master->branch: все postflight-проверки и повторная V13 успешны"
|
||||
checks = [{"id": "migration.master_upgrade", "status": status, "detail": detail}]
|
||||
with open(out, "w", encoding="utf-8") as fh:
|
||||
now = __import__("datetime").datetime.now(__import__("datetime").UTC).isoformat()
|
||||
json.dump({"generatedAt": now, "checks": checks}, fh, ensure_ascii=False, indent=2)
|
||||
print(("PASS " if not failed else "FAIL ") + "migration.master_upgrade: " + detail)
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
|
||||
log "Migration evidence: $EVIDENCE"
|
||||
312
scripts/ldd-runtime.sh
Executable file
312
scripts/ldd-runtime.sh
Executable file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env bash
|
||||
# LDD runtime smoke: поднимает полный стек на одноразовой БД и проверяет
|
||||
# ключевые сценарии через реальные HTTP-вызовы. Не трогает рабочую БД lms.
|
||||
#
|
||||
# ./scripts/ldd-runtime.sh
|
||||
#
|
||||
# Требования: docker compose v2, Java 21, curl, python3. Скрипт отказывается
|
||||
# работать, если порты 8080/8081/8082/8083/8090 уже заняты.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DB="lms_ldd_$(date -u +%H%M%S)_$$"
|
||||
EVIDENCE="build/evidence/runtime.json"
|
||||
POSTGRES_WAS_RUNNING=0
|
||||
STACK_STARTED=0
|
||||
|
||||
log() { echo ">> $*"; }
|
||||
|
||||
cleanup() {
|
||||
local code=$?
|
||||
if ((STACK_STARTED)); then
|
||||
log "Останавливаю тестовый стек..."
|
||||
./run.sh stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "DROP DATABASE IF EXISTS $DB;" >/dev/null 2>&1 || true
|
||||
if ((POSTGRES_WAS_RUNNING == 0)); then
|
||||
docker compose stop postgres >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit $code
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
for port in 8080 8081 8082 8083 8090; do
|
||||
if ss -ltn 2>/dev/null | grep -q ":$port "; then
|
||||
echo "Порт $port занят. Остановите текущий стек (./run.sh stop) и повторите." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
command -v docker >/dev/null 2>&1 || { echo "Нужен docker" >&2; exit 1; }
|
||||
command -v python3 >/dev/null 2>&1 || { echo "Нужен python3" >&2; exit 1; }
|
||||
JAVA_BIN="${JAVA_BIN:-/usr/lib/jvm/java-21-openjdk/bin/java}"
|
||||
[[ -x "$JAVA_BIN" ]] || { echo "Не найден Java 21: $JAVA_BIN" >&2; exit 1; }
|
||||
|
||||
if docker compose ps --status running --services 2>/dev/null | grep -qx postgres; then
|
||||
POSTGRES_WAS_RUNNING=1
|
||||
fi
|
||||
|
||||
log "Запускаю PostgreSQL..."
|
||||
docker compose up -d postgres >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null
|
||||
|
||||
log "Создаю одноразовую БД $DB..."
|
||||
docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "CREATE DATABASE $DB;" >/dev/null
|
||||
|
||||
log "Собираю bootJar..."
|
||||
./gradlew bootJar --console=plain >/tmp/lms-ldd-bootjar.log
|
||||
|
||||
log "Запускаю стек..."
|
||||
export LMS_DB_URL="jdbc:postgresql://localhost:5432/$DB"
|
||||
export LMS_DB_NAME="$DB"
|
||||
export LMS_DB_USER="lms"
|
||||
export LMS_DB_PASSWORD="lms"
|
||||
export LMS_INTERNAL_TOKEN="ldd-internal-token"
|
||||
export LMS_SESSION_COOKIE_SECURE="false"
|
||||
export JAVA_BIN
|
||||
STACK_STARTED=1
|
||||
./run.sh start
|
||||
|
||||
mkdir -p "$(dirname "$EVIDENCE")"
|
||||
|
||||
log "Проверяю HTTP-сценарии..."
|
||||
python3 - "$EVIDENCE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
out_path = sys.argv[1]
|
||||
gateway = "http://localhost:8080"
|
||||
web = "http://localhost:8090"
|
||||
checks = []
|
||||
failures = []
|
||||
|
||||
|
||||
def record(check_id, status, detail):
|
||||
checks.append({"id": check_id, "status": status, "detail": detail})
|
||||
print(("PASS " if status == "passed" else "FAIL ") + check_id + ": " + detail)
|
||||
if status != "passed":
|
||||
failures.append(check_id)
|
||||
|
||||
|
||||
def req(method, url, data=None, token=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = "Bearer " + token
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
raw = response.read().decode()
|
||||
return response.status, json.loads(raw) if raw.strip().startswith(("{", "[")) else raw
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read().decode()
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except Exception:
|
||||
parsed = raw
|
||||
return exc.code, parsed
|
||||
|
||||
|
||||
def expect(check_id, method, url, expected, data=None, token=None, detail=""):
|
||||
status, body = req(method, url, data=data, token=token)
|
||||
if status != expected:
|
||||
record(check_id, "failed", f"{detail or url}: ожидался {expected}, получен {status}: {body}")
|
||||
raise AssertionError(check_id)
|
||||
record(check_id, "passed", detail or f"{method} {url} -> {status}")
|
||||
return body
|
||||
|
||||
|
||||
try:
|
||||
# 1. Health всех сервисов.
|
||||
for name, port in [("identity", 8081), ("academic", 8082), ("progress", 8083), ("gateway", 8080), ("web", 8090)]:
|
||||
status, body = req("GET", f"http://localhost:{port}/actuator/health")
|
||||
if status != 200 or not isinstance(body, dict) or body.get("status") != "UP":
|
||||
record("runtime.stack_health", "failed", f"{name}: {status} {body}")
|
||||
raise AssertionError("stack_health")
|
||||
record("runtime.stack_health", "passed", "identity/academic/progress/gateway/web health=UP")
|
||||
|
||||
# 2. Admin setup + login через gateway.
|
||||
health = expect("runtime.admin_setup_login", "GET", gateway + "/api/v1/auth/health", 200)
|
||||
if health.get("adminExists") is not False:
|
||||
record("runtime.admin_setup_login", "failed", f"adminExists до setup: {health}")
|
||||
raise AssertionError("adminExists")
|
||||
admin = expect(
|
||||
"runtime.admin_setup_login",
|
||||
"POST",
|
||||
gateway + "/api/v1/auth/setup",
|
||||
200,
|
||||
data={"login": "ldd-admin", "password": "ldd-password-123", "password2": "ldd-password-123"},
|
||||
detail="setup admin",
|
||||
)
|
||||
admin_token = admin["token"]
|
||||
me = expect("runtime.admin_setup_login", "GET", gateway + "/api/v1/auth/me", 200, token=admin_token)
|
||||
if me.get("role") != "ADMIN" or not me.get("active", False):
|
||||
record("runtime.admin_setup_login", "failed", f"/auth/me: {me}")
|
||||
raise AssertionError("me")
|
||||
|
||||
# 3. Реальный доменный поток: группа -> курс -> преподаватель -> группа курса -> лаба.
|
||||
suffix = uuid.uuid4().hex[:6]
|
||||
group = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/groups",
|
||||
200,
|
||||
data={"stream": "LDD" + suffix, "course": 1, "number": 1, "degree": "БО", "enrollmentYear": 2026},
|
||||
token=admin_token,
|
||||
detail="create group",
|
||||
)
|
||||
course = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/courses",
|
||||
200,
|
||||
data={"title": "LDD runtime course " + suffix},
|
||||
token=admin_token,
|
||||
detail="create course",
|
||||
)
|
||||
teacher = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/teachers",
|
||||
200,
|
||||
data={"login": "ldd-teacher-" + suffix, "password": "ldd-teacher-123"},
|
||||
token=admin_token,
|
||||
detail="create teacher",
|
||||
)
|
||||
expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/courses/" + course["id"] + "/teachers",
|
||||
200,
|
||||
data={"accountId": teacher["id"]},
|
||||
token=admin_token,
|
||||
detail="assign teacher",
|
||||
)
|
||||
expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/courses/" + course["id"] + "/groups",
|
||||
200,
|
||||
data={"groupId": group["id"]},
|
||||
token=admin_token,
|
||||
detail="attach group",
|
||||
)
|
||||
lab = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/labs",
|
||||
200,
|
||||
data={
|
||||
"number": 1,
|
||||
"title": "LDD lab",
|
||||
"status": "published",
|
||||
"courseId": course["id"],
|
||||
"levels": [{"code": "basic", "title": "Базовый", "position": 1}],
|
||||
},
|
||||
token=admin_token,
|
||||
detail="create lab",
|
||||
)
|
||||
teacher_token = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/auth/login",
|
||||
200,
|
||||
data={"login": "ldd-teacher-" + suffix, "password": "ldd-teacher-123"},
|
||||
detail="teacher login",
|
||||
)["token"]
|
||||
mine = expect("runtime.course_flow", "GET", gateway + "/api/v1/courses/mine", 200, token=teacher_token)
|
||||
if not any(item.get("id") == course["id"] for item in mine):
|
||||
record("runtime.course_flow", "failed", "курс не найден в /courses/mine преподавателя")
|
||||
raise AssertionError("mine")
|
||||
labs = expect(
|
||||
"runtime.course_flow",
|
||||
"GET",
|
||||
gateway + "/api/v1/labs?courseId=" + course["id"],
|
||||
200,
|
||||
token=teacher_token,
|
||||
)
|
||||
if not any(item.get("id") == lab["id"] for item in labs):
|
||||
record("runtime.course_flow", "failed", "лаба не найдена у преподавателя")
|
||||
raise AssertionError("lab list")
|
||||
roster = expect(
|
||||
"runtime.course_flow",
|
||||
"GET",
|
||||
gateway + "/api/v1/courses/" + course["id"] + "/groups/" + group["id"] + "/students",
|
||||
200,
|
||||
token=teacher_token,
|
||||
)
|
||||
if roster.get("groupId") != group["id"]:
|
||||
record("runtime.course_flow", "failed", f"roster: {roster}")
|
||||
raise AssertionError("roster")
|
||||
record("runtime.course_flow", "passed", "группа/курс/преподаватель/лаба/roster доступны преподавателю")
|
||||
|
||||
# 4. Чужой преподаватель не получает доступ к курсу/лабе/аудиту.
|
||||
outsider = expect(
|
||||
"runtime.foreign_teacher_denied",
|
||||
"POST",
|
||||
gateway + "/api/v1/teachers",
|
||||
200,
|
||||
data={"login": "ldd-outsider-" + suffix, "password": "ldd-outsider-123"},
|
||||
token=admin_token,
|
||||
detail="create outsider",
|
||||
)
|
||||
outsider_token = expect(
|
||||
"runtime.foreign_teacher_denied",
|
||||
"POST",
|
||||
gateway + "/api/v1/auth/login",
|
||||
200,
|
||||
data={"login": "ldd-outsider-" + suffix, "password": "ldd-outsider-123"},
|
||||
detail="outsider login",
|
||||
)["token"]
|
||||
expect("runtime.foreign_teacher_denied", "GET", gateway + "/api/v1/courses/" + course["id"], 403, token=outsider_token, detail="foreign course 403")
|
||||
expect("runtime.foreign_teacher_denied", "GET", gateway + "/api/v1/labs/" + lab["id"], 403, token=outsider_token, detail="foreign lab 403")
|
||||
expect("runtime.foreign_teacher_denied", "GET", gateway + "/api/v1/audit", 403, token=outsider_token, detail="audit 403")
|
||||
record("runtime.foreign_teacher_denied", "passed", "чужой преподаватель получает 403 на курс/лабу/аудит")
|
||||
|
||||
# 5. Аудит доступен админу и содержит ключевые события.
|
||||
events = expect("runtime.audit_visible", "GET", gateway + "/api/v1/audit?limit=200", 200, token=admin_token)
|
||||
actions = {event.get("action") for event in events}
|
||||
required = {"COURSE_CREATE", "TEACHER_CREATE", "COURSE_TEACHER_ASSIGN", "COURSE_GROUP_ATTACH", "LAB_CREATE"}
|
||||
absent = required - actions
|
||||
if absent:
|
||||
record("runtime.audit_visible", "failed", f"в аудите нет: {sorted(absent)}; есть: {sorted(actions)}")
|
||||
raise AssertionError("audit actions")
|
||||
record("runtime.audit_visible", "passed", f"аудит содержит {sorted(required)}")
|
||||
|
||||
# 6. Web-страница входа и публичное дерево.
|
||||
status, _ = req("GET", web + "/login")
|
||||
if status != 200:
|
||||
record("runtime.web_login_page", "failed", f"/login -> {status}")
|
||||
raise AssertionError("web login")
|
||||
record("runtime.web_login_page", "passed", "web /login -> 200")
|
||||
expect("runtime.web_login_page", "GET", gateway + "/api/v1/content/tree", 200, detail="public content tree")
|
||||
expect("runtime.web_login_page", "GET", gateway + "/api/v1/courses/" + course["id"] + "/content/tree", 401, detail="anonymous course content 401")
|
||||
|
||||
except AssertionError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
record("runtime.unexpected", "failed", f"{type(exc).__name__}: {exc}")
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
now = __import__("datetime").datetime.now(__import__("datetime").UTC).isoformat()
|
||||
json.dump({"generatedAt": now, "checks": checks}, fh, ensure_ascii=False, indent=2)
|
||||
|
||||
if failures:
|
||||
print("Неуспешные runtime-проверки: " + ", ".join(failures), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("Все runtime-проверки пройдены.")
|
||||
PY
|
||||
|
||||
if [[ -f "$EVIDENCE" ]]; then
|
||||
log "Runtime evidence: $EVIDENCE"
|
||||
fi
|
||||
Reference in New Issue
Block a user