54af25d2da
- 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
94 lines
3.3 KiB
Python
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 ===')
|