Lewati ke isi

Laporan Remediasi Audit CBT — Batch 6: Alokasi Sesi & Form, Proteksi Beban, Metrik Sistem, Siklus Token, Aksesibilitas Frontend & Penyelarasan Skema Alembic (R04, R05, R06, R07, Q01, Q02, Q03)

Tanggal: 29 September 2026
Status Batch: SELESAI & TERVERIFIKASI (207 Integration Tests Passed 100%, 41 Frontend Unit Tests Passed, Frontend Build Bersih, 0 Alembic Drift)
Baseline Head: f1a2b3c4d5e6
Lingkungan Uji: Host 187.77.126.230, PostgreSQL 16 (scola_assessment_test), user cbt


1. Ringkasan Temuan, Akar Masalah & Solusi

R04 — Penugasan Sesi & Form Peserta Belum Ditegakkan pada Alur Masuk Ujian

  • Akar Masalah:
  • Meskipun model dan tabel EnrollmentSessionAssignment sudah ada di skema basis data, alur runner sebelumnya tidak memperhitungkan tabel ini:
    1. lookup_participant_credential mengembalikan seluruh sesi yang aktif di event tanpa menyaring sesi yang secara spesifik ditugaskan kepada peserta.
    2. start_or_resume_attempt mengizinkan peserta memulai ujian pada sesi mana pun di event terkait tanpa memverifikasi penugasan sesi, dan hanya memuat form default sesi (ExamForm.session_id == session.id) tanpa memuat form khusus yang ditugaskan di EnrollmentSessionAssignment.form_id.
    3. create_launch_authorization (SEB launcher) memilih sesi secara sembarang tanpa memeriksa penugasan peserta.
  • Perbaikan:
  • Di backend/app/services/runner_session_service.py:
    • Pada lookup_participant_credential: Memeriksa keberadaan rekaman EnrollmentSessionAssignment untuk peserta. Jika rekaman penugasan ada, daftar available_sessions difilter ketat hanya untuk sesi yang ditugaskan.
    • Pada start_or_resume_attempt: Memvalidasi penugasan sesi. Jika peserta memiliki penugasan sesi dan mencoba memulai ujian pada sesi yang tidak ditugaskan, sistem menolak dengan HTTP 403 Forbidden ("Peserta tidak memiliki penugasan untuk sesi ujian ini."). Jika penugasan menyertakan form_id khusus, paket soal dibaca langsung dari form yang ditugaskan tersebut.
  • Di backend/app/services/seb_service.py:create_launch_authorization:
    • Menyaring sesi berdasarkan EnrollmentSessionAssignment peserta sebelum menerbitkan token otorisasi peluncuran SEB.

R05 — Ketiadaan Batas Ukuran Payload Impor & Pagination Soal / Sweeper

  • Akar Masalah:
  • Endpoint upload berkas impor soal (/api/v1/authoring/questions/import) dan impor peserta (/api/v1/participants/import-csv) membaca file tanpa batas ukuran streaming, berisiko memicu memory exhaustion / DoS pada server saat ribuan peserta diunggah.
  • Pengambilan daftar soal (list_questions) mengambil seluruh baris tanpa batas (limit/offset), memperlambat respons aplikasi pada bank soal berukuran besar.
  • Pembersihan attempt kedaluwarsa (sweep_expired_attempts) mengambil seluruh baris sekaligus tanpa batasan batch size.
  • Perbaikan:
  • Batas Ukuran Upload (10 MB):
    • Di backend/app/api/v1/questions.py dan backend/app/api/v1/participants.py: Membatasi pembacaan berkas (file.read(MAX_UPLOAD_SIZE + 1)). Jika melebihi MAX_UPLOAD_SIZE = 10 * 1024 * 1024 (10 MB), request langsung ditolak dengan HTTP 413 Request Entity Too Large ("Ukuran berkas melebihi batas maksimal 10 MB").
  • Paginasi Bank Soal:
    • Di backend/app/services/question_service.py:list_questions: Menambahkan parameter limit: int = 100 dan offset: int = 0 dengan query .offset(offset).limit(limit).
    • Di backend/app/api/v1/questions.py: Mengekspos query parameter limit: int = Query(default=100, le=500) dan offset: int = Query(default=0, ge=0).
  • Batch Bounding pada Sweeper:
    • Di backend/app/workers/sweeper.py:sweep_expired_attempts: Menambahkan parameter batch_size: int = 100 dan menerapkan .limit(batch_size) pada query locking with_for_update(skip_locked=True).

