refactor(routes): remove /api/lesson-progress aliases and standardize tests to /lesson-progress

This commit is contained in:
a2nr 2026-08-30 04:45:37 +07:00
parent 41c434f064
commit ac3fdb8dff
4 changed files with 41 additions and 43 deletions

View File

@ -51,7 +51,6 @@ def _parse_ts(value, *, required: bool) -> datetime | None:
@lesson_progress_bp.route("/lesson-progress", methods=["POST"]) @lesson_progress_bp.route("/lesson-progress", methods=["POST"])
@lesson_progress_bp.route("/api/lesson-progress", methods=["POST"])
def submit_lesson_progress(): def submit_lesson_progress():
if request.content_length is not None and request.content_length > MAX_PAYLOAD_BYTES: if request.content_length is not None and request.content_length > MAX_PAYLOAD_BYTES:
return jsonify({"success": False, "message": "Payload terlalu besar"}), 413 return jsonify({"success": False, "message": "Payload terlalu besar"}), 413
@ -237,7 +236,6 @@ def submit_lesson_progress():
@lesson_progress_bp.route("/lesson-progress/<lesson_name>", methods=["GET"]) @lesson_progress_bp.route("/lesson-progress/<lesson_name>", methods=["GET"])
@lesson_progress_bp.route("/api/lesson-progress/<lesson_name>", methods=["GET"])
def get_lesson_progress(lesson_name: str): def get_lesson_progress(lesson_name: str):
"""Ambil progress lesson + audit attempt kuis (review-after-refresh). """Ambil progress lesson + audit attempt kuis (review-after-refresh).

View File

