feat(auth): oauth_identities table + password_hash nullable (#88)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from src.auth import db
|
||||
|
||||
|
||||
def test_oauth_identities_table_created(users_db_path):
|
||||
db.init_schema()
|
||||
tables = {
|
||||
r[0]
|
||||
for r in db.get_conn().execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
}
|
||||
assert "oauth_identities" in tables
|
||||
|
||||
|
||||
def test_password_hash_is_nullable_on_fresh_schema(users_db_path):
|
||||
db.init_schema()
|
||||
cols = {r["name"]: r for r in db.get_conn().execute("PRAGMA table_info(users)")}
|
||||
assert cols["password_hash"]["notnull"] == 0
|
||||
|
||||
|
||||
def test_create_oauth_user(users_db_path):
|
||||
db.init_schema()
|
||||
uid = db.create_oauth_user("oauth@example.com")
|
||||
row = db.get_user_by_id(uid)
|
||||
assert row["email"] == "oauth@example.com"
|
||||
assert row["password_hash"] is None
|
||||
assert row["email_verified"] == 1
|
||||
|
||||
|
||||
def test_link_and_get_oauth_identity(users_db_path):
|
||||
db.init_schema()
|
||||
uid = db.create_oauth_user("oauth@example.com")
|
||||
assert db.get_oauth_identity("linkedin", "sub-123") is None
|
||||
db.link_oauth_identity("linkedin", "sub-123", uid)
|
||||
row = db.get_oauth_identity("linkedin", "sub-123")
|
||||
assert row["user_id"] == uid
|
||||
|
||||
|
||||
def test_oauth_identity_pk_prevents_duplicate(users_db_path):
|
||||
db.init_schema()
|
||||
uid = db.create_oauth_user("oauth@example.com")
|
||||
db.link_oauth_identity("linkedin", "sub-123", uid)
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
db.link_oauth_identity("linkedin", "sub-123", uid)
|
||||
|
||||
|
||||
def test_migration_makes_legacy_password_hash_nullable(users_db_path):
|
||||
# Simule un schéma hérité où password_hash est NOT NULL.
|
||||
conn = db.get_conn()
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
email_verified INTEGER NOT NULL DEFAULT 0,
|
||||
pending_email TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE email_verification_tokens (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
"""
|
||||
)
|
||||
now = db._now()
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, email, password_hash, email_verified, created_at, updated_at)"
|
||||
" VALUES (1, 'legacy@example.com', 'hash', 1, ?, ?)",
|
||||
(now, now),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO email_verification_tokens (token_hash, user_id, expires_at, created_at)"
|
||||
" VALUES ('tok', 1, ?, ?)",
|
||||
(now, now),
|
||||
)
|
||||
|
||||
db.init_schema() # déclenche _migrate
|
||||
|
||||
cols = {r["name"]: r for r in conn.execute("PRAGMA table_info(users)")}
|
||||
assert cols["password_hash"]["notnull"] == 0
|
||||
# Données préservées, pas de cascade-delete déclenchée pendant la reconstruction.
|
||||
assert db.get_user_by_id(1)["email"] == "legacy@example.com"
|
||||
assert (
|
||||
conn.execute("SELECT COUNT(*) FROM email_verification_tokens").fetchone()[0]
|
||||
== 1
|
||||
)
|
||||
Reference in New Issue
Block a user