2026-05-22 18:13:06 +05:30
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import jwt
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-05-29 15:28:47 +05:30
|
|
|
from authsome.server.account_auth import UI_TOKEN_AUDIENCE, AccountAuthService
|
2026-05-27 16:02:24 +05:30
|
|
|
from authsome.server.store import ServerStore, create_server_store
|
2026-05-31 01:41:05 +05:30
|
|
|
from authsome.server.ui_sessions import UiSessionStore
|
2026-05-27 16:02:24 +05:30
|
|
|
|
|
|
|
|
|
2026-05-29 15:28:47 +05:30
|
|
|
async def _service(tmp_path: Path) -> tuple[AccountAuthService, ServerStore]:
|
2026-05-27 16:02:24 +05:30
|
|
|
store = await create_server_store(home=tmp_path)
|
|
|
|
|
return (
|
2026-05-29 15:28:47 +05:30
|
|
|
AccountAuthService(
|
2026-05-27 16:02:24 +05:30
|
|
|
principals=store.principals,
|
|
|
|
|
vaults=store.vaults,
|
|
|
|
|
bindings=store.principal_vault_bindings,
|
2026-05-31 01:41:05 +05:30
|
|
|
sessions=UiSessionStore("test-secret"),
|
2026-05-27 16:02:24 +05:30
|
|
|
),
|
|
|
|
|
store,
|
2026-05-22 18:13:06 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
async def _close(store: ServerStore) -> None:
|
|
|
|
|
await store.close()
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 18:13:06 +05:30
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_register_creates_principal_and_password_hash(tmp_path: Path) -> None:
|
2026-05-27 16:02:24 +05:30
|
|
|
service, store = await _service(tmp_path)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
try:
|
|
|
|
|
principal = await service.register(email="Dev@Example.com", password="password-1")
|
|
|
|
|
stored = await store.principals.get(principal.principal_id)
|
|
|
|
|
binding = await store.principal_vault_bindings.get_default_vault(principal.principal_id)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
assert principal.email == "dev@example.com"
|
|
|
|
|
assert principal.principal_id.startswith("principal_")
|
|
|
|
|
assert principal.password_hash != "password-1"
|
|
|
|
|
assert stored is not None
|
|
|
|
|
assert stored.password_hash == principal.password_hash
|
|
|
|
|
assert binding is not None
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_register_rejects_duplicate_email(tmp_path: Path) -> None:
|
2026-05-27 16:02:24 +05:30
|
|
|
service, store = await _service(tmp_path)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
try:
|
|
|
|
|
await service.register(email="dev@example.com", password="password-1")
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
|
|
|
await service.register(email="DEV@example.com", password="password-2")
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_register_adds_password_to_existing_passwordless_principal(tmp_path: Path) -> None:
|
2026-05-27 16:02:24 +05:30
|
|
|
service, store = await _service(tmp_path)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
try:
|
|
|
|
|
existing = await store.principals.create_by_email("dev@example.com")
|
|
|
|
|
registered = await service.register(email="dev@example.com", password="password-1")
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
assert registered.principal_id == existing.principal_id
|
|
|
|
|
assert registered.password_hash is not None
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_login_verifies_password_and_issues_jwt(tmp_path: Path) -> None:
|
2026-05-27 16:02:24 +05:30
|
|
|
service, store = await _service(tmp_path)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
try:
|
|
|
|
|
created = await service.register(email="dev@example.com", password="password-1")
|
|
|
|
|
session = await service.login(email="dev@example.com", password="password-1")
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
claims = jwt.decode(session.token, "test-secret", algorithms=["HS256"], audience=UI_TOKEN_AUDIENCE)
|
|
|
|
|
assert session.principal_id == created.principal_id
|
|
|
|
|
assert claims["sub"] == created.principal_id
|
|
|
|
|
assert claims["email"] == "dev@example.com"
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_login_rejects_wrong_password(tmp_path: Path) -> None:
|
2026-05-27 16:02:24 +05:30
|
|
|
service, store = await _service(tmp_path)
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
try:
|
|
|
|
|
await service.register(email="dev@example.com", password="password-1")
|
2026-05-22 18:13:06 +05:30
|
|
|
|
2026-05-27 16:02:24 +05:30
|
|
|
with pytest.raises(ValueError, match="Invalid email or password"):
|
|
|
|
|
await service.login(email="dev@example.com", password="wrong-password")
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|
2026-06-15 17:27:10 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_change_password_requires_current_password_and_updates_login(tmp_path: Path) -> None:
|
|
|
|
|
service, store = await _service(tmp_path)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
principal = await service.register(email="dev@example.com", password="password-1")
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match="Invalid current password"):
|
|
|
|
|
await service.change_password(
|
|
|
|
|
principal_id=principal.principal_id,
|
|
|
|
|
current_password="wrong-password",
|
|
|
|
|
new_password="password-2",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await service.change_password(
|
|
|
|
|
principal_id=principal.principal_id,
|
|
|
|
|
current_password="password-1",
|
|
|
|
|
new_password="password-2",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match="Invalid email or password"):
|
|
|
|
|
await service.login(email="dev@example.com", password="password-1")
|
|
|
|
|
|
|
|
|
|
session = await service.login(email="dev@example.com", password="password-2")
|
|
|
|
|
assert session.principal_id == principal.principal_id
|
|
|
|
|
finally:
|
|
|
|
|
await _close(store)
|