docs: record redesign review artifacts
This commit is contained in:
BIN
tasks/evidence/2026-09-24/concept-a-1440.png
Normal file
BIN
tasks/evidence/2026-09-24/concept-a-1440.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
BIN
tasks/evidence/2026-09-24/concept-a-390.png
Normal file
BIN
tasks/evidence/2026-09-24/concept-a-390.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
tasks/evidence/2026-09-24/concept-b-1440.png
Normal file
BIN
tasks/evidence/2026-09-24/concept-b-1440.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
BIN
tasks/evidence/2026-09-24/concept-b-390.png
Normal file
BIN
tasks/evidence/2026-09-24/concept-b-390.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
74
tasks/evidence/2026-09-24/create-local-baseline-fixtures.py
Normal file
74
tasks/evidence/2026-09-24/create-local-baseline-fixtures.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Create disposable Bundle 0 browser fixtures in the isolated Django test database.
|
||||
|
||||
Run only with --settings=config.settings.test and BUNDLE0_TEST_PASSWORD set.
|
||||
Never point TEST_DB_NAME at a shared or production database.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.blog.models import Post
|
||||
from apps.events.models import Event
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
password = os.environ["BUNDLE0_TEST_PASSWORD"]
|
||||
now = timezone.now()
|
||||
|
||||
|
||||
def fixture_user(username, mobile, *, staff=False):
|
||||
user, _ = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults={"mobile": mobile, "email": f"{username}@example.invalid"},
|
||||
)
|
||||
user.mobile = mobile
|
||||
user.email = f"{username}@example.invalid"
|
||||
user.is_email_verified = True
|
||||
user.is_mobile_verified = True
|
||||
user.is_staff = staff
|
||||
user.is_superuser = staff
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
student = fixture_user("bundle0_student", "09198000111")
|
||||
staff = fixture_user("bundle0_staff", "09198000112", staff=True)
|
||||
|
||||
for slug, title, price, capacity in (
|
||||
("bundle0-free", "Bundle 0 Free Event", 0, 20),
|
||||
("bundle0-full", "Bundle 0 Full Event", 0, 0),
|
||||
("bundle0-paid", "Bundle 0 Paid Event", 50000, 20),
|
||||
):
|
||||
Event.objects.update_or_create(
|
||||
slug=slug,
|
||||
defaults={
|
||||
"title": title,
|
||||
"description": "Local baseline event for browser review.",
|
||||
"start_time": now + timedelta(days=7),
|
||||
"end_time": now + timedelta(days=7, hours=2),
|
||||
"registration_start_date": now - timedelta(days=1),
|
||||
"registration_end_date": now + timedelta(days=6),
|
||||
"status": Event.StatusChoices.PUBLISHED,
|
||||
"price": price,
|
||||
"capacity": capacity,
|
||||
},
|
||||
)
|
||||
|
||||
Post.objects.update_or_create(
|
||||
slug="bundle0-article",
|
||||
defaults={
|
||||
"title": "Bundle 0 Article",
|
||||
"content": "# Local baseline article\n\nA sample article for the browser review.",
|
||||
"author": staff,
|
||||
"status": Post.StatusChoices.PUBLISHED,
|
||||
"published_at": now,
|
||||
},
|
||||
)
|
||||
|
||||
print("Created isolated student, staff, free/full/paid events, and article fixtures.")
|
||||
95
tasks/evidence/2026-09-24/replay-local-auth.py
Normal file
95
tasks/evidence/2026-09-24/replay-local-auth.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Replay signup, OTP login, and password reset in the isolated test database.
|
||||
|
||||
SMS and the short-lived OTP store are mocked. Run after the fixture seed with
|
||||
--settings=config.settings.test and BUNDLE0_TEST_PASSWORD set.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client, override_settings
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
password = os.environ["BUNDLE0_TEST_PASSWORD"]
|
||||
mobile = "09198000113"
|
||||
client = Client()
|
||||
|
||||
|
||||
class TemporaryOtpStore:
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
|
||||
def setex(self, key, _seconds, value):
|
||||
self.values[key] = str(value).encode("utf-8")
|
||||
|
||||
def get(self, key):
|
||||
return self.values.get(key)
|
||||
|
||||
def delete(self, key):
|
||||
self.values.pop(key, None)
|
||||
|
||||
|
||||
store = TemporaryOtpStore()
|
||||
|
||||
|
||||
def post(path, payload):
|
||||
return client.post(path, data=json.dumps(payload), content_type="application/json")
|
||||
|
||||
|
||||
with (
|
||||
override_settings(ALLOWED_HOSTS=["testserver", "localhost"]),
|
||||
patch("apps.users.services.auth._redis", return_value=store),
|
||||
patch("apps.users.services.auth.send_critical_sms.delay"),
|
||||
):
|
||||
invalid = post("/api/auth/otp/send", {"mobile": "123", "mode": "register"})
|
||||
assert invalid.status_code == 400
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:register:{mobile}").decode("utf-8")
|
||||
wrong = "00000" if code != "00000" else "99999"
|
||||
rejected = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": wrong})
|
||||
assert rejected.status_code == 400
|
||||
verified = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": code})
|
||||
assert verified.status_code == 200, verified.content
|
||||
registered = post(
|
||||
"/api/auth/register",
|
||||
{"username": "bundle0_signup", "mobile": mobile, "code": code, "password": password},
|
||||
)
|
||||
assert registered.status_code == 201, registered.content
|
||||
assert User.objects.get(username="bundle0_signup").is_mobile_verified
|
||||
|
||||
duplicate = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"})
|
||||
assert duplicate.status_code == 400
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "login"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:login:{mobile}").decode("utf-8")
|
||||
login = post("/api/auth/login/otp", {"mobile": mobile, "code": code})
|
||||
assert login.status_code == 200 and login.json()["access_token"]
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "reset_password"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:reset_password:{mobile}").decode("utf-8")
|
||||
new_password = password + "New"
|
||||
rejected = post(
|
||||
"/api/auth/reset-password",
|
||||
{"mobile": mobile, "code": wrong if wrong != code else "88888", "new_password": new_password},
|
||||
)
|
||||
assert rejected.status_code == 400
|
||||
changed = post(
|
||||
"/api/auth/reset-password",
|
||||
{"mobile": mobile, "code": code, "new_password": new_password},
|
||||
)
|
||||
assert changed.status_code == 200, changed.content
|
||||
old_login = post("/api/auth/login", {"identifier": mobile, "password": password})
|
||||
new_login = post("/api/auth/login", {"identifier": mobile, "password": new_password})
|
||||
assert old_login.status_code == 401 and new_login.status_code == 200
|
||||
|
||||
print("Auth replay: invalid OTP request and code rejected; signup, OTP login, and password reset succeeded; old password rejected.")
|
||||
85
tasks/evidence/2026-09-24/replay-local-payment.py
Normal file
85
tasks/evidence/2026-09-24/replay-local-payment.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Replay paid-event outcomes in the isolated Bundle 0 Django test database.
|
||||
|
||||
External gateway calls, notifications, and SMS are mocked. This script is baseline
|
||||
evidence, not a production payment test or a browser gateway substitute.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client, override_settings
|
||||
|
||||
from apps.events.models import Event, Registration
|
||||
from apps.payments.models import Payment
|
||||
from apps.users.models import User
|
||||
from core.authentication import create_jwt_token
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
|
||||
student = User.objects.get(username="bundle0_student")
|
||||
event = Event.objects.get(slug="bundle0-paid")
|
||||
client = Client()
|
||||
headers = {"HTTP_AUTHORIZATION": f"Bearer {create_jwt_token(student)}"}
|
||||
body = json.dumps({"event_id": event.pk, "description": "Local mocked payment replay"})
|
||||
|
||||
|
||||
def create_payment():
|
||||
return client.post(
|
||||
"/api/payments/create",
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
**headers,
|
||||
)
|
||||
|
||||
|
||||
with (
|
||||
override_settings(ALLOWED_HOSTS=["testserver", "localhost"]),
|
||||
patch("apps.payments.api.views.notify_user"),
|
||||
patch("apps.payments.api.views.send_critical_sms.delay"),
|
||||
patch("apps.payments.api.views.requests.post") as gateway,
|
||||
):
|
||||
gateway.side_effect = RuntimeError("simulated gateway outage")
|
||||
failed_request = create_payment()
|
||||
assert failed_request.status_code == 502, failed_request.content
|
||||
assert not Payment.objects.filter(user=student, event=event).exists()
|
||||
|
||||
gateway.side_effect = None
|
||||
gateway.return_value = Mock()
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "authority": "BUNDLE0-CANCEL"}
|
||||
}
|
||||
pending = create_payment()
|
||||
assert pending.status_code == 200, pending.content
|
||||
assert pending.json()["start_pay_url"]
|
||||
canceled = client.get(
|
||||
"/api/payments/callback",
|
||||
{"Authority": "BUNDLE0-CANCEL", "Status": "NOK"},
|
||||
)
|
||||
assert canceled.status_code == 302
|
||||
assert "status=failed" in canceled.url
|
||||
assert Payment.objects.get(authority="BUNDLE0-CANCEL").status == Payment.OrderStatusChoices.CANCELED
|
||||
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "authority": "BUNDLE0-SUCCESS"}
|
||||
}
|
||||
pending = create_payment()
|
||||
assert pending.status_code == 200, pending.content
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "ref_id": "BUNDLE0-REF"}
|
||||
}
|
||||
paid = client.get(
|
||||
"/api/payments/callback",
|
||||
{"Authority": "BUNDLE0-SUCCESS", "Status": "OK"},
|
||||
)
|
||||
assert paid.status_code == 302
|
||||
assert "status=success" in paid.url
|
||||
assert Payment.objects.get(authority="BUNDLE0-SUCCESS").status == Payment.OrderStatusChoices.PAID
|
||||
assert Registration.objects.get(user=student, event=event).status == Registration.StatusChoices.CONFIRMED
|
||||
duplicate = create_payment()
|
||||
assert duplicate.status_code == 400
|
||||
|
||||
print("Paid replay: gateway outage 502; cancellation redirect; mocked success and confirmed registration; duplicate blocked.")
|
||||
Reference in New Issue
Block a user