Files
ServerKit/backend/tests/test_cloud_provision_extraction.py
Juan DenisandClaude Opus 5 9d570834aa perf(tests): build the Flask app once per session, not once per test
The `app` fixture called create_app('testing') for every one of 3173 tests,
registering 90+ blueprints each time. Measured on a fixed 199-test sample
(pytest --splits 16 --group 5 --durations=0):

    setup     144.9s   89.5%
    call        8.8s    5.4%
    teardown    8.2s    5.0%

95% of the suite's time was preparing to test; 5% was testing. The app is now
built once per process and shared.

Same 794-test slice, before and after — identical results, 2.66x faster:

    before   4 failed, 773 passed, 17 skipped   593.30s
    after    4 failed, 773 passed, 17 skipped   223.11s

(the 4 are pre-existing locally: serverkit-wordpress isn't checked out here.)

Sharing one app surfaces three things a per-test app hid, all handled in the
fixture rather than patched test by test:

1. create_app() is not a pure constructor — it SEEDS the bundled flagship
   extension rows as a boot side effect (test_cloudflare_extraction asserts
   they are "seeded on boot"). The per-test drop_all wipes them, and a shared
   app never re-boots, so every test after the first saw no flagship and the
   extension routes answered 503. seed_flagship_extensions() is now re-run
   after each create_all. The session fixture also boots twice on purpose:
   once to create the schema, once to seed into a schema that exists.

2. Config mutation now leaks where it used to die with the app (e.g.
   test_demo_deploys sets DEMO_DEPLOYS_ENABLED=False and never restores it),
   so config is snapshotted and restored around every test.

3. Structure mutation cannot be undone at all — Flask has no
   unregister_blueprint, and refuses setup methods once an app has served a
   request. Those tests opt out via `pytestmark = pytest.mark.fresh_app` and
   get a private app. Three groups needed it: 13 modules calling
   register_blueprint/add_url_rule directly, 11 that install plugins (
   plugin_service hot-loads their blueprints onto the live app), and
   test_audit_log which adds routes with @app.route.

Tests using the `wp_extension` fixture are detected from the fixture graph
instead of being marked, since that fixture mounts blueprints on whatever app
it is handed — miss one marker and the failure lands in an unrelated test much
later in the run.

Plan 64 Phase 1. Phase 2 (savepoint rollback instead of create_all/drop_all)
is NOT included: it currently takes the slice from 4 failures to 100 failures
and 201 errors, and needs its own round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 02:59:14 -04:00

122 lines
4.7 KiB
Python

"""Prove the Cloud Provisioning extraction (plan 47 Phase 2).
A fresh panel no longer carries the cloud-provider API in core; installing the
serverkit-cloud-provision builtin registers `/api/v1/cloud`. The CloudProvider
model stays core (G2). Also covers the one-shot backend re-acquisition that keeps
upgraded panels (which installed the extension frontend-only) from losing the API.
"""
import importlib
import sys
import pytest
from app import db
from app.models.plugin import InstalledPlugin
from app.services import plugin_service
# Installs plugins, and plugin_service hot-loads their blueprints onto the
# live app. Flask refuses register_blueprint once an app has served a
# request, so these need a private app (plan 64 Phase 1).
pytestmark = pytest.mark.fresh_app
SLUG = 'serverkit-cloud-provision'
_PKG = f'app.plugins.{SLUG}'
def test_core_has_no_cloud_routes(app):
rules = [r.rule for r in app.url_map.iter_rules()]
assert [r for r in rules if r.startswith('/api/v1/cloud/')] == []
def test_core_import_of_cloud_service_is_gone():
with pytest.raises(ModuleNotFoundError):
importlib.import_module('app.services.cloud_provisioning_service')
def test_cloud_in_converted_builtin_slugs():
from app.services.extension_migration import CONVERTED_BUILTIN_SLUGS
assert SLUG in CONVERTED_BUILTIN_SLUGS
def test_cloud_model_stays_core():
"""G2: the CloudProvider/CloudServer models remain importable from core."""
from app.models.cloud_server import CloudProvider, CloudServer # noqa: F401
@pytest.fixture
def install_dirs(tmp_path, monkeypatch):
backend = tmp_path / 'plugins_backend'
frontend = tmp_path / 'plugins_frontend'
backend.mkdir()
frontend.mkdir()
monkeypatch.setattr(plugin_service, 'BACKEND_PLUGINS_DIR', str(backend))
monkeypatch.setattr(plugin_service, 'FRONTEND_PLUGINS_DIR', str(frontend))
added = str(backend)
app_pkg_plugins = importlib.import_module('app.plugins')
if added not in app_pkg_plugins.__path__:
app_pkg_plugins.__path__.append(added)
yield {'backend': backend, 'frontend': frontend}
if added in app_pkg_plugins.__path__:
app_pkg_plugins.__path__.remove(added)
for name in list(sys.modules):
if name == _PKG or name.startswith(_PKG + '.'):
del sys.modules[name]
def test_install_cloud_extension_registers_routes(app, client, auth_headers, install_dirs):
available = {e['slug'] for e in plugin_service.list_builtin_extensions()}
assert SLUG in available
plugin = plugin_service.install_builtin_extension(SLUG)
assert plugin.status == InstalledPlugin.STATUS_ACTIVE
assert plugin.has_backend is True
assert plugin.url_prefix == '/api/v1/cloud'
resp = client.get('/api/v1/cloud/providers', headers=auth_headers)
assert resp.status_code not in (404, 503), resp.status_code
def test_cloud_provider_secret_encrypted_at_rest(app, install_dirs):
"""The extracted service still encrypts provider secrets at rest (moved here
from test_provider_secret_encryption.py)."""
plugin_service.install_builtin_extension(SLUG)
from app.models.cloud_server import CloudProvider
from app.utils.crypto import is_encrypted, decrypt_secret_safe
svc_mod = importlib.import_module(f'{_PKG}.cloud_provisioning_service')
CloudProvisioningService = svc_mod.CloudProvisioningService
p = CloudProvisioningService.create_provider(
{'provider_type': 'digitalocean', 'name': 'do', 'api_key': 'do-token-xyz'})
row = CloudProvider.query.get(p.id)
assert row.api_key_encrypted != 'do-token-xyz'
assert is_encrypted(row.api_key_encrypted) is True
assert decrypt_secret_safe(row.api_key_encrypted) == 'do-token-xyz'
assert CloudProvisioningService._auth_headers(row)['Authorization'] == 'Bearer do-token-xyz'
def test_backend_acquisition_upgrades_frontend_only_install(app, install_dirs):
"""An upgraded panel that installed the extension frontend-only re-acquires
the now-extracted backend (plan 47 Phase 2 migration)."""
from app.services import extension_migration
# Simulate the pre-plan-47 state: a frontend-only install (API came from core)
row = InstalledPlugin(
name=SLUG, display_name='Cloud Provisioning', slug=SLUG, version='1.0.0',
source_type='builtin', status=InstalledPlugin.STATUS_ACTIVE,
)
row.has_backend = False
row.has_frontend = True
db.session.add(row)
db.session.commit()
extension_migration.run_backend_acquisition()
refreshed = InstalledPlugin.query.filter_by(slug=SLUG).first()
assert refreshed.has_backend is True
rules = [r.rule for r in app.url_map.iter_rules()]
assert any(r.startswith('/api/v1/cloud/') for r in rules)