mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-02 18:17:11 +09:00
Single-use, short-TTL, optionally IP-bound login URLs (hash-at-rest, migration 049, hourly reap job) minted from Settings > Users; plus a config-gated demo mode that 403s mutating API calls and seeds read-only credentials on the login page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""Demo mode: mutation blocking, allowlist, env-wins precedence, demo-info."""
|
|
import pytest
|
|
|
|
from app.middleware.demo import init_demo_mode
|
|
|
|
|
|
@pytest.fixture
|
|
def demo_app(app):
|
|
"""The conftest app with the demo guard wired (not yet in create_app)."""
|
|
init_demo_mode(app)
|
|
# Idempotency: a second call must be a no-op (no double-registration).
|
|
init_demo_mode(app)
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def demo_client(demo_app):
|
|
return demo_app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def _no_env_flag(monkeypatch):
|
|
monkeypatch.delenv('SERVERKIT_DEMO_MODE', raising=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def _env_flag_on(monkeypatch):
|
|
monkeypatch.setenv('SERVERKIT_DEMO_MODE', '1')
|
|
|
|
|
|
def test_flag_off_mutations_pass(_no_env_flag, demo_client, auth_headers):
|
|
resp = demo_client.post('/api/v1/auth/login-links', json={},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 201 # not blocked
|
|
|
|
|
|
def test_env_flag_blocks_mutations(_env_flag_on, demo_client, auth_headers):
|
|
resp = demo_client.post('/api/v1/auth/login-links', json={},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 403
|
|
assert resp.get_json() == {'error': 'demo_mode'}
|
|
|
|
resp = demo_client.delete('/api/v1/auth/login-links/1', headers=auth_headers)
|
|
assert resp.status_code == 403
|
|
assert resp.get_json() == {'error': 'demo_mode'}
|
|
|
|
|
|
def test_env_flag_get_passes(_env_flag_on, demo_client, auth_headers):
|
|
resp = demo_client.get('/api/v1/auth/login-links', headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_login_allowlisted_in_demo_mode(_env_flag_on, demo_client, auth_headers):
|
|
# auth_headers created the 'testadmin' user with password 'testpass'
|
|
resp = demo_client.post('/api/v1/auth/login',
|
|
json={'email': 'testadmin', 'password': 'testpass'})
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()['access_token']
|
|
|
|
|
|
def test_settings_flag_blocks_mutations(_no_env_flag, demo_app, demo_client,
|
|
auth_headers):
|
|
from app.services.settings_service import SettingsService
|
|
with demo_app.app_context():
|
|
SettingsService.set('demo_mode', True)
|
|
|
|
resp = demo_client.post('/api/v1/auth/login-links', json={},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 403
|
|
assert resp.get_json() == {'error': 'demo_mode'}
|
|
|
|
|
|
def test_env_wins_over_setting(demo_app, demo_client, auth_headers, monkeypatch):
|
|
from app.services.settings_service import SettingsService
|
|
with demo_app.app_context():
|
|
SettingsService.set('demo_mode', True)
|
|
monkeypatch.setenv('SERVERKIT_DEMO_MODE', '0') # env OFF overrides setting ON
|
|
|
|
resp = demo_client.post('/api/v1/auth/login-links', json={},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 201
|
|
|
|
|
|
def test_demo_info_off(_no_env_flag, demo_app, demo_client):
|
|
resp = demo_client.get('/api/v1/auth/demo-info')
|
|
assert resp.status_code == 200
|
|
assert resp.get_json() == {'enabled': False}
|
|
|
|
# No demo user was created
|
|
from app.models import User
|
|
with demo_app.app_context():
|
|
assert User.query.filter_by(username='demo').first() is None
|
|
|
|
|
|
def test_demo_info_on_seeds_working_readonly_user(_env_flag_on, demo_app,
|
|
demo_client):
|
|
resp = demo_client.get('/api/v1/auth/demo-info')
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data['enabled'] is True
|
|
assert data['username'] == 'demo'
|
|
assert data['password']
|
|
|
|
# Stable across calls
|
|
again = demo_client.get('/api/v1/auth/demo-info').get_json()
|
|
assert again['password'] == data['password']
|
|
|
|
# Seeded user is a viewer and the credentials actually log in
|
|
from app.models import User
|
|
with demo_app.app_context():
|
|
user = User.query.filter_by(username='demo').first()
|
|
assert user is not None
|
|
assert user.role == User.ROLE_VIEWER
|
|
|
|
login = demo_client.post('/api/v1/auth/login',
|
|
json={'email': 'demo', 'password': data['password']})
|
|
assert login.status_code == 200
|