trainUs/tests/test_step2b_schema_hardening.py
David 352a3d5e88
Some checks are pending
CI / Lint & Format (push) Waiting to run
CI / Build (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (chromium) (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (firefox) (push) Waiting to run
Initial commit
2026-06-18 13:06:57 +02:00

412 lines
12 KiB
Python

"""
Step 2b — Schema Hardening Tests
- test_suffix_table_exists: The suffix table is created in the schema
- test_exercise_title_unique: Duplicate exercise titles are rejected
- test_combo_title_unique: Duplicate combo titles are rejected
- test_session_title_unique: Duplicate session titles are rejected
- test_collection_label_unique: Duplicate collection labels are rejected
- test_suffix_crud: Full CRUD cycle on the suffix table
- test_suffix_unique_constraint: Duplicate suffixes are rejected
- test_suffix_user_uuid_primary_key: Duplicate user_uuid is rejected
- test_unique_constraint_allows_update: Updating a record to its own title succeeds
- test_unique_constraint_across_updates: Updating a title to an existing title fails
"""
from playwright.sync_api import Page, expect
def _wait_for_db(page: Page, app_url: str):
"""Navigate and wait for the database to be ready."""
page.goto(app_url)
page.wait_for_function(
"document.documentElement.getAttribute('data-db-ready') === 'true'", timeout=15000
)
def test_suffix_table_exists(page: Page, app_url: str):
"""The suffix table is created in the schema."""
_wait_for_db(page, app_url)
tables = page.evaluate(
"""
window.__db.selectAll(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
"""
)
table_names = [t["name"] for t in tables]
assert "suffix" in table_names, f"Table 'suffix' not found. Found: {table_names}"
# Verify table structure
columns = page.evaluate(
"""
window.__db.selectAll("PRAGMA table_info(suffix)")
"""
)
col_names = [c["name"] for c in columns]
assert "user_uuid" in col_names, "Column 'user_uuid' not found in suffix table"
assert "user_name" in col_names, "Column 'user_name' not found in suffix table"
assert "suffix" in col_names, "Column 'suffix' not found in suffix table"
def test_exercise_title_unique(page: Page, app_url: str):
"""Duplicate exercise titles are rejected by the UNIQUE constraint."""
_wait_for_db(page, app_url)
# Create first exercise
ex_id = page.evaluate(
"""
window.__repos.exercises.create({
title: 'Unique Push-ups',
description: '',
created_by: 'test'
})
"""
)
assert ex_id is not None
# Try to create a duplicate
error = page.evaluate(
"""
(async () => {
try {
await window.__repos.exercises.create({
title: 'Unique Push-ups',
description: 'Duplicate',
created_by: 'test'
});
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate exercise title was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate(f"window.__repos.exercises.delete('{ex_id}')")
def test_combo_title_unique(page: Page, app_url: str):
"""Duplicate combo titles are rejected by the UNIQUE constraint."""
_wait_for_db(page, app_url)
combo_id = page.evaluate(
"""
window.__repos.combos.create({
title: 'Unique Combo',
description: '',
type: 'NONE',
created_by: 'test'
})
"""
)
assert combo_id is not None
error = page.evaluate(
"""
(async () => {
try {
await window.__repos.combos.create({
title: 'Unique Combo',
description: 'Duplicate',
type: 'NONE',
created_by: 'test'
});
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate combo title was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate(f"window.__repos.combos.delete('{combo_id}')")
def test_session_title_unique(page: Page, app_url: str):
"""Duplicate session titles are rejected by the UNIQUE constraint."""
_wait_for_db(page, app_url)
session_id = page.evaluate(
"""
window.__repos.sessions.create({
title: 'Unique Session',
description: '',
created_by: 'test'
})
"""
)
assert session_id is not None
error = page.evaluate(
"""
(async () => {
try {
await window.__repos.sessions.create({
title: 'Unique Session',
description: 'Duplicate',
created_by: 'test'
});
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate session title was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate(f"window.__repos.sessions.delete('{session_id}')")
def test_collection_label_unique(page: Page, app_url: str):
"""Duplicate collection labels are rejected by the UNIQUE constraint."""
_wait_for_db(page, app_url)
coll_id = page.evaluate(
"""
window.__repos.collections.create({
label: 'Unique Collection',
created_by: 'test'
})
"""
)
assert coll_id is not None
error = page.evaluate(
"""
(async () => {
try {
await window.__repos.collections.create({
label: 'Unique Collection',
created_by: 'test'
});
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate collection label was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate(f"window.__repos.collections.delete('{coll_id}')")
def test_suffix_crud(page: Page, app_url: str):
"""Full CRUD cycle on the suffix table."""
_wait_for_db(page, app_url)
# Insert
page.evaluate(
"""
window.__db.run(
"INSERT INTO suffix (user_uuid, user_name, suffix) VALUES (?, ?, ?)",
['uuid-001', 'Alice', 'AL']
)
"""
)
# Read
row = page.evaluate(
"""
window.__db.selectAll("SELECT * FROM suffix WHERE user_uuid = 'uuid-001'")
"""
)
assert len(row) == 1, f"Expected 1 row, got {len(row)}"
assert row[0]["user_uuid"] == "uuid-001"
assert row[0]["user_name"] == "Alice"
assert row[0]["suffix"] == "AL"
# Update
page.evaluate(
"""
window.__db.run(
"UPDATE suffix SET suffix = ?, user_name = ? WHERE user_uuid = ?",
['ALC', 'Alice C.', 'uuid-001']
)
"""
)
updated = page.evaluate(
"""
window.__db.selectAll("SELECT * FROM suffix WHERE user_uuid = 'uuid-001'")
"""
)
assert updated[0]["suffix"] == "ALC"
assert updated[0]["user_name"] == "Alice C."
# Delete
page.evaluate(
"""
window.__db.run("DELETE FROM suffix WHERE user_uuid = 'uuid-001'")
"""
)
deleted = page.evaluate(
"""
window.__db.selectAll("SELECT * FROM suffix WHERE user_uuid = 'uuid-001'")
"""
)
assert len(deleted) == 0, "Suffix row still exists after deletion"
def test_suffix_unique_constraint(page: Page, app_url: str):
"""Duplicate suffix values are rejected by the UNIQUE constraint."""
_wait_for_db(page, app_url)
# Insert first
page.evaluate(
"""
window.__db.run(
"INSERT INTO suffix (user_uuid, user_name, suffix) VALUES (?, ?, ?)",
['uuid-100', 'Bob', 'BB']
)
"""
)
# Try duplicate suffix with different user
error = page.evaluate(
"""
(async () => {
try {
await window.__db.run(
"INSERT INTO suffix (user_uuid, user_name, suffix) VALUES (?, ?, ?)",
['uuid-200', 'Charlie', 'BB']
);
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate suffix was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate("""window.__db.run("DELETE FROM suffix WHERE user_uuid = 'uuid-100'")""")
def test_suffix_user_uuid_primary_key(page: Page, app_url: str):
"""Duplicate user_uuid (primary key) is rejected."""
_wait_for_db(page, app_url)
page.evaluate(
"""
window.__db.run(
"INSERT INTO suffix (user_uuid, user_name, suffix) VALUES (?, ?, ?)",
['uuid-pk-test', 'Dave', 'DV']
)
"""
)
error = page.evaluate(
"""
(async () => {
try {
await window.__db.run(
"INSERT INTO suffix (user_uuid, user_name, suffix) VALUES (?, ?, ?)",
['uuid-pk-test', 'Dave Again', 'DV2']
);
return null;
} catch (e) {
return e.message;
}
})()
"""
)
assert error is not None, "Duplicate user_uuid was allowed"
# Cleanup
page.evaluate("""window.__db.run("DELETE FROM suffix WHERE user_uuid = 'uuid-pk-test'")""")
def test_unique_constraint_allows_update(page: Page, app_url: str):
"""Updating a record to its own title succeeds (UNIQUE constraint does not block self-update)."""
_wait_for_db(page, app_url)
ex_id = page.evaluate(
"""
window.__repos.exercises.create({
title: 'Self Update Test',
description: '',
created_by: 'test'
})
"""
)
# Update to same title should work
error = page.evaluate(
f"""
(async () => {{
try {{
await window.__repos.exercises.update('{ex_id}', {{
title: 'Self Update Test',
description: 'Updated description'
}});
return null;
}} catch (e) {{
return e.message;
}}
}})()
"""
)
assert error is None, f"Self-update was rejected: {error}"
# Cleanup
page.evaluate(f"window.__repos.exercises.delete('{ex_id}')")
def test_unique_constraint_across_updates(page: Page, app_url: str):
"""Updating a title to match another existing title fails."""
_wait_for_db(page, app_url)
id_a = page.evaluate(
"""
window.__repos.exercises.create({
title: 'Exercise A',
description: '',
created_by: 'test'
})
"""
)
id_b = page.evaluate(
"""
window.__repos.exercises.create({
title: 'Exercise B',
description: '',
created_by: 'test'
})
"""
)
# Try to update B's title to A's title
error = page.evaluate(
f"""
(async () => {{
try {{
await window.__repos.exercises.update('{id_b}', {{
title: 'Exercise A'
}});
return null;
}} catch (e) {{
return e.message;
}}
}})()
"""
)
assert error is not None, "Updating to a duplicate title was allowed"
assert "UNIQUE" in error or "unique" in error.lower(), f"Unexpected error: {error}"
# Cleanup
page.evaluate(f"window.__repos.exercises.delete('{id_a}')")
page.evaluate(f"window.__repos.exercises.delete('{id_b}')")