mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 00:27:11 +09:00
Route manifest-created apps to a fleet server via top-level/per-service server: (sets Application.server_id; unknown server = plan issue). Clone the manifest-resolved config (env + per-preview overrides + domain) into non-WP PR previews via ManifestPreviewService. Add 'serverkit manifest plan|apply|diff' CLI verbs against the manifests API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
186 lines
6.3 KiB
Python
186 lines
6.3 KiB
Python
"""Tests for the `serverkit manifest` CLI verbs (#22): plan/apply/diff against a
|
|
stubbed API client, mirroring the monkeypatch pattern in test_cli_api.py."""
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
import cli as serverkit_cli
|
|
|
|
|
|
def _all_output(result):
|
|
out = result.output
|
|
try:
|
|
out += result.stderr
|
|
except (ValueError, AttributeError):
|
|
pass
|
|
return out
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, responses):
|
|
self.responses = responses
|
|
self.calls = []
|
|
|
|
def get(self, path):
|
|
self.calls.append(('GET', path, None))
|
|
return self.responses[path]
|
|
|
|
def post(self, path, json_body=None):
|
|
self.calls.append(('POST', path, json_body))
|
|
resp = self.responses[path]
|
|
if isinstance(resp, Exception):
|
|
raise resp
|
|
return resp
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_api(monkeypatch):
|
|
def _install(responses):
|
|
fake = FakeClient(responses)
|
|
monkeypatch.setattr(serverkit_cli, '_api_client', lambda with_token=True: fake)
|
|
return fake
|
|
return _install
|
|
|
|
|
|
def _plan(steps, summary='Plan computed', issues=None):
|
|
return {'plan': {
|
|
'steps': steps,
|
|
'step_count': len(steps),
|
|
'summary': summary,
|
|
'issues': issues or [],
|
|
}}
|
|
|
|
|
|
def test_plan_prints_steps_and_summary(fake_api):
|
|
fake = fake_api({
|
|
'/manifests/plan': _plan(
|
|
[
|
|
{'type': 'create', 'service': 'web', 'description': 'create app web'},
|
|
{'type': 'update', 'service': 'api', 'description': 'update env'},
|
|
],
|
|
summary='2 changes planned',
|
|
),
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'plan', '--project', '7'])
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert ('POST', '/manifests/plan', {'project_id': 7}) in fake.calls
|
|
assert 'create web create app web' in result.output
|
|
assert 'update api update env' in result.output
|
|
assert '2 changes planned' in result.output
|
|
assert '(2 steps)' in result.output
|
|
|
|
|
|
def test_plan_prints_issues_as_warnings(fake_api):
|
|
fake_api({
|
|
'/manifests/plan': _plan(
|
|
[{'type': 'create', 'service': 'web', 'description': 'create'}],
|
|
issues=[{'message': 'Missing secret DB_PASSWORD'}],
|
|
),
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'plan', '--project', '1'])
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert 'Missing secret DB_PASSWORD' in result.output
|
|
|
|
|
|
def test_diff_empty_plan_prints_no_changes(fake_api):
|
|
fake_api({'/manifests/plan': _plan([])})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'diff', '--project', '3'])
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert 'No changes — live state matches the manifest.' in result.output
|
|
|
|
|
|
def test_diff_shows_planned_changes(fake_api):
|
|
fake_api({
|
|
'/manifests/plan': _plan(
|
|
[{'type': 'delete', 'service': 'old', 'description': 'remove old app'}]),
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'diff', '--project', '3'])
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert 'Planned changes:' in result.output
|
|
assert 'delete old remove old app' in result.output
|
|
|
|
|
|
def test_apply_yes_posts_and_reports_success(fake_api):
|
|
fake = fake_api({
|
|
'/manifests/apply': {
|
|
'success': True,
|
|
'applied': 2,
|
|
'issues': [],
|
|
'results': [
|
|
{'type': 'create', 'service': 'web', 'status': 'ok'},
|
|
{'type': 'update', 'service': 'api', 'status': 'applied'},
|
|
],
|
|
'job_id': 42,
|
|
},
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'apply', '--project', '9', '--yes'])
|
|
assert result.exit_code == 0, _all_output(result)
|
|
# --yes skips the plan call; only apply is posted.
|
|
assert fake.calls == [('POST', '/manifests/apply', {'project_id': 9})]
|
|
assert 'Applied 2 change(s).' in result.output
|
|
assert 'web' in result.output and 'api' in result.output
|
|
|
|
|
|
def test_apply_reports_failure_and_exits_nonzero(fake_api):
|
|
fake_api({
|
|
'/manifests/apply': {
|
|
'success': False,
|
|
'applied': 1,
|
|
'issues': [],
|
|
'results': [
|
|
{'type': 'create', 'service': 'web', 'status': 'ok'},
|
|
{'type': 'update', 'service': 'api', 'status': 'error',
|
|
'error': 'port in use'},
|
|
],
|
|
'job_id': 43,
|
|
},
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'apply', '--project', '9', '--yes'])
|
|
assert result.exit_code == 1
|
|
assert 'port in use' in _all_output(result)
|
|
|
|
|
|
def test_apply_shows_plan_and_confirms(fake_api):
|
|
fake = fake_api({
|
|
'/manifests/plan': _plan(
|
|
[{'type': 'create', 'service': 'web', 'description': 'create app web'}]),
|
|
'/manifests/apply': {
|
|
'success': True, 'applied': 1, 'issues': [],
|
|
'results': [{'type': 'create', 'service': 'web', 'status': 'ok'}],
|
|
'job_id': 1,
|
|
},
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'apply', '--project', '5'], input='y\n')
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert 'create app web' in result.output
|
|
posted = [c[1] for c in fake.calls]
|
|
assert '/manifests/plan' in posted and '/manifests/apply' in posted
|
|
|
|
|
|
def test_apply_abort_when_declined(fake_api):
|
|
fake = fake_api({
|
|
'/manifests/plan': _plan(
|
|
[{'type': 'create', 'service': 'web', 'description': 'create app web'}]),
|
|
'/manifests/apply': {'success': True, 'applied': 0, 'results': []},
|
|
})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'apply', '--project', '5'], input='n\n')
|
|
assert result.exit_code == 0, _all_output(result)
|
|
assert 'Aborted.' in result.output
|
|
assert not any(c[1] == '/manifests/apply' for c in fake.calls)
|
|
|
|
|
|
def test_cli_api_error_surfaces_via_fail(fake_api):
|
|
from app.services.cli_api_client import CliApiError
|
|
fake_api({'/manifests/plan': CliApiError('Admin access required')})
|
|
result = CliRunner().invoke(
|
|
serverkit_cli.cli, ['manifest', 'plan', '--project', '2'])
|
|
assert result.exit_code == 1
|
|
assert 'Admin access required' in _all_output(result)
|