""" 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}')")