mathly / mathly2 /tests /test_migrations.py
wzender's picture
Claude
Prompter on the developer screen (add-prompter) (#8)
bc052e5 unverified
Raw History Blame Contribute Delete
35.8 kB
"""Apply the migrations to a real local PostgreSQL and check the browser roles cannot touch the tables."""
from pathlib import Path
import pytest
psycopg = pytest.importorskip("psycopg")
pgserver = pytest.importorskip("pgserver")
MIGRATIONS = sorted((Path(__file__).parent.parent / "supabase" / "migrations").glob("*.sql"))
@pytest.fixture(scope="module")
def db(tmp_path_factory):
server = pgserver.get_server(tmp_path_factory.mktemp("postgres") / "data", cleanup_mode="delete")
with psycopg.connect(server.get_uri(), autocommit=True) as c:
c.execute("create role anon; create role authenticated; create role service_role bypassrls;")
c.execute("grant usage on schema public to anon, authenticated, service_role;")
for path in MIGRATIONS:
c.execute(path.read_text(encoding="utf-8"))
yield server.get_uri()
server.cleanup()
def as_role(uri, role, sql, *params):
with psycopg.connect(uri, autocommit=True) as c:
c.execute(f"set role {role}")
return c.execute(sql, params).fetchall() if sql.lstrip().lower().startswith("select") else c.execute(sql, params)
def test_server_role_can_use_the_tables(db):
as_role(db, "service_role", "insert into public.m2_classes(name, teacher_email) values ('ט''3', 't@school.test')")
rows = as_role(db, "service_role", "select name from public.m2_classes")
assert rows == [("ט'3",)]
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_are_refused(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "insert into public.m2_classes(name, teacher_email) values ('x', 'a@b.test')")
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_class_members")
def test_row_level_security_is_on_and_has_no_policies(db):
with psycopg.connect(db, autocommit=True) as c:
on = dict(c.execute("select relname, relrowsecurity from pg_class where relname in ('m2_classes','m2_class_members')").fetchall())
policies = c.execute("select count(*) from pg_policies where tablename like 'm2_%'").fetchone()[0]
assert on == {"m2_classes": True, "m2_class_members": True}
assert policies == 0
def _class(c, name, teacher=None, lessons="{t}"):
return c.execute("insert into public.m2_classes(name, teacher_email, created_by, lessons) values (%s, %s, 'mgr@school.test', %s) returning id",
(name, teacher, lessons)).fetchone()[0]
def test_a_student_can_be_in_several_classes_but_only_once_in_each(db):
with psycopg.connect(db, autocommit=True) as c:
a, b = _class(c, "ו'1"), _class(c, "ו'2")
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'multi@school.test')", (a,))
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'multi@school.test')", (b,))
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'multi@school.test')", (a,))
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'UPPER@school.test')", (a,))
def test_class_names_are_unique_ignoring_case_across_creators(db):
with psycopg.connect(db, autocommit=True) as c:
_class(c, "Dalet")
with pytest.raises(psycopg.errors.UniqueViolation):
_class(c, "dALET", teacher="someone@school.test")
def test_a_class_can_have_no_teacher_and_has_the_ai_off_with_150_checks(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "ז'1")
assert c.execute("select teacher_email, ai_enabled, ai_daily_checks, lessons from public.m2_classes where id = %s", (cid,)).fetchone() == (None, False, 150, ["t"])
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("update public.m2_classes set ai_daily_checks = -1 where id = %s", (cid,))
def test_a_teacher_cannot_also_be_a_student_of_the_same_class(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "ח'7", teacher="teach@school.test")
with pytest.raises(psycopg.errors.CheckViolation, match="teacher_is_student"):
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'teach@school.test')", (cid,))
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'kid7@school.test')", (cid,))
with pytest.raises(psycopg.errors.CheckViolation, match="teacher_is_student"):
c.execute("update public.m2_classes set teacher_email = 'kid7@school.test' where id = %s", (cid,))
other = _class(c, "ח'8")
c.execute("insert into public.m2_class_members(class_id, email) values (%s, 'teach@school.test')", (other,)) # a student elsewhere is fine
def test_roles_accept_only_the_four_grantable_ones_in_lower_case(db):
with psycopg.connect(db, autocommit=True) as c:
for role in ("student", "teacher", "manager", "admin"):
c.execute("insert into public.m2_roles(email, role, granted_by) values ('grant@school.test', %s, 'a@school.test')", (role,))
for bad in (("grant@school.test", "developer"), ("grant@school.test", "boss"), ("Grant@school.test", "admin")):
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_roles(email, role) values (%s, %s)", bad)
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_roles(email, role) values ('grant@school.test', 'admin')")
def test_take_role_in_one_step_keeps_the_last_admin(db):
take = lambda email, role, settings=0: as_role(db, "service_role", "select public.m2_take_role(%s, %s, %s)", email, role, settings)[0][0]
with psycopg.connect(db, autocommit=True) as c:
c.execute("delete from public.m2_roles where role = 'admin'")
c.execute("insert into public.m2_roles(email, role) values ('adm1@school.test', 'admin'), ('adm2@school.test', 'admin'), ('mgr@school.test', 'manager')")
assert take("nobody@school.test", "admin") == "absent"
assert take("mgr@school.test", "manager") == "removed"
assert take("adm1@school.test", "admin") == "removed"
assert take("adm2@school.test", "admin") == "last_admin" # the only one left stays
assert take("adm2@school.test", "admin", 1) == "removed" # unless the settings name another admin
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select count(*) from public.m2_roles where role = 'admin'").fetchone()[0] == 0
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_read_roles_or_take_one_away(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_roles")
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select public.m2_take_role('a@school.test', 'admin', 0)")
def test_row_level_security_is_on_for_roles(db):
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select relrowsecurity from pg_class where relname = 'm2_roles'").fetchone()[0] is True
assert c.execute("select count(*) from pg_policies where tablename like 'm2_%'").fetchone()[0] == 0
# --- skill learning (task 2.1) ---
USER = "11111111-1111-1111-1111-111111111111"
OTHER = "22222222-2222-2222-2222-222222222222"
def test_daily_correct_counts_only_correct_answers_per_student_and_day(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("delete from public.m2_answers")
rows = [(USER, "s", "q1", True, True, "2026-11-02"), (USER, "s", "q2", True, False, "2026-11-02"),
(USER, "s", "q3", False, True, "2026-11-02"), (USER, "s", "q4", True, True, "2026-11-03"),
(OTHER, "s", "q1", True, True, "2026-11-02")]
for user, skill, key, correct, counted, day in rows:
c.execute("insert into public.m2_answers(user_id, skill_id, question_key, correct, counted, day) "
"values (%s, %s, %s, %s, %s, %s)", (user, skill, key, correct, counted, day))
got = as_role(db, "service_role", "select to_char(day, 'YYYY-MM-DD'), correct from public.m2_daily_correct "
"where user_id = %s order by day", USER)
assert got == [("2026-11-02", 2), ("2026-11-03", 1)] # the helped answer counts; the wrong one does not
def test_progress_row_is_unique_per_student_and_skill_with_defaults(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("insert into public.m2_skill_progress(user_id, skill_id) values (%s, 'a')", (USER,))
row = c.execute("select current_key, current_helped, started_at, mastered_at from public.m2_skill_progress "
"where user_id = %s and skill_id = 'a'", (USER,)).fetchone()
assert row == (None, False, None, None)
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_skill_progress(user_id, skill_id) values (%s, 'a')", (USER,))
def test_progress_no_longer_has_the_stage_columns_and_keeps_the_rest(db):
with psycopg.connect(db, autocommit=True) as c:
columns = {r[0] for r in c.execute("select column_name from information_schema.columns "
"where table_schema = 'public' and table_name = 'm2_skill_progress'").fetchall()}
assert columns == {"user_id", "skill_id", "current_key", "current_helped", "started_at", "mastered_at", "round_start"}
def test_round_start_is_empty_for_old_rows_and_cannot_be_negative(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("insert into public.m2_skill_progress(user_id, skill_id) values ('33333333-3333-3333-3333-333333333333', 'r')")
assert c.execute("select round_start from public.m2_skill_progress where skill_id = 'r'").fetchone() == (None,)
c.execute("update public.m2_skill_progress set round_start = 10 where skill_id = 'r'")
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("update public.m2_skill_progress set round_start = -1 where skill_id = 'r'")
@pytest.mark.parametrize("role", ["authenticated", "anon"])
@pytest.mark.parametrize("table", ["m2_answers", "m2_skill_progress", "m2_daily_correct"])
def test_browser_roles_cannot_read_skill_learning_objects(db, role, table):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_write_answers(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "insert into public.m2_answers(user_id, skill_id, question_key, correct, day) "
"values (%s, 's', 'q', true, '2026-11-02')", USER)
def test_row_level_security_is_on_for_the_new_tables(db):
with psycopg.connect(db, autocommit=True) as c:
on = dict(c.execute("select relname, relrowsecurity from pg_class "
"where relname in ('m2_answers','m2_skill_progress')").fetchall())
policies = c.execute("select count(*) from pg_policies where tablename like 'm2_%'").fetchone()[0]
assert on == {"m2_answers": True, "m2_skill_progress": True}
assert policies == 0
def test_server_role_can_insert_answers_and_progress(db):
as_role(db, "service_role", "insert into public.m2_answers(user_id, skill_id, question_key, correct, day) "
"values (%s, 'svc', 'q', true, '2026-11-04')", USER)
as_role(db, "service_role", "insert into public.m2_skill_progress(user_id, skill_id) values (%s, 'svc')", USER)
def test_last_answered_gives_the_newest_day_per_student_and_skill(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("delete from public.m2_answers")
for user, skill, key, day in ((USER, "s", "q1", "2026-11-02"), (USER, "s", "q2", "2026-11-09"), (USER, "t", "q1", "2026-11-03"),
(OTHER, "s", "q1", "2026-11-20")):
c.execute("insert into public.m2_answers(user_id, skill_id, question_key, correct, counted, day) values (%s, %s, %s, false, false, %s)",
(user, skill, key, day))
got = as_role(db, "service_role", "select skill_id, to_char(day, 'YYYY-MM-DD') from public.m2_last_answered where user_id = %s order by skill_id", USER)
assert got == [("s", "2026-11-09"), ("t", "2026-11-03")]
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_read_the_reward_tables(db, role):
for table in ("m2_comets", "m2_reward_state", "m2_last_answered"):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
def test_a_student_catches_each_comet_once_and_has_one_state_row(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("insert into public.m2_comets(user_id, comet_id) values (%s, 'pi')", (USER,))
c.execute("insert into public.m2_comets(user_id, comet_id) values (%s, 'pi')", (OTHER,)) # another student may have it too
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_comets(user_id, comet_id) values (%s, 'pi')", (USER,))
c.execute("insert into public.m2_reward_state(user_id) values (%s)", (USER,))
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_reward_state(user_id) values (%s)", (USER,))
assert c.execute("select last_comet_day, last_catch_day from public.m2_reward_state where user_id = %s", (USER,)).fetchone() == (None, None)
def test_attempts_have_defaults_and_refuse_bad_values(db):
with psycopg.connect(db, autocommit=True) as c:
row = c.execute("insert into public.m2_attempts(user_id, kind, scope, due_at) values (%s, 'quiz', 't', now() + interval '15 minutes') "
"returning submitted_at, timed_out, grade, questions, answers", (USER,)).fetchone()
assert row == (None, False, None, [], {})
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_attempts(user_id, kind, scope, due_at) values (%s, 'oral', 't', now() + interval '1 minute')", (USER,))
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_attempts(user_id, kind, scope, due_at, grade) values (%s, 'quiz', 't', now() + interval '1 minute', 101)", (USER,))
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_attempts(user_id, kind, scope, due_at) values (%s, 'quiz', 't', now() - interval '1 minute')", (USER,))
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_touch_attempts(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_attempts")
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "insert into public.m2_attempts(user_id, kind, scope, due_at) values ('11111111-1111-1111-1111-111111111111', 'quiz', 't', now() + interval '1 minute')")
def test_the_server_role_can_use_attempts_and_rls_has_no_policies(db):
as_role(db, "service_role", "insert into public.m2_attempts(user_id, kind, scope, due_at) values (%s, 'quiz', 'u', now() + interval '1 minute')", OTHER)
assert as_role(db, "service_role", "select count(*) from public.m2_attempts where user_id = %s", OTHER) == [(1,)]
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select relrowsecurity from pg_class where relname = 'm2_attempts'").fetchone() == (True,)
assert c.execute("select count(*) from pg_policies where tablename = 'm2_attempts'").fetchone() == (0,)
def test_a_test_has_defaults_limits_and_cascades_with_its_class(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "מבחן-1")
row = c.execute("insert into public.m2_tests(class_id, date, lessons, created_by) values (%s, '2026-11-20', '{t,t2}', 't@school.test') "
"returning questions, minutes, note, removed_at", (cid,)).fetchone()
assert row == (12, 45, "", None)
for column, value in (("questions", 0), ("questions", 41), ("minutes", 4), ("minutes", 181)):
with pytest.raises(psycopg.errors.CheckViolation):
c.execute(f"insert into public.m2_tests(class_id, date, lessons, created_by, {column}) values (%s, '2026-11-20', '{{t}}', 't@s', {value})", (cid,))
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_tests(class_id, date, lessons, created_by) values (%s, '2026-11-20', '{}', 't@s')", (cid,))
c.execute("delete from public.m2_classes where id = %s", (cid,))
assert c.execute("select count(*) from public.m2_tests where class_id = %s", (cid,)).fetchone() == (0,)
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_touch_tests(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_tests")
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "insert into public.m2_tests(class_id, date, lessons, created_by) values (1, '2026-11-20', '{t}', 'x')")
def test_the_server_role_can_use_tests_and_rls_has_no_policies(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "מבחן-2")
as_role(db, "service_role", "insert into public.m2_tests(class_id, date, lessons, created_by) values (%s, '2026-11-21', '{t}', 'x')", cid)
assert as_role(db, "service_role", "select count(*) from public.m2_tests where class_id = %s", cid) == [(1,)]
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select relrowsecurity from pg_class where relname = 'm2_tests'").fetchone() == (True,)
assert c.execute("select count(*) from pg_policies where tablename = 'm2_tests'").fetchone() == (0,)
# --- add-written-work 1.1 and 1.2 ---
U1, U2 = "44444444-4444-4444-4444-444444444444", "55555555-5555-5555-5555-555555555555"
def _assignment(c, class_id, **kw):
cols = {"title": "x", "due_date": "2026-11-20", "kind": "quiz", "quiz_lesson": "t", "created_by": "t@school.test", **kw}
names = ", ".join(["class_id"] + list(cols)); marks = ", ".join(["%s"] * (len(cols) + 1))
return c.execute(f"insert into public.m2_assignments({names}) values ({marks}) returning id", (class_id, *cols.values())).fetchone()[0]
def test_written_work_rows_have_their_defaults_and_checks(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "כתיבה-1")
pic = c.execute("insert into public.m2_pictures(owner_id, kind, path, content_type, size) values (%s, 'answer', 'a/1.jpg', 'image/jpeg', 100) returning id, deleted_at", (U1,)).fetchone()
assert pic[1] is None
aid = _assignment(c, cid)
assert c.execute("select note, removed_at, cardinality(pages) from public.m2_assignments where id = %s", (aid,)).fetchone() == ("", None, 0)
sub = c.execute("insert into public.m2_submissions(assignment_id, user_id) values (%s, %s) returning state, late, grade, teacher_note", (aid, U1)).fetchone()
assert sub == ("handed_in", False, None, "")
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_submissions(assignment_id, user_id) values (%s, %s)", (aid, U1))
req = c.execute("insert into public.m2_photo_requests(user_id, assignment_id, device, code, expires_at) values (%s, %s, 'd', '0042', now() + interval '10 minutes') returning state", (U1, aid)).fetchone()
assert req == ("waiting",)
def test_written_work_checks_refuse_bad_values(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "כתיבה-2")
bad = [
"insert into public.m2_pictures(owner_id, kind, path, content_type, size) values ('%s', 'scan', 'b/1.jpg', 'image/jpeg', 5)" % U1, # kind
"insert into public.m2_pictures(owner_id, kind, path, content_type, size) values ('%s', 'answer', 'b/2.jpg', 'image/jpeg', 0)" % U1, # size
]
for sql in bad:
with pytest.raises(psycopg.errors.CheckViolation):
c.execute(sql)
for kw in (dict(kind="pages"), dict(kind="quiz", quiz_lesson=None), dict(kind="exercise_test", quiz_lesson=None), dict(title="")):
with pytest.raises(psycopg.errors.CheckViolation):
_assignment(c, cid, **kw)
aid = _assignment(c, cid)
for sql in ("insert into public.m2_submissions(assignment_id, user_id, grade) values (%s, '%s', 101)" % (aid, U2),
"insert into public.m2_submissions(assignment_id, user_id, state) values (%s, '%s', 'graded')" % (aid, U2), # graded without a grade
"insert into public.m2_submissions(assignment_id, user_id, state) values (%s, '%s', 'lost')" % (aid, U2),
"insert into public.m2_photo_requests(user_id, assignment_id, device, code, expires_at) values ('%s', %s, 'd', '12', now() + interval '1 minute')" % (U2, aid),
"insert into public.m2_photo_requests(user_id, assignment_id, device, code, expires_at) values ('%s', %s, 'd', '1234', now() - interval '1 minute')" % (U2, aid)):
with pytest.raises(psycopg.errors.CheckViolation):
c.execute(sql)
def test_written_work_cascades_with_the_class_and_push_endpoints_are_unique(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "כתיבה-3")
aid = _assignment(c, cid)
c.execute("insert into public.m2_submissions(assignment_id, user_id) values (%s, %s)", (aid, U1))
c.execute("delete from public.m2_classes where id = %s", (cid,))
assert c.execute("select count(*) from public.m2_assignments where id = %s", (aid,)).fetchone() == (0,)
assert c.execute("select count(*) from public.m2_submissions where assignment_id = %s", (aid,)).fetchone() == (0,)
c.execute("insert into public.m2_push_subscriptions(user_id, endpoint, p256dh, auth, key_id) values (%s, 'https://push/x', 'p', 'a', 'k')", (U1,))
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_push_subscriptions(user_id, endpoint, p256dh, auth, key_id) values (%s, 'https://push/x', 'p', 'a', 'k')", (U2,))
@pytest.mark.parametrize("role", ["authenticated", "anon"])
@pytest.mark.parametrize("table", ["m2_pictures", "m2_assignments", "m2_submissions", "m2_photo_requests", "m2_push_subscriptions"])
def test_browser_roles_cannot_touch_written_work(db, role, table):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
def test_the_server_role_uses_written_work_tables_and_rls_has_no_policies(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "כתיבה-4")
aid = as_role(db, "service_role", "insert into public.m2_assignments(class_id, title, due_date, kind, quiz_lesson, created_by) values (%s, 'x', '2026-11-20', 'quiz', 't', 'x') returning id", cid)
assert aid is not None
with psycopg.connect(db, autocommit=True) as c:
on = dict(c.execute("select relname, relrowsecurity from pg_class where relname in ('m2_pictures','m2_assignments','m2_submissions','m2_photo_requests','m2_push_subscriptions')").fetchall())
assert all(on.values()) and len(on) == 5
assert c.execute("select count(*) from pg_policies where tablename in ('m2_pictures','m2_assignments','m2_submissions','m2_photo_requests','m2_push_subscriptions')").fetchone() == (0,)
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_touch_the_curriculum(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_curriculum")
def test_the_server_role_keeps_one_date_per_lesson_and_rls_has_no_policies(db):
as_role(db, "service_role", "insert into public.m2_curriculum(lesson_id, due_date) values ('t', '2026-11-03') on conflict (lesson_id) do update set due_date = excluded.due_date")
as_role(db, "service_role", "insert into public.m2_curriculum(lesson_id, due_date) values ('t', '2026-11-10') on conflict (lesson_id) do update set due_date = excluded.due_date")
assert as_role(db, "service_role", "select count(*) from public.m2_curriculum where lesson_id = 't'") == [(1,)]
with pytest.raises(psycopg.errors.NotNullViolation):
as_role(db, "service_role", "insert into public.m2_curriculum(lesson_id) values ('x')")
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select relrowsecurity from pg_class where relname = 'm2_curriculum'").fetchone() == (True,)
assert c.execute("select count(*) from pg_policies where tablename = 'm2_curriculum'").fetchone() == (0,)
# --- AI help ---
@pytest.mark.parametrize("role", ["authenticated", "anon"])
@pytest.mark.parametrize("table", ["m2_ai_requests", "m2_ai_allowance", "m2_ai_usage"])
def test_browser_roles_cannot_touch_ai_tables(db, role, table):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_call_the_budget_functions(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select public.m2_ai_take('class:1', '2026-11-09', 1, 5)")
def test_ai_requests_have_defaults_checks_and_cascade_with_the_class(db):
with psycopg.connect(db, autocommit=True) as c:
cid = _class(c, "בינה-1")
pic = c.execute("insert into public.m2_pictures(owner_id, kind, path, content_type, size) values ('11111111-1111-1111-1111-111111111111', 'help', 'h/1.jpg', 'image/jpeg', 10) returning id").fetchone()[0]
rid = c.execute("insert into public.m2_ai_requests(kind, user_id, class_id, picture_id) values ('photo', '11111111-1111-1111-1111-111111111111', %s, %s) returning id", (cid, pic)).fetchone()[0]
assert c.execute("select state, teacher_note from public.m2_ai_requests where id = %s", (rid,)).fetchone() == ("waiting", "")
with pytest.raises(psycopg.errors.CheckViolation): # a photo needs its picture
c.execute("insert into public.m2_ai_requests(kind, user_id, class_id) values ('photo', '11111111-1111-1111-1111-111111111111', %s)", (cid,))
with pytest.raises(psycopg.errors.CheckViolation): # a check needs its assignment
c.execute("insert into public.m2_ai_requests(kind, user_id, class_id) values ('check', '11111111-1111-1111-1111-111111111111', %s)", (cid,))
with pytest.raises(psycopg.errors.CheckViolation):
c.execute("insert into public.m2_ai_requests(kind, user_id, class_id, picture_id, state) values ('photo', '11111111-1111-1111-1111-111111111111', %s, %s, 'bogus')", (cid, pic))
c.execute("delete from public.m2_classes where id = %s", (cid,))
assert c.execute("select count(*) from public.m2_ai_requests where id = %s", (rid,)).fetchone() == (0,)
def test_the_allowance_is_taken_in_one_step_never_past_the_cap_and_can_be_given_back(db):
take = lambda n, cap: as_role(db, "service_role", "select public.m2_ai_take('class:9', '2026-11-09', %s, %s)", n, cap)[0][0]
assert take(3, 5) is True and take(3, 5) is False and take(2, 5) is True and take(1, 5) is False
as_role(db, "service_role", "select public.m2_ai_give_back('class:9', '2026-11-09', 2)")
assert take(2, 5) is True
as_role(db, "service_role", "select public.m2_ai_give_back('class:9', '2026-11-09', 99)")
assert as_role(db, "service_role", "select used from public.m2_ai_allowance where scope = 'class:9'") == [(0,)] # never below zero
def test_two_takes_at_once_never_pass_the_cap(db):
import threading
got = []
def go():
with psycopg.connect(db, autocommit=True) as c:
got.append(c.execute("select public.m2_ai_take('class:10', '2026-11-09', 1, 3)").fetchone()[0])
threads = [threading.Thread(target=go) for _ in range(12)]
[t.start() for t in threads]
[t.join() for t in threads]
assert got.count(True) == 3 and got.count(False) == 9
def test_the_server_role_keeps_ai_usage(db):
as_role(db, "service_role", "insert into public.m2_ai_usage(feature, model, input_tokens, output_tokens, cost_usd) values ('photo', 'claude-opus-5-5', 100, 50, 0.001)")
assert as_role(db, "service_role", "select count(*) from public.m2_ai_usage") == [(1,)]
with pytest.raises(psycopg.errors.CheckViolation):
as_role(db, "service_role", "insert into public.m2_ai_usage(feature, model) values ('other', 'x')")
# --- the review list ---
@pytest.mark.parametrize("role", ["authenticated", "anon"])
@pytest.mark.parametrize("table", ["m2_review", "m2_review_done"])
def test_browser_roles_cannot_touch_the_review_tables(db, role, table):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
def test_a_question_is_in_the_review_list_once_and_the_source_is_checked(db):
u = "22222222-2222-2222-2222-222222222222"
as_role(db, "service_role", "insert into public.m2_review(user_id, question_key, skill_id, source) values (%s, 's:k', 's', 'quiz') on conflict do nothing", u)
as_role(db, "service_role", "insert into public.m2_review(user_id, question_key, skill_id, source) values (%s, 's:k', 's', 'practice') on conflict do nothing", u)
assert as_role(db, "service_role", "select source from public.m2_review where user_id = %s", u) == [("quiz",)] # the first one stays
with pytest.raises(psycopg.errors.CheckViolation):
as_role(db, "service_role", "insert into public.m2_review(user_id, question_key, skill_id, source) values (%s, 's:j', 's', 'other')", u)
with pytest.raises(psycopg.errors.UniqueViolation):
as_role(db, "service_role", "insert into public.m2_review(user_id, question_key, skill_id, source) values (%s, 's:k', 's', 'quiz')", u)
def test_the_daily_goal_view_counts_answers_and_review_answers_together(db):
u = "33333333-3333-3333-3333-333333333333"
as_role(db, "service_role", "insert into public.m2_answers(user_id, skill_id, question_key, correct, counted, day) values (%s, 's', 's:a', true, true, '2026-11-09')", u)
as_role(db, "service_role", "insert into public.m2_answers(user_id, skill_id, question_key, correct, counted, day) values (%s, 's', 's:b', false, true, '2026-11-09')", u)
as_role(db, "service_role", "insert into public.m2_review_done(user_id, day, question_key) values (%s, '2026-11-09', 's:b')", u)
as_role(db, "service_role", "insert into public.m2_review_done(user_id, day, question_key) values (%s, '2026-11-10', 's:c')", u)
assert as_role(db, "service_role", "select day::text, correct from public.m2_daily_correct where user_id = %s order by day", u) == [("2026-11-09", 2), ("2026-11-10", 1)]
def test_the_activity_view_counts_each_person_once_a_day_on_israel_time_and_is_server_only(db):
with psycopg.connect(db, autocommit=True) as c:
c.execute("insert into public.m2_answers(user_id, skill_id, question_key, correct, day) values "
"('00000000-0000-0000-0000-0000000000a1', 's', 'k1', true, '2026-11-10'), "
"('00000000-0000-0000-0000-0000000000a1', 's', 'k2', false, '2026-11-10')")
c.execute("insert into public.m2_attempts(user_id, kind, scope, started_at, due_at, questions) values "
"('00000000-0000-0000-0000-0000000000a2', 'quiz', 't', '2026-11-10 22:30:00+00', '2026-11-10 23:15:00+00', '[]')")
rows = as_role(db, "service_role", "select day::text, user_id::text, answers, attempts from public.m2_activity "
"where user_id in ('00000000-0000-0000-0000-0000000000a1', '00000000-0000-0000-0000-0000000000a2') order by day, user_id")
assert rows == [("2026-11-10", "00000000-0000-0000-0000-0000000000a1", 2, 0),
("2026-11-11", "00000000-0000-0000-0000-0000000000a2", 0, 1)] # 00:30 in Israel is the next day
for role in ("anon", "authenticated"):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_activity")
def test_subjects_start_with_math_and_existing_classes_and_tests_read_as_math(db):
with psycopg.connect(db, autocommit=True) as c:
assert c.execute("select id, name from public.m2_subjects where id = 'math'").fetchone() == ("math", "מתמטיקה")
cid = _class(c, "נושאים1")
assert c.execute("select subjects from public.m2_classes where id = %s", (cid,)).fetchone() == (["math"],)
tid = c.execute("insert into public.m2_tests(class_id, date, lessons, created_by) values (%s, '2026-12-01', '{t}', 't@school.test') returning id",
(cid,)).fetchone()[0]
assert c.execute("select subject_id from public.m2_tests where id = %s", (tid,)).fetchone() == ("math",)
with pytest.raises(psycopg.errors.UniqueViolation):
c.execute("insert into public.m2_subjects(id, name) values ('math2', 'מתמטיקה')") # names are unique, ignoring case
c.execute("insert into public.m2_subjects(id, name) values ('science', 'מדעים')")
lid = c.execute("insert into public.m2_lessons(subject_id, draft, created_by) values ('science', '{\"name\": \"x\"}', 'm@school.test') returning id").fetchone()[0]
assert c.execute("select published, status from public.m2_lessons where id = %s", (lid,)).fetchone() == (None, "ready")
with pytest.raises(psycopg.errors.ForeignKeyViolation):
c.execute("insert into public.m2_lessons(subject_id, created_by) values ('nowhere', 'm@school.test')")
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_read_subjects_or_lessons(db, role):
for table in ("m2_subjects", "m2_lessons"):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, f"select * from public.{table}")
# --- the prompter (add-prompter) ---
def test_the_server_keeps_prompter_texts_and_worksheet_and_prompter_usage(db):
uid = "00000000-0000-0000-0000-0000000000aa"
as_role(db, "service_role", f"insert into public.m2_prompter_texts(user_id, title, body) values ('{uid}', 'שיעור', 'Slide 1 — פתיחה')")
assert as_role(db, "service_role", "select title, outline from public.m2_prompter_texts") == [("שיעור", None)]
with pytest.raises(psycopg.errors.CheckViolation):
as_role(db, "service_role", f"insert into public.m2_prompter_texts(user_id, title, body) values ('{uid}', '', 'x')")
for kind in ("worksheet", "prompter"): # the old check refused worksheet's records
as_role(db, "service_role", f"insert into public.m2_ai_usage(feature, model) values ('{kind}', 'claude-opus-5-5')")
@pytest.mark.parametrize("role", ["authenticated", "anon"])
def test_browser_roles_cannot_read_prompter_texts(db, role):
with pytest.raises(psycopg.errors.InsufficientPrivilege):
as_role(db, role, "select * from public.m2_prompter_texts")