Files
hermes-command-center/test_config_editor.py
T
shawn 54af25d2da feat: config file editor with API endpoints, frontend UI, and tests
- Added GET /api/config/list - list whitelisted config files with metadata
- Added GET /api/config/read - read file content with line numbers
- Added POST /api/config/save - save with auto-backup and YAML validation
- Added POST /api/config/backup - manual backup endpoint
- Security: whitelist-based path validation, rejects paths outside whitelist
- Auto-backup before save with .bak.<timestamp> naming
- YAML validation for .yaml/.yml files
- Frontend: file list sidebar, code editor with line numbers, status bar
- Mobile-responsive layout (file list top, editor below)
- Config panel in nav, lazy-loaded when navigated to
- 13/13 tests passing
2026-06-01 23:49:30 -04:00

94 lines
3.3 KiB
Python

"""Test the config editor API endpoints."""
import multiprocessing
import time
import json
import sys
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Start server
import uvicorn
import requests
def run_server():
uvicorn.run('server:app', host='127.0.0.1', port=8891, log_level='warning')
p = multiprocessing.Process(target=run_server, daemon=True)
p.start()
time.sleep(2)
print('=== Server started ===')
BASE = 'http://127.0.0.1:8891'
AUTH = ('shawn', 'Brett85!@')
def t(name, ok, detail=''):
status = 'PASS' if ok else 'FAIL'
print(f' [{status}] {name}' + (f'{detail}' if detail else ''))
# 1. Health
r = requests.get(f'{BASE}/health')
t('Health endpoint', r.ok and r.json()['status'] == 'ok')
# 2. Config list
r = requests.get(f'{BASE}/api/config/list', auth=AUTH)
t('Config list returns 200', r.status_code == 200)
data = r.json()
t('Has files array', 'files' in data and len(data['files']) > 0, f'{len(data["files"])} files')
# 3. Read first file
first_file = data['files'][0]['path']
r = requests.get(f'{BASE}/api/config/read?path={requests.utils.quote(first_file)}', auth=AUTH)
t('Read file returns 200', r.status_code == 200)
rd = r.json()
t('Has content and metadata', all(k in rd for k in ('content', 'line_count', 'size', 'last_modified')))
t('Line count matches content', rd['line_count'] == len(rd['content'].split('\n')))
# 4. Unauthorized path
r = requests.get(f'{BASE}/api/config/read?path=/etc/passwd', auth=AUTH)
t('Rejects /etc/passwd', r.status_code == 403)
r = requests.get(f'{BASE}/api/config/read?path={requests.utils.quote("/home/oplabs/.bashrc")}', auth=AUTH)
t('Rejects .bashrc', r.status_code == 403)
# 5. Missing file in whitelist
missing_files = [f for f in data['files'] if f.get('missing')]
t('Missing files reported correctly', not missing_files or all(f.get('missing') for f in missing_files))
# 6. YAML validation - invalid YAML
yaml_files = [f for f in data['files'] if f['filename'].endswith('.yaml')]
assert len(yaml_files) > 0, 'Need at least one yaml file to test validation'
yaml_path = yaml_files[0]['path']
r = requests.post(f'{BASE}/api/config/save',
json={'path': yaml_path, 'content': 'invalid: yaml: [[[bad'},
auth=AUTH)
t('Rejects invalid YAML', r.status_code == 400 and 'YAML validation' in r.json().get('error', ''))
# 7. Save valid YAML
r = requests.post(f'{BASE}/api/config/save',
json={'path': yaml_path, 'content': 'key: value\nnested:\n sub: data\n'},
auth=AUTH)
t('Saves valid YAML', r.status_code == 200 and r.json().get('saved') and 'backup_path' in r.json())
saved = r.json()
print(f' Backup created: {saved["backup_path"]}')
# 8. Verify backup exists
bak_path = saved['backup_path']
r = requests.get(f'{BASE}/api/config/read?path={requests.utils.quote(first_file)}', auth=AUTH)
t('Backup file path is returned', 'bak.' in bak_path)
print(f' Original: {saved["path"]}')
print(f' Lines: {saved["line_count"]}, Size: {saved["size"]}')
# 9. Test backup endpoint directly
r = requests.post(f'{BASE}/api/config/backup?path={requests.utils.quote(yaml_path)}', auth=AUTH)
t('Manual backup endpoint', r.status_code == 200 and 'backup_path' in r.json())
# 10. HTML serving
r = requests.get(f'{BASE}/', auth=AUTH)
t('Serves index.html', r.status_code == 200 and 'Config Editor' in r.text)
p.terminate()
p.join()
print('\n=== All tests complete ===')