R06 — Observability Terbatas pada Backlog Outbox & Attempt Kedaluwarsa

  • Akar Masalah:
  • Endpoint /health/metrics hanya menampilkan statistik connection pool dan hitungan active_attempts tanpa visibilitas antrean asynchronous outbox event dan attempt yang terlewat batas waktu auto-submit.
  • Perbaikan:
  • Di backend/app/api/v1/health.py:health_metrics:
    • Menghitung jumlah pesan outbox tertunda: pending_outbox = db.query(OutboxMessage).filter(OutboxMessage.status == "pending").count()
    • Menghitung jumlah attempt aktif yang telah melewati batas deadline: overdue_attempts = db.query(Attempt).filter(Attempt.status.in_(["in_progress", "paused"]), Attempt.server_deadline <= now).count()
    • Mengekspos nilai pending_outbox_events dan overdue_unfinalized_attempts pada respons JSON.

R07 — Sesi Staf Aktif Tidak Dicabut Saat Ganti Password

  • Akar Masalah:
  • Ketika pengguna staf mengubah kata sandi di auth_service.py:change_password, hanya hash password yang diperbarui. Token sesi staf aktif (StaffSession) yang tersimpan di browser lain tidak dicabut secara otomatis.
  • Perbaikan:
  • Di backend/app/services/auth_service.py:change_password:
    • Mengambil seluruh sesi staf aktif (user_id == user.id, revoked_at.is_(None)).
    • Menyetel s.revoked_at = datetime.now(timezone.utc) secara seketika pada semua sesi tersebut.

Q01 — Frontend Test Gate Terhambat Kegagalan Pinia & Selector Uji

  • Akar Masalah:
  • Pengujian Vitest di frontend/tests/unit/help_guide.test.ts gagal karena setActivePinia(createPinia()) tidak diinisialisasi dalam hook pengujian, serta adanya perbedaan ekspektasi teks warna status proctoring ('Oranye' vs 'Kuning').
  • Pengujian router.test.ts mencoba mengakses path /organizer biasa alih-alih rute dengan izin spesifik (/organizer/events).
  • Perbaikan:
  • Di frontend/tests/unit/help_guide.test.ts: Menambahkan inisialisasi Pinia di beforeEach() dan menyelaraskan penegasan teks warna status dengan implementasi komponen.
  • Di frontend/tests/unit/router.test.ts: Memperbarui navigasi rute terbatas proctoring ke /organizer/events.
  • Hasil: Seluruh 41 unit tests di frontend berhasil lulus 100% tanpa galat.

Q02 — Aksesibilitas Formulir Jawaban, Dukungan KaTeX & Focus Trap Modal

  • Akar Masalah:
  • Pilihan ganda pada QuestionContentCard.vue menggunakan binding teks mentah tanpa rendering KaTeX/HTML yang aman.
  • Input numerik tidak memiliki label eksplisit untuk screen reader dan tidak mengembalikan nilai null saat dikosongkan.
  • Indikator status penyimpanan tidak memiliki aria-live="polite".
  • Modal panduan (HelpModalBase.vue) belum menerapkan focus trap (penguncian tombol Tab di dalam modal) dan pemulihan fokus elemen asal saat ditutup.
  • Perbaikan:
  • Di frontend/src/components/runner/QuestionContentCard.vue:
    • Menambahkan v-html="safeHtml(opt.text)" untuk merender formula matematika dan teks pilihan ganda secara aman.
    • Menghubungkan label dengan input numerik dan esai menggunakan :for dan :id.
    • Menambahkan aria-live="polite" pada indikator status penyimpanan.
    • Menambahkan fungsi onNumericInput yang memancarkan update:numericValue dengan null saat input dikosongkan.
  • Di frontend/src/components/common/HelpModalBase.vue:
    • Mengimplementasikan penanganan siklus Tab (focus trap) agar fokus keyboard tidak keluar dari dialog modal saat terbuka.
    • Menyimpan previousActiveElement saat modal dibuka dan mengembalikan fokus keyboard ke elemen pemanggil saat modal ditutup.

