"""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'