mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
The largest single move in the plan (#38). The entire WordPress backend leaves core for the bundled, default-installed serverkit-wordpress extension (D4). Moved (12 files → builtin-extensions/serverkit-wordpress/backend/, intra-extension imports made relative, core imports stay absolute): api/wordpress.py, api/wordpress_sites.py, api/environment_pipeline.py, wordpress_service.py (2676 lines), git_wordpress_service, wordpress_env_service, wordpress_plugin_library_service, wp_analytics/reports/security/update/vulnerability. Wiring: - Blueprints keep /api/v1/wordpress, /wordpress/projects and the /wordpress/pipelines alias (D9) via new manifest `extra_blueprints` support (3 blueprints / 4 mounts). - Flagship (D4): seeded active on every panel (fresh + upgrade), uninstallable, never deletes tracked builtin/pre-bundled files; the `wordpress` module toggle is retired (MODULES now empty) — the plugin status guard is the gate. - Loadability: the dashed package app.plugins.serverkit-wordpress is registered in-place via spec_from_file_location (no copy) — uniform across prod/dev/test. - The 11 WP test files re-pointed through app.services.wordpress_bridge; the 6 lazy core callers were re-pointed in the prior commit. - _regenerate_frontend_manifest() is a no-op under testing so the flagship boot seed can't clobber the checked-in plugins-manifest.json. Deviations (documented, matching the Email precedent): - WordPress *models* stay core — status_page/environment_activity/promotion_job hold FKs into wordpress_sites, so relocation would be a breaking data migration. - Event catalog + Fail2ban WP filter stay core as two-speed items (extension calls them via core APIs); inverting adds risk without blocking the move. - Frontend UI stays core this session (it lives in a TabGroupLayout — the tab-grouped conversion Phase 1 deliberately deferred); the extension is backend-only, so there is no route duplication. Frontend contribution is the scoped follow-up. Verified: clean collection across all 1223 tests; 199 passed / 2 skipped / 0 failed across the WP + plugin/extension + module + backup + core-caller parity set, incl. the new tests/test_wordpress_extraction.py proving the move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
"""§7 unification — WordPress reachable through the generic /apps surface.
|
|
|
|
A WordPress site is a 1:1 extension of an Application. These additive routes
|
|
expose the linkage (/apps/:id/wordpress) and the WP DB snapshots
|
|
(/apps/:id/db-snapshots) under the app id WITHOUT changing app_type, so the
|
|
docker-compose paths that key on app_type=='docker' are unaffected.
|
|
"""
|
|
import pytest
|
|
|
|
from app.services import wordpress_bridge
|
|
|
|
|
|
def _seed_app(app, *, wordpress=True):
|
|
from app import db
|
|
from app.models import User
|
|
from app.models.application import Application
|
|
from app.models.wordpress_site import WordPressSite
|
|
with app.app_context():
|
|
admin = User.query.filter_by(username='testadmin').first()
|
|
a = Application(name='wp-app', app_type='docker', status='running',
|
|
root_path='/srv/wp', user_id=admin.id if admin else 1)
|
|
db.session.add(a)
|
|
db.session.commit()
|
|
site_id = None
|
|
if wordpress:
|
|
site = WordPressSite(application_id=a.id, db_name='wpdb', db_user='wpuser',
|
|
db_host='localhost', is_production=True,
|
|
environment_type='production', wp_version='6.5')
|
|
db.session.add(site)
|
|
db.session.commit()
|
|
site_id = site.id
|
|
return a.id, site_id
|
|
|
|
|
|
def test_app_wordpress_info_true(app, client, auth_headers):
|
|
app_id, site_id = _seed_app(app, wordpress=True)
|
|
res = client.get(f'/api/v1/apps/{app_id}/wordpress', headers=auth_headers)
|
|
assert res.status_code == 200
|
|
body = res.get_json()
|
|
assert body['is_wordpress'] is True
|
|
assert body['site_id'] == site_id
|
|
assert body['is_production'] is True
|
|
|
|
|
|
def test_app_wordpress_info_false_for_plain_app(app, client, auth_headers):
|
|
app_id, _ = _seed_app(app, wordpress=False)
|
|
res = client.get(f'/api/v1/apps/{app_id}/wordpress', headers=auth_headers)
|
|
assert res.status_code == 200
|
|
assert res.get_json()['is_wordpress'] is False
|
|
|
|
|
|
def test_db_snapshots_rejects_non_wordpress(app, client, auth_headers):
|
|
app_id, _ = _seed_app(app, wordpress=False)
|
|
res = client.get(f'/api/v1/apps/{app_id}/db-snapshots', headers=auth_headers)
|
|
assert res.status_code == 400
|
|
|
|
|
|
def test_db_snapshots_list_empty(app, client, auth_headers):
|
|
app_id, _ = _seed_app(app, wordpress=True)
|
|
res = client.get(f'/api/v1/apps/{app_id}/db-snapshots', headers=auth_headers)
|
|
assert res.status_code == 200
|
|
assert res.get_json()['snapshots'] == []
|
|
|
|
|
|
def test_db_snapshot_create_and_delete(app, client, auth_headers, monkeypatch):
|
|
from app.services.db_sync_service import DatabaseSyncService
|
|
WordPressEnvService = wordpress_bridge.get('wordpress_env_service', 'WordPressEnvService')
|
|
|
|
monkeypatch.setattr(WordPressEnvService, '_get_db_password', staticmethod(lambda site: 'pw'))
|
|
monkeypatch.setattr(DatabaseSyncService, 'create_snapshot', staticmethod(
|
|
lambda **kw: {'success': True, 'snapshot': {
|
|
'name': kw.get('name'), 'file_path': '/var/backups/snap.sql.gz',
|
|
'size_bytes': 1234, 'compressed': True, 'tables': ['wp_posts'], 'row_count': 10,
|
|
}}))
|
|
monkeypatch.setattr(DatabaseSyncService, 'upload_snapshot_offsite', staticmethod(lambda p: None))
|
|
monkeypatch.setattr(DatabaseSyncService, 'delete_snapshot', staticmethod(lambda p: True))
|
|
|
|
app_id, _ = _seed_app(app, wordpress=True)
|
|
created = client.post(f'/api/v1/apps/{app_id}/db-snapshots',
|
|
json={'name': 'nightly', 'tag': 'manual'}, headers=auth_headers)
|
|
assert created.status_code == 201, created.get_json()
|
|
snap_id = created.get_json()['snapshot']['id']
|
|
|
|
listed = client.get(f'/api/v1/apps/{app_id}/db-snapshots', headers=auth_headers)
|
|
assert listed.get_json()['total'] == 1
|
|
|
|
deleted = client.delete(f'/api/v1/apps/{app_id}/db-snapshots/{snap_id}', headers=auth_headers)
|
|
assert deleted.status_code == 200
|
|
assert client.get(f'/api/v1/apps/{app_id}/db-snapshots', headers=auth_headers).get_json()['total'] == 0
|