2026-08-08 11:57:08 +07:00
|
|
|
"""
|
|
|
|
|
Kontrak persistence token/progress — mengunci perilaku yang HARUS dipertahankan
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
setelah cutover penuh ke PostgreSQL (backend CSV sudah dicabut).
|
2026-08-08 11:57:08 +07:00
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
Suite ini berjalan TERHADAP PostgreSQL nyata (butuh DATABASE_URL). Seeding
|
|
|
|
|
dilakukan via repositories langsung — bukan lewat file CSV.
|
|
|
|
|
|
|
|
|
|
Semantik PG yang dikunci di sini:
|
|
|
|
|
- lesson tanpa record progress → "" (blank = belum mulai), bukan "not_started".
|
|
|
|
|
- skor terstruktur → status "3/4".
|
2026-08-08 11:57:08 +07:00
|
|
|
"""
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-08-08 11:57:08 +07:00
|
|
|
from services import token_service as ts
|
|
|
|
|
from services.tests.conftest import STUDENT2_TOKEN, STUDENT_TOKEN, TEACHER_TOKEN
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
pytestmark = pytest.mark.skipif(
|
|
|
|
|
not os.environ.get("DATABASE_URL"), reason="butuh PostgreSQL nyata"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _seed_database():
|
|
|
|
|
"""Reset + seed data uji: 1 guru, 2 siswa, 3 lesson, progress Budi."""
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
|
|
|
from services import repositories as repo
|
|
|
|
|
from services.database import SessionLocal
|
|
|
|
|
from services.models import Lesson
|
|
|
|
|
|
|
|
|
|
db = SessionLocal()
|
|
|
|
|
try:
|
|
|
|
|
db.execute(
|
|
|
|
|
text(
|
|
|
|
|
"TRUNCATE student_progress, access_tokens, lessons, users "
|
|
|
|
|
"RESTART IDENTITY CASCADE"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
teacher = repo.create_user(db, display_name="Pak Guru", role="teacher")
|
|
|
|
|
repo.create_access_token(db, user_id=teacher.id, raw_token=TEACHER_TOKEN)
|
|
|
|
|
budi = repo.create_user(db, display_name="Budi Santoso", role="student")
|
|
|
|
|
repo.create_access_token(db, user_id=budi.id, raw_token=STUDENT_TOKEN)
|
|
|
|
|
siti = repo.create_user(db, display_name="Siti Aminah", role="student")
|
|
|
|
|
repo.create_access_token(db, user_id=siti.id, raw_token=STUDENT2_TOKEN)
|
|
|
|
|
for idx, slug in enumerate(["hello_world", "quiz_test", "variabel"]):
|
|
|
|
|
db.add(Lesson(slug=slug, title=slug.replace("_", " ").title(), order_index=idx))
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
# progress Budi (seperti baris CSV lama)
|
|
|
|
|
ts.update_student_progress(STUDENT_TOKEN, "hello_world", "completed")
|
|
|
|
|
ts.update_student_progress(STUDENT_TOKEN, "quiz_test", "3/4")
|
|
|
|
|
finally:
|
|
|
|
|
db.close()
|
|
|
|
|
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
def test_valid_token_returns_student_info():
|
|
|
|
|
info = ts.validate_token(TEACHER_TOKEN)
|
|
|
|
|
assert info is not None
|
|
|
|
|
assert info["student_name"] == "Pak Guru"
|
|
|
|
|
assert info["is_teacher"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_valid_student_token_not_teacher():
|
|
|
|
|
info = ts.validate_token(STUDENT_TOKEN)
|
|
|
|
|
assert info is not None
|
|
|
|
|
assert info["is_teacher"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_token_rejected():
|
|
|
|
|
assert ts.validate_token("TOKEN_TIDAK_ADA") is None
|
|
|
|
|
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
def test_teacher_role_is_explicit_in_db():
|
2026-08-08 11:57:08 +07:00
|
|
|
assert ts.is_teacher_token(TEACHER_TOKEN) is True
|
|
|
|
|
assert ts.is_teacher_token(STUDENT_TOKEN) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_progress_is_effectively_not_started():
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
# Siswa tanpa record progress: semua lesson blank ("") — bukan completed.
|
2026-08-08 11:57:08 +07:00
|
|
|
progress = ts.get_student_progress(STUDENT2_TOKEN)
|
|
|
|
|
assert progress is not None
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
assert progress["hello_world"] == ""
|
2026-08-08 11:57:08 +07:00
|
|
|
assert not progress["quiz_test"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_completed_and_scored_both_count_as_completed():
|
|
|
|
|
progress = ts.get_student_progress(STUDENT_TOKEN)
|
|
|
|
|
assert progress["hello_world"] == "completed"
|
|
|
|
|
assert progress["quiz_test"] == "3/4"
|
|
|
|
|
lessons = [
|
|
|
|
|
{"filename": "hello_world.md"},
|
|
|
|
|
{"filename": "quiz_test.md"},
|
|
|
|
|
{"filename": "variabel.md"},
|
|
|
|
|
]
|
|
|
|
|
assert ts.calculate_student_completion(progress, lessons) == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_progress_is_idempotent():
|
|
|
|
|
assert ts.update_student_progress(STUDENT_TOKEN, "hello_world", "completed") is True
|
|
|
|
|
assert ts.update_student_progress(STUDENT_TOKEN, "hello_world", "completed") is True
|
|
|
|
|
progress = ts.get_student_progress(STUDENT_TOKEN)
|
|
|
|
|
assert progress["hello_world"] == "completed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_unknown_lesson_returns_false():
|
|
|
|
|
assert ts.update_student_progress(STUDENT_TOKEN, "tidak_ada", "completed") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_scored_status_preserved():
|
|
|
|
|
assert ts.update_student_progress(STUDENT_TOKEN, "quiz_test", "3/4") is True
|
|
|
|
|
progress = ts.get_student_progress(STUDENT_TOKEN)
|
|
|
|
|
assert progress["quiz_test"] == "3/4"
|