@ -1,4 +1,4 @@
"""Flow komposit end-to-end via /api/lesson-progress. """Flow komposit end-to-end via /lesson-progress.
Skenario utama: Skenario utama:
- Lesson dengan BOTH exercise (tab c) DAN quiz → exercise + quiz 3/4 - Lesson dengan BOTH exercise (tab c) DAN quiz → exercise + quiz 3/4
@ -93,19 +93,19 @@ def _quiz_payload(attempt_id: str, score: str, status: str = "submitted") -> dic
def _post_exercise(client): def _post_exercise(client):
return client.post( return client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": LESSON_BOTH, "type": "exercise"}, json={"token": STUDENT_TOKEN, "lesson_name": LESSON_BOTH, "type": "exercise"},
) )
def _fetch(client): def _fetch(client):
return client.get(f"/api/lesson-progress/{LESSON_BOTH}?token={STUDENT_TOKEN}") return client.get(f"/lesson-progress/{LESSON_BOTH}?token={STUDENT_TOKEN}")
def test_exercise_then_quiz_makes_done(client): def test_exercise_then_quiz_makes_done(client):
_post_exercise(client) _post_exercise(client)
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json=_quiz_payload(str(uuid.uuid4()), "3/4"), json=_quiz_payload(str(uuid.uuid4()), "3/4"),
) )
assert resp.status_code == 200 assert resp.status_code == 200
@ -125,7 +125,7 @@ def test_exercise_only_not_done_without_quiz(client):
def test_low_quiz_below_threshold_not_done(client): def test_low_quiz_below_threshold_not_done(client):
_post_exercise(client) _post_exercise(client)
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json=_quiz_payload(str(uuid.uuid4()), "0/4"), json=_quiz_payload(str(uuid.uuid4()), "0/4"),
) )
assert resp.status_code == 200 assert resp.status_code == 200
@ -147,7 +147,7 @@ def test_reading_flow_returns_audit_answer_fields(client):
} }
] ]
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={ json={
**_quiz_payload(str(uuid.uuid4()), "4/4"), **_quiz_payload(str(uuid.uuid4()), "4/4"),
"answers": ans, "answers": ans,

View File

@ -60,7 +60,7 @@ def _seed(seed_demo_users):
def test_exercise_marks_exercise_passed(client): def test_exercise_marks_exercise_passed(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": LESSON, "type": "exercise"}, json={"token": STUDENT_TOKEN, "lesson_name": LESSON, "type": "exercise"},
) )
assert resp.status_code == 200 assert resp.status_code == 200
@ -69,7 +69,7 @@ def test_exercise_marks_exercise_passed(client):
def test_exercise_invalid_token_rejected(client): def test_exercise_invalid_token_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": "TOKEN_SALAH", "lesson_name": LESSON, "type": "exercise"}, json={"token": "TOKEN_SALAH", "lesson_name": LESSON, "type": "exercise"},
) )
assert resp.status_code == 401 assert resp.status_code == 401
@ -77,7 +77,7 @@ def test_exercise_invalid_token_rejected(client):
def test_exercise_unknown_lesson_rejected(client): def test_exercise_unknown_lesson_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": "tidak_ada", "type": "exercise"}, json={"token": STUDENT_TOKEN, "lesson_name": "tidak_ada", "type": "exercise"},
) )
assert resp.status_code == 404 assert resp.status_code == 404
@ -87,7 +87,7 @@ def test_exercise_unknown_lesson_rejected(client):
def test_missing_token_rejected(client): def test_missing_token_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"lesson_name": LESSON, "type": "exercise"}, json={"lesson_name": LESSON, "type": "exercise"},
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -95,7 +95,7 @@ def test_missing_token_rejected(client):
def test_missing_lesson_rejected(client): def test_missing_lesson_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "type": "exercise"}, json={"token": STUDENT_TOKEN, "type": "exercise"},
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -103,7 +103,7 @@ def test_missing_lesson_rejected(client):
def test_missing_type_rejected(client): def test_missing_type_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": LESSON}, json={"token": STUDENT_TOKEN, "lesson_name": LESSON},
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -111,7 +111,7 @@ def test_missing_type_rejected(client):
def test_invalid_type_rejected(client): def test_invalid_type_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": LESSON, "type": "biasa"}, json={"token": STUDENT_TOKEN, "lesson_name": LESSON, "type": "biasa"},
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -121,7 +121,7 @@ def test_invalid_type_rejected(client):
def test_quiz_submit_creates_attempt(client): def test_quiz_submit_creates_attempt(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json=_payload({"status": "submitted", "termination_reason": None, "score": "4/4"}), json=_payload({"status": "submitted", "termination_reason": None, "score": "4/4"}),
) )
assert resp.status_code == 200 assert resp.status_code == 200
@ -133,17 +133,17 @@ def test_quiz_submit_creates_attempt(client):
def test_quiz_retry_same_attempt_id_idempotent(client): def test_quiz_retry_same_attempt_id_idempotent(client):
payload = _payload() payload = _payload()
first = client.post("/api/lesson-progress", json=payload) first = client.post("/lesson-progress", json=payload)
assert first.status_code == 200 assert first.status_code == 200
assert first.get_json()["idempotent"] is False assert first.get_json()["idempotent"] is False
second = client.post("/api/lesson-progress", json=payload) second = client.post("/lesson-progress", json=payload)
assert second.status_code == 200 assert second.status_code == 200
assert second.get_json()["idempotent"] is True assert second.get_json()["idempotent"] is True
def test_quiz_focus_lost_terminated_accepted(client): def test_quiz_focus_lost_terminated_accepted(client):
resp = client.post("/api/lesson-progress", json=_payload()) resp = client.post("/lesson-progress", json=_payload())
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.get_json()["success"] is True assert resp.get_json()["success"] is True
@ -156,7 +156,7 @@ def test_quiz_beacon_format_accepted(client):
_payload({"status": "terminated", "termination_reason": "page_unload", "score": "0/4"}) _payload({"status": "terminated", "termination_reason": "page_unload", "score": "0/4"})
) )
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
data=payload, data=payload,
content_type="application/json", content_type="application/json",
) )
@ -165,14 +165,14 @@ def test_quiz_beacon_format_accepted(client):
def test_quiz_invalid_status_rejected(client): def test_quiz_invalid_status_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"status": "cheated"}) "/lesson-progress", json=_payload({"status": "cheated"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_focus_lost_must_be_terminated(client): def test_quiz_focus_lost_must_be_terminated(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json=_payload({"status": "submitted", "termination_reason": "focus_lost", "score": "4/4"}), json=_payload({"status": "submitted", "termination_reason": "focus_lost", "score": "4/4"}),
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -180,7 +180,7 @@ def test_quiz_focus_lost_must_be_terminated(client):
def test_quiz_submitted_requires_null_reason(client): def test_quiz_submitted_requires_null_reason(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json=_payload({"status": "submitted", "termination_reason": "user_exit"}), json=_payload({"status": "submitted", "termination_reason": "user_exit"}),
) )
assert resp.status_code == 400 assert resp.status_code == 400
@ -188,62 +188,62 @@ def test_quiz_submitted_requires_null_reason(client):
def test_quiz_invalid_reason_rejected(client): def test_quiz_invalid_reason_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"termination_reason": "minimize_app"}) "/lesson-progress", json=_payload({"termination_reason": "minimize_app"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_invalid_score_rejected(client): def test_quiz_invalid_score_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"score": "9/4"}) "/lesson-progress", json=_payload({"score": "9/4"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_malformed_attempt_id_rejected(client): def test_quiz_malformed_attempt_id_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"attempt_id": "bukan-uuid"}) "/lesson-progress", json=_payload({"attempt_id": "bukan-uuid"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_invalid_timestamp_rejected(client): def test_quiz_invalid_timestamp_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"occurred_at": "kemarin-sore"}) "/lesson-progress", json=_payload({"occurred_at": "kemarin-sore"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_negative_visibility_count_rejected(client): def test_quiz_negative_visibility_count_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"visibility_event_count": -1}) "/lesson-progress", json=_payload({"visibility_event_count": -1})
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_malformed_json_rejected(client): def test_quiz_malformed_json_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", data="{rusak", content_type="application/json" "/lesson-progress", data="{rusak", content_type="application/json"
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_quiz_invalid_token_rejected(client): def test_quiz_invalid_token_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"token": "TOKEN_SALAH"}) "/lesson-progress", json=_payload({"token": "TOKEN_SALAH"})
) )
assert resp.status_code == 401 assert resp.status_code == 401
def test_quiz_unknown_lesson_rejected(client): def test_quiz_unknown_lesson_rejected(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"lesson_name": "tidak_ada"}) "/lesson-progress", json=_payload({"lesson_name": "tidak_ada"})
) )
assert resp.status_code == 404 assert resp.status_code == 404
def test_quiz_response_contains_no_raw_token(client): def test_quiz_response_contains_no_raw_token(client):
resp = client.post("/api/lesson-progress", json=_payload()) resp = client.post("/lesson-progress", json=_payload())
assert resp.status_code == 200 assert resp.status_code == 200
text = resp.get_data(as_text=True) text = resp.get_data(as_text=True)
assert STUDENT_TOKEN not in text assert STUDENT_TOKEN not in text
@ -251,34 +251,34 @@ def test_quiz_response_contains_no_raw_token(client):
def test_quiz_error_does_not_leak_internal_details(client): def test_quiz_error_does_not_leak_internal_details(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", json=_payload({"attempt_id": "bukan-uuid"}) "/lesson-progress", json=_payload({"attempt_id": "bukan-uuid"})
) )
assert resp.status_code == 400 assert resp.status_code == 400
assert "traceback" not in resp.get_data(as_text=True).lower() assert "traceback" not in resp.get_data(as_text=True).lower()
# ── GET /api/lesson-progress/<lesson_name> (review-after-refresh) ── # ── GET /lesson-progress/<lesson_name> (review-after-refresh) ──
def test_get_progress_requires_token(client): def test_get_progress_requires_token(client):
resp = client.get(f"/api/lesson-progress/{LESSON}") resp = client.get(f"/lesson-progress/{LESSON}")
assert resp.status_code == 400 assert resp.status_code == 400
def test_get_progress_invalid_token_rejected(client): def test_get_progress_invalid_token_rejected(client):
resp = client.get(f"/api/lesson-progress/{LESSON}?token=TOKEN_SALAH") resp = client.get(f"/lesson-progress/{LESSON}?token=TOKEN_SALAH")
assert resp.status_code == 401 assert resp.status_code == 401
def test_get_progress_unknown_lesson_rejected(client): def test_get_progress_unknown_lesson_rejected(client):
resp = client.get("/api/lesson-progress/tidak_ada?token=" + STUDENT_TOKEN) resp = client.get("/lesson-progress/tidak_ada?token=" + STUDENT_TOKEN)
assert resp.status_code == 404 assert resp.status_code == 404
def test_get_progress_returns_audit_after_quiz(client): def test_get_progress_returns_audit_after_quiz(client):
payload = _payload({"status": "submitted", "termination_reason": None, "score": "3/4"}) payload = _payload({"status": "submitted", "termination_reason": None, "score": "3/4"})
client.post("/api/lesson-progress", json=payload) client.post("/lesson-progress", json=payload)
resp = client.get(f"/api/lesson-progress/{LESSON}?token={STUDENT_TOKEN}") resp = client.get(f"/lesson-progress/{LESSON}?token={STUDENT_TOKEN}")
assert resp.status_code == 200 assert resp.status_code == 200
body = resp.get_json() body = resp.get_json()
assert body["success"] is True assert body["success"] is True

