Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

645 lines
22 KiB
Python

"""
Tests for SQLCipher backwards compatibility.
These tests verify that databases can be created, closed, and reopened
correctly across multiple sessions, ensuring data persistence and
settings consistency.
"""
import shutil
import tempfile
from pathlib import Path
import pytest
from src.local_deep_research.database.sqlcipher_utils import (
apply_cipher_defaults_before_key,
apply_performance_pragmas,
apply_sqlcipher_pragmas,
set_sqlcipher_key,
)
from src.local_deep_research.database.sqlcipher_compat import (
get_sqlcipher_module,
)
@pytest.fixture
def temp_db_path():
"""Create a temporary database path."""
temp_dir = tempfile.mkdtemp()
db_path = Path(temp_dir) / "test_compat.db"
yield db_path
shutil.rmtree(temp_dir)
@pytest.fixture
def sqlcipher_module():
"""Get the SQLCipher module."""
return get_sqlcipher_module()
def create_configured_connection(sqlcipher_module, db_path, password):
"""Helper to create a properly configured connection.
Uses the correct PRAGMA ordering:
- New DB: cipher_default_* -> key -> kdf_iter -> performance
- Existing DB: key -> cipher_* + kdf_iter -> performance
"""
creation_mode = not db_path.exists()
conn = sqlcipher_module.connect(str(db_path))
cursor = conn.cursor()
if creation_mode:
apply_cipher_defaults_before_key(cursor)
set_sqlcipher_key(cursor, password)
apply_sqlcipher_pragmas(cursor, creation_mode=creation_mode)
apply_performance_pragmas(cursor)
cursor.close()
return conn
class TestCreateCloseReopen:
"""Tests for create -> close -> reopen cycles."""
def test_create_and_reopen_same_session(
self, sqlcipher_module, temp_db_path
):
"""Verify database can be created and reopened within same test."""
password = "test_password_123"
# Create database
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)")
conn1.execute("INSERT INTO test VALUES (1, 'test_data')")
conn1.commit()
conn1.close()
# Reopen database
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
result = conn2.execute("SELECT data FROM test WHERE id = 1").fetchone()
assert result[0] == "test_data", (
"Data should persist after close/reopen"
)
conn2.close()
def test_create_reopen_multiple_times(self, sqlcipher_module, temp_db_path):
"""Verify database can be opened and closed multiple times."""
password = "test_password_456"
# Create database
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute(
"CREATE TABLE counter (id INTEGER PRIMARY KEY, count INTEGER)"
)
conn.execute("INSERT INTO counter VALUES (1, 0)")
conn.commit()
conn.close()
# Open and close 5 times, incrementing counter each time
for i in range(5):
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute("UPDATE counter SET count = count + 1 WHERE id = 1")
conn.commit()
conn.close()
# Final open to verify counter
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
result = conn.execute(
"SELECT count FROM counter WHERE id = 1"
).fetchone()
assert result[0] == 5, (
f"Counter should be 5 after 5 increments, got {result[0]}"
)
conn.close()
def test_data_persists_across_sessions(
self, sqlcipher_module, temp_db_path
):
"""Verify complex data persists correctly across sessions."""
password = "persistence_test"
# Session 1: Create database with multiple tables and data
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE,
email TEXT
)
""")
conn1.execute("""
CREATE TABLE settings (
user_id INTEGER,
key TEXT,
value TEXT,
FOREIGN KEY(user_id) REFERENCES users(id)
)
""")
# Insert test data
conn1.execute(
"INSERT INTO users VALUES (1, 'alice', 'alice@example.com')"
)
conn1.execute("INSERT INTO users VALUES (2, 'bob', 'bob@example.com')")
conn1.execute("INSERT INTO settings VALUES (1, 'theme', 'dark')")
conn1.execute("INSERT INTO settings VALUES (1, 'language', 'en')")
conn1.execute("INSERT INTO settings VALUES (2, 'theme', 'light')")
conn1.commit()
conn1.close()
# Session 2: Verify all data persists
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
# Check users
users = conn2.execute(
"SELECT username, email FROM users ORDER BY id"
).fetchall()
assert users == [
("alice", "alice@example.com"),
("bob", "bob@example.com"),
]
# Check settings
alice_settings = conn2.execute(
"SELECT key, value FROM settings WHERE user_id = 1 ORDER BY key"
).fetchall()
assert alice_settings == [("language", "en"), ("theme", "dark")]
bob_settings = conn2.execute(
"SELECT key, value FROM settings WHERE user_id = 2"
).fetchall()
assert bob_settings == [("theme", "light")]
conn2.close()
def test_settings_match_after_reopen(self, sqlcipher_module, temp_db_path):
"""Verify SQLCipher settings remain consistent after reopen."""
password = "settings_test"
# Create database
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE test (id INTEGER)")
conn1.commit()
# Capture settings from first connection
original_page_size = conn1.execute(
"PRAGMA cipher_page_size"
).fetchone()[0]
original_hmac = conn1.execute(
"PRAGMA cipher_hmac_algorithm"
).fetchone()[0]
original_kdf = conn1.execute("PRAGMA cipher_kdf_algorithm").fetchone()[
0
]
conn1.close()
# Reopen and verify settings
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
new_page_size = conn2.execute("PRAGMA cipher_page_size").fetchone()[0]
new_hmac = conn2.execute("PRAGMA cipher_hmac_algorithm").fetchone()[0]
new_kdf = conn2.execute("PRAGMA cipher_kdf_algorithm").fetchone()[0]
assert str(new_page_size) == str(original_page_size), (
f"Page size changed: {original_page_size} -> {new_page_size}"
)
assert new_hmac == original_hmac, (
f"HMAC algorithm changed: {original_hmac} -> {new_hmac}"
)
assert new_kdf == original_kdf, (
f"KDF algorithm changed: {original_kdf} -> {new_kdf}"
)
conn2.close()
class TestDatabaseMigration:
"""Tests for database schema changes across sessions."""
def test_schema_migration(self, sqlcipher_module, temp_db_path):
"""Verify schema can be modified across sessions."""
password = "migration_test"
# Session 1: Create initial schema
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE data (id INTEGER PRIMARY KEY, value TEXT)")
conn1.execute("INSERT INTO data VALUES (1, 'initial')")
conn1.commit()
conn1.close()
# Session 2: Modify schema (add column)
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn2.execute("ALTER TABLE data ADD COLUMN created_at TEXT")
conn2.execute("UPDATE data SET created_at = '2024-01-01' WHERE id = 1")
conn2.commit()
conn2.close()
# Session 3: Verify modified schema
conn3 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
result = conn3.execute(
"SELECT value, created_at FROM data WHERE id = 1"
).fetchone()
assert result == ("initial", "2024-01-01")
conn3.close()
def test_create_index_persists(self, sqlcipher_module, temp_db_path):
"""Verify indexes persist across sessions."""
password = "index_test"
# Session 1: Create table and index
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
conn1.execute("CREATE INDEX idx_email ON users(email)")
conn1.commit()
conn1.close()
# Session 2: Verify index exists
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
indexes = conn2.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='users'"
).fetchall()
index_names = [idx[0] for idx in indexes]
assert "idx_email" in index_names, (
f"Index not found. Indexes: {index_names}"
)
conn2.close()
class TestLargeDataPersistence:
"""Tests for large data persistence."""
def test_large_blob_persists(self, sqlcipher_module, temp_db_path):
"""Verify large binary data persists correctly."""
password = "blob_test"
import os
# Create 1MB of random data
large_data = os.urandom(1024 * 1024)
# Session 1: Store large blob
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)")
conn1.execute("INSERT INTO blobs VALUES (1, ?)", (large_data,))
conn1.commit()
conn1.close()
# Session 2: Retrieve and verify
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
result = conn2.execute("SELECT data FROM blobs WHERE id = 1").fetchone()
assert result[0] == large_data, "Large blob data corrupted or changed"
conn2.close()
def test_many_rows_persist(self, sqlcipher_module, temp_db_path):
"""Verify many rows persist correctly."""
password = "many_rows_test"
# Session 1: Insert 1000 rows
conn1 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn1.execute(
"CREATE TABLE numbers (id INTEGER PRIMARY KEY, value INTEGER)"
)
for i in range(1000):
conn1.execute("INSERT INTO numbers VALUES (?, ?)", (i, i * 2))
conn1.commit()
conn1.close()
# Session 2: Verify all rows
conn2 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
count = conn2.execute("SELECT COUNT(*) FROM numbers").fetchone()[0]
assert count == 1000, f"Expected 1000 rows, got {count}"
# Verify some specific values
for test_id in [0, 499, 999]:
result = conn2.execute(
"SELECT value FROM numbers WHERE id = ?", (test_id,)
).fetchone()
assert result[0] == test_id * 2, (
f"Row {test_id} has wrong value: {result[0]} != {test_id * 2}"
)
conn2.close()
class TestOldToNewPragmaOrderMigration:
"""
Tests for backwards compatibility when migrating from OLD pragma order
(key first, then cipher settings) to NEW pragma order (cipher_default_*
before key for creation, cipher_* after key for existing).
"""
def _create_connection_old_order(self, sqlcipher_module, db_path, password):
"""
Create a connection using the OLD pragma order (pre-PR behavior).
OLD order: key -> cipher_page_size -> cipher_hmac_algorithm -> kdf_iter
This simulates how databases were created before this PR.
"""
from src.local_deep_research.database.sqlcipher_utils import (
get_sqlcipher_settings,
set_sqlcipher_key,
)
conn = sqlcipher_module.connect(str(db_path))
cursor = conn.cursor()
# OLD ORDER: Set key FIRST
set_sqlcipher_key(cursor, password)
# OLD ORDER: Then set cipher settings AFTER the key
settings = get_sqlcipher_settings()
cursor.execute(f"PRAGMA cipher_page_size = {settings['page_size']}")
cursor.execute(
f"PRAGMA cipher_hmac_algorithm = {settings['hmac_algorithm']}"
)
cursor.execute(f"PRAGMA kdf_iter = {settings['kdf_iterations']}")
cursor.close()
return conn
def _create_connection_new_order(self, sqlcipher_module, db_path, password):
"""
Create a connection using the NEW pragma order (post-PR behavior).
"""
return create_configured_connection(sqlcipher_module, db_path, password)
def test_old_db_opens_with_new_code(self, sqlcipher_module, temp_db_path):
"""
CRITICAL: Verify database created with OLD pragma order can be
opened with NEW pragma order.
"""
password = "old_to_new_migration"
# Create database using OLD pragma order (simulating pre-PR code)
conn1 = self._create_connection_old_order(
sqlcipher_module, temp_db_path, password
)
conn1.execute(
"CREATE TABLE migration_test (id INTEGER PRIMARY KEY, data TEXT)"
)
conn1.execute(
"INSERT INTO migration_test VALUES (1, 'created_with_old_code')"
)
conn1.execute("INSERT INTO migration_test VALUES (2, 'should_persist')")
conn1.commit()
conn1.close()
# Reopen using NEW pragma order (simulating post-PR code)
conn2 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
# Verify data is accessible
result = conn2.execute(
"SELECT data FROM migration_test WHERE id = 1"
).fetchone()
assert result[0] == "created_with_old_code", (
"Data created with old pragma order should be readable with new order"
)
# Verify all rows
all_rows = conn2.execute(
"SELECT id, data FROM migration_test ORDER BY id"
).fetchall()
assert len(all_rows) == 2, f"Expected 2 rows, got {len(all_rows)}"
assert all_rows[0] == (1, "created_with_old_code")
assert all_rows[1] == (2, "should_persist")
conn2.close()
def test_old_db_can_be_modified_with_new_code(
self, sqlcipher_module, temp_db_path
):
"""
Verify database created with OLD pragma order can be modified
after opening with NEW pragma order.
"""
password = "old_to_new_modify"
# Create database using OLD pragma order
conn1 = self._create_connection_old_order(
sqlcipher_module, temp_db_path, password
)
conn1.execute(
"CREATE TABLE data (id INTEGER PRIMARY KEY, value INTEGER)"
)
conn1.execute("INSERT INTO data VALUES (1, 100)")
conn1.commit()
conn1.close()
# Open with NEW pragma order and modify
conn2 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
conn2.execute("UPDATE data SET value = 200 WHERE id = 1")
conn2.execute("INSERT INTO data VALUES (2, 300)")
conn2.commit()
conn2.close()
# Reopen with NEW pragma order and verify changes persisted
conn3 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
results = conn3.execute(
"SELECT id, value FROM data ORDER BY id"
).fetchall()
assert results == [(1, 200), (2, 300)], (
f"Modifications should persist. Got: {results}"
)
conn3.close()
def test_old_db_schema_migration_with_new_code(
self, sqlcipher_module, temp_db_path
):
"""
Verify schema changes work on databases created with OLD pragma order
when opened with NEW pragma order.
"""
password = "old_to_new_schema"
# Create database using OLD pragma order
conn1 = self._create_connection_old_order(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
conn1.execute("INSERT INTO users VALUES (1, 'Alice')")
conn1.commit()
conn1.close()
# Open with NEW pragma order and modify schema
conn2 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
conn2.execute("ALTER TABLE users ADD COLUMN email TEXT")
conn2.execute(
"UPDATE users SET email = 'alice@example.com' WHERE id = 1"
)
conn2.execute("CREATE INDEX idx_users_email ON users(email)")
conn2.commit()
conn2.close()
# Verify schema changes persisted
conn3 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
# Check data
result = conn3.execute(
"SELECT name, email FROM users WHERE id = 1"
).fetchone()
assert result == ("Alice", "alice@example.com")
# Check index exists
indexes = conn3.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='users'"
).fetchall()
index_names = [idx[0] for idx in indexes]
assert "idx_users_email" in index_names, (
f"Index should persist. Found indexes: {index_names}"
)
conn3.close()
def test_settings_consistent_across_migration(
self, sqlcipher_module, temp_db_path
):
"""
Verify SQLCipher settings remain consistent when migrating from
OLD to NEW pragma order.
"""
password = "settings_migration"
# Create database using OLD pragma order
conn1 = self._create_connection_old_order(
sqlcipher_module, temp_db_path, password
)
conn1.execute("CREATE TABLE test (id INTEGER)")
conn1.commit()
# Capture settings from old-order connection
old_page_size = conn1.execute("PRAGMA cipher_page_size").fetchone()[0]
old_hmac = conn1.execute("PRAGMA cipher_hmac_algorithm").fetchone()[0]
conn1.close()
# Open with NEW pragma order
conn2 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, password
)
# Verify settings match
new_page_size = conn2.execute("PRAGMA cipher_page_size").fetchone()[0]
new_hmac = conn2.execute("PRAGMA cipher_hmac_algorithm").fetchone()[0]
assert str(new_page_size) == str(old_page_size), (
f"Page size mismatch after migration: {old_page_size} -> {new_page_size}"
)
assert new_hmac == old_hmac, (
f"HMAC algorithm mismatch after migration: {old_hmac} -> {new_hmac}"
)
conn2.close()
def test_rekey_on_old_db_with_new_code(
self, sqlcipher_module, temp_db_path
):
"""
Verify password change (rekey) works on databases created with
OLD pragma order when using NEW pragma order code.
"""
from src.local_deep_research.database.sqlcipher_utils import (
set_sqlcipher_rekey,
)
old_password = "original_password"
new_password = "changed_password"
# Create database using OLD pragma order
conn1 = self._create_connection_old_order(
sqlcipher_module, temp_db_path, old_password
)
conn1.execute(
"CREATE TABLE secrets (id INTEGER PRIMARY KEY, data TEXT)"
)
conn1.execute("INSERT INTO secrets VALUES (1, 'sensitive_data')")
conn1.commit()
conn1.close()
# Open with NEW pragma order and change password
conn2 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, old_password
)
# Verify data is accessible before rekey
result = conn2.execute(
"SELECT data FROM secrets WHERE id = 1"
).fetchone()
assert result[0] == "sensitive_data"
# Change the password
set_sqlcipher_rekey(conn2, new_password)
conn2.commit()
conn2.close()
# Verify old password no longer works
try:
conn_fail = self._create_connection_new_order(
sqlcipher_module, temp_db_path, old_password
)
# If we get here, try to access data (should fail)
conn_fail.execute("SELECT * FROM secrets").fetchone()
conn_fail.close()
raise AssertionError("Old password should not work after rekey")
except Exception as e:
if isinstance(e, AssertionError):
raise
# Any other exception is expected (file is not a database, etc.)
# Verify new password works
conn3 = self._create_connection_new_order(
sqlcipher_module, temp_db_path, new_password
)
result = conn3.execute(
"SELECT data FROM secrets WHERE id = 1"
).fetchone()
assert result[0] == "sensitive_data", (
"Data should be accessible with new password after rekey"
)
conn3.close()