Q03 — Penyelarasan Model SQLAlchemy dengan Skema Migrasi Alembic (0 Drift)

  • Akar Masalah:
  • Perintah alembic check mendeteksi ketidaksesuaian (drift) antara model SQLAlchemy dan database PostgreSQL:
    1. Indeks komposit ix_camera_evidences_attempt_status di tabel camera_evidences tidak ada di model CameraEvidence.
    2. Indeks ix_enr_sess_assign_enrollment dan ix_enr_sess_assign_session di model EnrollmentSessionAssignment menggunakan nama default SQLAlchemy alih-alih nama eksplisit migrasi.
    3. Indeks ix_result_publications_session_active di tabel result_publications tidak dideklarasikan di model ResultPublication.
    4. Kolom token_hash di StaffSession dan attempt_id di SubmissionReceipt memiliki unique=True di level kolom, yang menyebabkan SQLAlchemy memperlakukannya sebagai indeks unik tunggal alih-alih pasangan constraint unik + indeks non-unik sesuai DDL PostgreSQL.
  • Perbaikan:
  • Di backend/app/models/proctor.py (CameraEvidence):
    • Mengimpor Index dan menambahkan Index("ix_camera_evidences_attempt_status", "attempt_id", "status") di __table_args__.
  • Di backend/app/models/event.py (EnrollmentSessionAssignment):
    • Menghapus index=True di level kolom dan mendeklarasikan Index("ix_enr_sess_assign_enrollment", "enrollment_id") serta Index("ix_enr_sess_assign_session", "session_id") di __table_args__.
  • Di backend/app/models/attempt.py (ResultPublication & SubmissionReceipt):
    • Menambahkan Index("ix_result_publications_session_active", "session_id", "is_active") di ResultPublication.__table_args__.
    • Menghapus unique=True dari kolom attempt_id di SubmissionReceipt dan mendeklarasikan UniqueConstraint("attempt_id", name="submission_receipts_attempt_id_key") di __table_args__.
  • Di backend/app/models/organizer.py (StaffSession):
    • Menghapus unique=True dari kolom token_hash dan mendeklarasikan UniqueConstraint("token_hash", name="staff_sessions_token_hash_key") di __table_args__.
  • Hasil: PYTHONPATH=backend .venv/bin/alembic -c backend/alembic.ini check menghasilkan No new upgrade operations detected (0 drift, exit code 0).

2. Bukti Pengujian & Verifikasi

A. Pengujian Regresi Khusus Batch 6 (tests/integration/test_batch6_regression.py)

==================== test session starts =====================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
collected 10 items

tests/integration/test_batch6_regression.py::test_r04_admission_session_filter_lookup_runner_credentials PASSED [ 10%]
tests/integration/test_batch6_regression.py::test_r04_start_attempt_enforces_assigned_session PASSED [ 20%]
tests/integration/test_batch6_regression.py::test_r04_assigned_form_loaded_when_specified PASSED [ 30%]
tests/integration/test_batch6_regression.py::test_r04_launch_authorization_selects_assigned_session PASSED [ 40%]
tests/integration/test_batch6_regression.py::test_r05_upload_size_limit_rejected_with_413 PASSED [ 50%]
tests/integration/test_batch6_regression.py::test_r05_list_questions_pagination_enforced PASSED [ 60%]
tests/integration/test_batch6_regression.py::test_r05_sweeper_batch_size_bounded PASSED [ 70%]
tests/integration/test_batch6_regression.py::test_r06_health_metrics_reports_outbox_and_overdue_attempts PASSED [ 80%]
tests/integration/test_batch6_regression.py::test_r07_change_password_revokes_active_staff_sessions PASSED [ 90%]
tests/integration/test_batch6_regression.py::test_q03_alembic_models_match_migrations PASSED [100%]

