diff --git a/tests/test_migrations.py b/tests/test_migrations.py index c04f3e3..50b1f8b 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -49,19 +49,54 @@ def temp_migrations_dir(): @pytest.fixture def fresh_db_with_schema(temp_db): - """Create a fresh database with current schema (includes code_verifier)""" + """Create a fresh database with current schema (no code_verifier after migration 003)""" conn = sqlite3.connect(temp_db) try: - # Create auth_state table with code_verifier (current schema) + # Create auth_state table WITHOUT code_verifier (current schema after Phase 1) conn.execute(""" CREATE TABLE auth_state ( state TEXT PRIMARY KEY, - code_verifier TEXT NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, redirect_uri TEXT ) """) + # Also need other tables to make schema truly current + conn.execute(""" + CREATE TABLE tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token_hash TEXT UNIQUE NOT NULL, + me TEXT NOT NULL, + client_id TEXT, + scope TEXT DEFAULT 'create', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + last_used_at TIMESTAMP, + revoked_at TIMESTAMP + ) + """) + conn.execute(""" + CREATE TABLE authorization_codes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code_hash TEXT UNIQUE NOT NULL, + me TEXT NOT NULL, + client_id TEXT NOT NULL, + redirect_uri TEXT NOT NULL, + scope TEXT, + state TEXT, + code_challenge TEXT, + code_challenge_method TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + used_at TIMESTAMP + ) + """) + # Add required indexes + conn.execute("CREATE INDEX idx_tokens_hash ON tokens(token_hash)") + conn.execute("CREATE INDEX idx_tokens_me ON tokens(me)") + conn.execute("CREATE INDEX idx_tokens_expires ON tokens(expires_at)") + conn.execute("CREATE INDEX idx_auth_codes_hash ON authorization_codes(code_hash)") + conn.execute("CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at)") conn.commit() finally: conn.close() @@ -69,11 +104,11 @@ def fresh_db_with_schema(temp_db): @pytest.fixture -def legacy_db_without_code_verifier(temp_db): - """Create a legacy database without code_verifier column""" +def legacy_db_basic(temp_db): + """Create a basic database with auth_state table""" conn = sqlite3.connect(temp_db) try: - # Create auth_state table WITHOUT code_verifier (legacy schema) + # Create auth_state table WITHOUT code_verifier (current schema) conn.execute(""" CREATE TABLE auth_state ( state TEXT PRIMARY KEY, @@ -132,17 +167,18 @@ class TestSchemaDetection: """Tests for fresh database detection""" def test_is_schema_current_with_code_verifier(self, fresh_db_with_schema): - """Test detecting current schema (has code_verifier)""" + """Test detecting current schema (no code_verifier after Phase 1)""" conn = sqlite3.connect(fresh_db_with_schema) try: assert is_schema_current(conn) is True finally: conn.close() - def test_is_schema_current_without_code_verifier(self, legacy_db_without_code_verifier): - """Test detecting legacy schema (no code_verifier)""" - conn = sqlite3.connect(legacy_db_without_code_verifier) + def test_is_schema_current_without_code_verifier(self, legacy_db_basic): + """Test detecting incomplete schema (missing tokens/authorization_codes tables)""" + conn = sqlite3.connect(legacy_db_basic) try: + # Should be False because missing tokens and authorization_codes tables assert is_schema_current(conn) is False finally: conn.close() @@ -179,15 +215,17 @@ class TestHelperFunctions: """Test detecting existing column""" conn = sqlite3.connect(fresh_db_with_schema) try: - assert column_exists(conn, 'auth_state', 'code_verifier') is True + # Test with a column that actually exists in current schema + assert column_exists(conn, 'auth_state', 'state') is True finally: conn.close() - def test_column_exists_false(self, legacy_db_without_code_verifier): + def test_column_exists_false(self, legacy_db_basic): """Test detecting non-existent column""" - conn = sqlite3.connect(legacy_db_without_code_verifier) + conn = sqlite3.connect(legacy_db_basic) try: - assert column_exists(conn, 'auth_state', 'code_verifier') is False + # Test with a column that doesn't exist + assert column_exists(conn, 'auth_state', 'nonexistent_column') is False finally: conn.close() @@ -375,48 +413,55 @@ class TestRunMigrations: assert migration_count == 0 assert is_schema_current(conn) is True - # Manually mark migration as applied (simulating fresh DB detection) + # Manually mark migrations as applied (simulating fresh DB detection) conn.execute( "INSERT INTO schema_migrations (migration_name) VALUES (?)", ("001_add_code_verifier_to_auth_state.sql",) ) + conn.execute( + "INSERT INTO schema_migrations (migration_name) VALUES (?)", + ("003_remove_code_verifier_from_auth_state.sql",) + ) conn.commit() - # Verify migration was marked but NOT executed + # Verify migrations were marked but NOT executed applied = get_applied_migrations(conn) assert "001_add_code_verifier_to_auth_state.sql" in applied + assert "003_remove_code_verifier_from_auth_state.sql" in applied - # Table should still have only one code_verifier column (not duplicated) + # Table should NOT have code_verifier column (current schema after Phase 1) cursor = conn.execute("PRAGMA table_info(auth_state)") columns = [row[1] for row in cursor.fetchall()] - assert columns.count('code_verifier') == 1 + assert 'code_verifier' not in columns + assert 'state' in columns + assert 'expires_at' in columns finally: conn.close() - def test_run_migrations_legacy_database(self, legacy_db_without_code_verifier, temp_migrations_dir): + def test_run_migrations_legacy_database(self, legacy_db_basic, temp_migrations_dir): """Test legacy database scenario - migration should execute""" - # Create the migration to add code_verifier - migration_file = temp_migrations_dir / "001_add_code_verifier_to_auth_state.sql" + # Create a migration to add a test column + migration_file = temp_migrations_dir / "001_add_test_column.sql" migration_file.write_text( - "ALTER TABLE auth_state ADD COLUMN code_verifier TEXT NOT NULL DEFAULT '';" + "ALTER TABLE auth_state ADD COLUMN test_column TEXT;" ) - conn = sqlite3.connect(legacy_db_without_code_verifier) + conn = sqlite3.connect(legacy_db_basic) try: create_migrations_table(conn) - # Verify code_verifier doesn't exist yet - assert column_exists(conn, 'auth_state', 'code_verifier') is False + # Verify test_column doesn't exist yet + assert column_exists(conn, 'auth_state', 'test_column') is False # Apply migration - apply_migration(conn, "001_add_code_verifier_to_auth_state.sql", migration_file) + apply_migration(conn, "001_add_test_column.sql", migration_file) - # Verify code_verifier was added - assert column_exists(conn, 'auth_state', 'code_verifier') is True + # Verify test_column was added + assert column_exists(conn, 'auth_state', 'test_column') is True # Verify migration was recorded applied = get_applied_migrations(conn) - assert "001_add_code_verifier_to_auth_state.sql" in applied + assert "001_add_test_column.sql" in applied finally: conn.close() @@ -525,36 +570,52 @@ class TestRunMigrations: class TestRealMigration: - """Test with actual migration file from the project""" + """Test with actual migration files from the project""" - def test_actual_migration_001(self, legacy_db_without_code_verifier): - """Test the actual 001 migration file""" + def test_actual_migration_003(self, temp_db): + """Test the actual 003 migration file (remove code_verifier)""" # Get the actual migration file project_root = Path(__file__).parent.parent - migration_file = project_root / "migrations" / "001_add_code_verifier_to_auth_state.sql" + migration_file = project_root / "migrations" / "003_remove_code_verifier_from_auth_state.sql" if not migration_file.exists(): - pytest.skip("Migration file 001_add_code_verifier_to_auth_state.sql not found") + pytest.skip("Migration file 003_remove_code_verifier_from_auth_state.sql not found") - conn = sqlite3.connect(legacy_db_without_code_verifier) + conn = sqlite3.connect(temp_db) try: create_migrations_table(conn) - # Verify starting state - assert not column_exists(conn, 'auth_state', 'code_verifier') + # Create auth_state table WITH code_verifier (pre-migration state) + conn.execute(""" + CREATE TABLE auth_state ( + state TEXT PRIMARY KEY, + code_verifier TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + redirect_uri TEXT + ) + """) + conn.execute("CREATE INDEX idx_auth_state_expires ON auth_state(expires_at)") + conn.commit() + + # Verify starting state (has code_verifier) + assert column_exists(conn, 'auth_state', 'code_verifier') is True # Apply migration apply_migration( conn, - "001_add_code_verifier_to_auth_state.sql", + "003_remove_code_verifier_from_auth_state.sql", migration_file ) - # Verify end state - assert column_exists(conn, 'auth_state', 'code_verifier') + # Verify end state (no code_verifier) + assert column_exists(conn, 'auth_state', 'code_verifier') is False + # Other columns should still exist + assert column_exists(conn, 'auth_state', 'state') is True + assert column_exists(conn, 'auth_state', 'redirect_uri') is True # Verify migration recorded applied = get_applied_migrations(conn) - assert "001_add_code_verifier_to_auth_state.sql" in applied + assert "003_remove_code_verifier_from_auth_state.sql" in applied finally: conn.close() diff --git a/tests/test_routes_dev_auth.py b/tests/test_routes_dev_auth.py index 5ce769c..9bc2bee 100644 --- a/tests/test_routes_dev_auth.py +++ b/tests/test_routes_dev_auth.py @@ -167,7 +167,7 @@ class TestConfigurationValidation: "SESSION_SECRET": "test-secret", "SITE_URL": "http://localhost:5000", "DEV_MODE": True, - # Missing DEV_ADMIN_ME + "DEV_ADMIN_ME": "", # Explicitly set to empty string } with pytest.raises(ValueError, match="DEV_ADMIN_ME"):