View File

@ -29,14 +29,14 @@ def _seed(seed_demo_users):
def _post_exercise(client, lesson="hello_world"): def _post_exercise(client, lesson="hello_world"):
return client.post( return client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": STUDENT_TOKEN, "lesson_name": lesson, "type": "exercise"}, json={"token": STUDENT_TOKEN, "lesson_name": lesson, "type": "exercise"},
) )
def _post_quiz(client, score, answers=None, lesson="hello_world"): def _post_quiz(client, score, answers=None, lesson="hello_world"):
return client.post( return client.post(
"/api/lesson-progress", "/lesson-progress",
json={ json={
"token": STUDENT_TOKEN, "token": STUDENT_TOKEN,
"lesson_name": lesson, "lesson_name": lesson,
@ -61,14 +61,14 @@ def test_lesson_progress_exercise_updates(client):
def test_lesson_progress_invalid_token(client): def test_lesson_progress_invalid_token(client):
resp = client.post( resp = client.post(
"/api/lesson-progress", "/lesson-progress",
json={"token": "TOKEN_SALAH", "lesson_name": "hello_world", "type": "exercise"}, json={"token": "TOKEN_SALAH", "lesson_name": "hello_world", "type": "exercise"},
) )
assert resp.status_code == 401 assert resp.status_code == 401
def test_lesson_progress_requires_fields(client): def test_lesson_progress_requires_fields(client):
resp = client.post("/api/lesson-progress", json={"token": STUDENT_TOKEN}) resp = client.post("/lesson-progress", json={"token": STUDENT_TOKEN})
assert resp.status_code == 400 assert resp.status_code == 400