=============== 10 passed, 6 warnings in 8.01s ===============

B. Pengujian Unit Frontend (frontend/ npm test)

 RUN  v5.0.1 /home/cbt/scola-assessment/frontend

 ✓ tests/unit/router.test.ts (3 tests)
 ✓ tests/unit/help_guide.test.ts (10 tests)
 ✓ tests/unit/useExamRunner.test.ts (7 tests)
 ✓ tests/unit/safeHtml.test.ts (2 tests)
 ✓ tests/unit/auth.test.ts (1 test)
 ✓ tests/unit/confirm_dialog.test.ts (3 tests)
 ✓ tests/unit/proctor_flow.test.ts (5 tests)
 ✓ tests/unit/grader_flow.test.ts (5 tests)
 ✓ tests/unit/organizer_flow.test.ts (3 tests)
 ✓ tests/unit/api.test.ts (2 tests)

 Test Files  10 passed (10)
      Tests  41 passed (41)
   Duration  9.28s

C. Build Produksi Frontend (frontend/ npm run build)

> vue-tsc && vite build
vite v8.3.0 building client environment for production...
✓ 389 modules transformed.
✓ built in 1.17s (0 errors)

D. Seluruh Rangkaian Pengujian Integrasi Repositori

207 passed, 8 warnings in 107.30s (0:01:47)
Tingkat Keberhasilan: 100% (207 dari 207 tests passed)

3. Matriks Status Remediasi Menyeluruh

Temuan Deskripsi Batch Status Bukti Pengujian
B03 Server deadline non-fail-closed Batch 1 SELESAI test_b03_b06_regression.py
B06 Status attempt tidak sinkron Batch 1 SELESAI test_b03_b06_regression.py
B01 Kebocoran kunci jawaban di HTML Batch 2 SELESAI test_batch2_regression.py
B07 Injeksi formula LaTeX / KaTeX Batch 2 SELESAI test_batch2_regression.py
B08 Race condition shuffle jawaban Batch 2 SELESAI test_batch2_regression.py
B09 Validasi toleransi numerik float Batch 2 SELESAI test_batch2_regression.py
B11 Konkurensi start crash IntegrityError Batch 2 SELESAI test_b11_concurrent_start_regression.py
B04 Form locking & lifecycle event Batch 3 SELESAI test_batch3_regression.py
B05 Credential rotation & runner revocation Batch 3 SELESAI test_batch3_regression.py
B10 Dual camera readiness timeout & hold Batch 3 SELESAI test_batch3_regression.py
R03 Pembatalan publikasi hasil & audit trail Batch 3 SELESAI test_batch3_regression.py
B12 Validasi format file impor peserta Batch 4 SELESAI test_batch4_regression.py
B13 XSS sanitizer & KaTeX math render Batch 4 SELESAI test_batch4_regression.py
B14 Pembatasan sesi proctoring multi-ruang Batch 4 SELESAI test_batch4_regression.py
B15 OTP recovery token leakage di response Batch 4 SELESAI test_batch4_regression.py
R01 Sweeper standalone & outbox handlers Batch 5 SELESAI test_batch5_regression.py
R02 Pessimistic locking & unique constraint Batch 5 SELESAI test_batch5_regression.py
B02 SEB HTTP probe rejection & BEK enforcement Batch 5 SELESAI test_batch5_regression.py
R04 Penugasan sesi & form spesifik peserta Batch 6 SELESAI test_batch6_regression.py
R05 Batas upload 10MB, paginasi & sweep bounds Batch 6 SELESAI test_batch6_regression.py
R06 Metrik outbox & overdue attempts Batch 6 SELESAI test_batch6_regression.py
R07 Pencabutan sesi staf saat ganti password Batch 6 SELESAI test_batch6_regression.py
Q01 Frontend Vitest test gate passed Batch 6 SELESAI frontend/ npm test (41/41)
Q02 KaTeX pilihan ganda & focus trap modal Batch 6 SELESAI QuestionContentCard, HelpModalBase
Q03 0 drift Alembic migration check Batch 6 SELESAI alembic check