Files

67 lines
3.2 KiB
Python
Raw Permalink Normal View History

"""Environment credential precedence, rotation and account ownership boundaries."""
import pytest
from pydantic import SecretStr, ValidationError
from app.config import Settings
from app.models import Account, Admin
from app.security import bootstrap, cipher
def test_dotenv_and_process_precedence(settings, tmp_path, monkeypatch):
env = tmp_path / '.env'
env.write_text("WQ_EMAIL=local@example.com\nWQ_PASSWORD='literal-$value#password'\n")
values = settings.model_dump(exclude={'wq_email', 'wq_password'})
local = Settings(_env_file=env, **values)
assert local.wq_email == 'local@example.com'
assert local.wq_password.get_secret_value() == 'literal-$value#password'
monkeypatch.setenv('WQ_EMAIL', 'production@example.com')
monkeypatch.setenv('WQ_PASSWORD', 'production-secret')
production = Settings(_env_file=env, **values)
assert production.wq_email == 'production@example.com'
assert production.wq_password.get_secret_value() == 'production-secret'
assert 'production-secret' not in repr(production)
@pytest.mark.parametrize('email,password', [('person@example.com', ''), ('', 'private-value')])
def test_partial_configuration_fails_without_leaking(settings, email, password):
values = settings.model_dump(exclude={'wq_email', 'wq_password'})
with pytest.raises(ValidationError) as error:
Settings(_env_file=None, **values, wq_email=email, wq_password=password)
assert 'must be configured together' in str(error.value)
assert 'private-value' not in str(error.value)
assert 'person@example.com' not in str(error.value)
async def test_env_rotation_api_lock_and_bound_account(app, logged_in):
settings = app.state.settings
settings.wq_email = 'person@example.com'
settings.wq_password = SecretStr('initial-env-secret')
async with app.state.sessions() as db:
original_admin = (await db.get(Admin, 1)).password_hash
await bootstrap(db, settings)
account = await db.get(Account, 1)
assert cipher(settings).decrypt(account.password_encrypted.encode()) == b'initial-env-secret'
account.wq_user_id = 'bound-user'
await db.commit()
settings.wq_password = SecretStr('rotated-env-secret')
async with app.state.sessions() as db:
await bootstrap(db, settings)
account = await db.get(Account, 1)
assert cipher(settings).decrypt(account.password_encrypted.encode()) == b'rotated-env-secret'
assert (await db.get(Admin, 1)).password_hash == original_admin
response = await logged_in.get('/api/v1/account')
assert response.json()['credentials_source'] == 'environment'
assert response.json()['configured'] is True
assert 'secret' not in response.text and 'password' not in response.text
response = await logged_in.put('/api/v1/account/credentials', json={
'email': 'person@example.com', 'password': 'manual-secret',
})
assert response.status_code == 409
settings.wq_email = 'other@example.com'
async with app.state.sessions() as db:
with pytest.raises(ValueError, match='bound WorldQuant account'):
await bootstrap(db, settings)
await db.rollback()
assert (await db.get(Account, 1)).email == 'person@example.com'