feat(updates): check Gitea automatically and deploy on owner confirmation
Add owner-only update controls, a read-only SSH checker and independent systemd deployment services. Build before maintenance; back up code and database; restore on failed deployment. Preserve runtime configuration and gate business writes during the switch.
This commit is contained in:
1 parent
ed8dc01ed9
commit
f3ad539998
28 files changed
+1346
-1
No files matched your search
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api import system_updates as updates
|
||||
from app.core.auth import get_current_user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup(tmp_path, monkeypatch):
|
||||
owner = str(uuid4())
|
||||
cfg = tmp_path / 'config.json'
|
||||
cfg.write_text(json.dumps({'owner_ids': [owner], 'state_dir': str(tmp_path), 'web_url': 'https://gitea.example/repo', 'branch': 'main'}))
|
||||
(tmp_path / 'status.json').write_text(json.dumps({'current': {'revision': 'a' * 40}, 'latest': {'revision': 'b' * 40}, 'update': {'status': 'idle'}}))
|
||||
monkeypatch.setattr(updates, 'CONFIG_FILE', cfg)
|
||||
command = AsyncMock(return_value={'accepted': True})
|
||||
monkeypatch.setattr(updates, 'command', command)
|
||||
app = FastAPI()
|
||||
app.include_router(updates.router, prefix='/system-updates')
|
||||
user = SimpleNamespace(id=owner, role=SimpleNamespace(permissions=[SimpleNamespace(name='super_admin')]))
|
||||
app.dependency_overrides[get_current_user] = lambda: user
|
||||
return TestClient(app), user, command
|
||||
|
||||
|
||||
def test_owner_can_check_and_queue_only_a_valid_revision(setup):
|
||||
client, user, command = setup
|
||||
assert client.get('/system-updates').json()['update_available']
|
||||
assert client.post('/system-updates/check').status_code == 202
|
||||
assert client.post('/system-updates/apply', json={'revision': 'b' * 40}).status_code == 202
|
||||
command.assert_awaited_with('request', '--revision', 'b' * 40, '--actor', user.id)
|
||||
assert client.post('/system-updates/apply', json={'revision': 'main; echo unsafe'}).status_code == 422
|
||||
|
||||
|
||||
def test_other_organization_admin_cannot_deploy_or_read_state(setup):
|
||||
client, user, command = setup
|
||||
user.id = str(uuid4())
|
||||
assert client.get('/system-updates').json() == {'configured': True, 'can_manage': False}
|
||||
assert client.post('/system-updates/check').status_code == 403
|
||||
assert client.post('/system-updates/apply', json={'revision': 'b' * 40}).status_code == 403
|
||||
command.assert_not_awaited()
|
||||
|
||||
|
||||
def test_allowlisted_account_still_needs_super_admin(setup):
|
||||
client, user, command = setup
|
||||
user.role.permissions = [SimpleNamespace(name='manage_organization')]
|
||||
assert client.post('/system-updates/check').status_code == 403
|
||||
command.assert_not_awaited()
|
||||
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
from app.core import system_maintenance as maintenance
|
||||
|
||||
|
||||
def test_maintenance_gates_business_requests_but_allows_status(tmp_path, monkeypatch):
|
||||
marker = tmp_path / 'maintenance'
|
||||
monkeypatch.setattr(maintenance, 'MAINTENANCE_FILE', marker)
|
||||
app = FastAPI()
|
||||
for path in ['/health', '/api/v1/system-updates', '/api/v1/chats']:
|
||||
app.get(path)(lambda: {'ok': True})
|
||||
client = TestClient(maintenance.SystemMaintenanceMiddleware(app))
|
||||
assert client.get('/api/v1/chats').status_code == 200
|
||||
marker.touch()
|
||||
assert client.get('/health').status_code == 200
|
||||
assert client.get('/api/v1/system-updates').status_code == 200
|
||||
assert client.get('/api/v1/chats').status_code == 503
|
||||
assert client.post('/api/v1/system-updates/check').status_code == 503
|
||||
with pytest.raises(WebSocketDisconnect) as exc:
|
||||
with client.websocket_connect('/socket.io/'):
|
||||
pass
|
||||
assert exc.value.code == 1013
|
||||
Reference in new issue
Block a user