Add debug mode feature flag with environment variable parsing, UI badge indicator, secret redaction utility, and enhanced terminal session management with status checks and session limit error handling
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -27,6 +27,7 @@ os.environ["API_KEY"] = "test-api-key-12345"
|
||||
os.environ["JWT_SECRET_KEY"] = "test-jwt-secret-key-for-testing-only"
|
||||
os.environ["NTFY_ENABLED"] = "false"
|
||||
os.environ["ANSIBLE_DIR"] = "."
|
||||
os.environ["DEBUG_MODE"] = "YES"
|
||||
|
||||
from app.models.database import Base
|
||||
from app.core.dependencies import get_db, verify_api_key, get_current_user
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import pytest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.core.config import parse_env_bool, settings
|
||||
from app.models.terminal_session import TerminalSession, SESSION_STATUS_ACTIVE
|
||||
|
||||
|
||||
class TestParseEnvBool:
|
||||
def test_parse_env_bool(self):
|
||||
assert parse_env_bool("YES") is True
|
||||
assert parse_env_bool("yes") is True
|
||||
assert parse_env_bool("TRUE") is True
|
||||
assert parse_env_bool("1") is True
|
||||
|
||||
assert parse_env_bool("NO") is False
|
||||
assert parse_env_bool("no") is False
|
||||
assert parse_env_bool("FALSE") is False
|
||||
assert parse_env_bool("0") is False
|
||||
|
||||
assert parse_env_bool("invalid") is False
|
||||
assert parse_env_bool(None) is False
|
||||
assert parse_env_bool(None, default=True) is True
|
||||
|
||||
|
||||
class TestDebugModeApi:
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_endpoint_reflects_debug_mode(self, client, monkeypatch):
|
||||
monkeypatch.setattr(settings, "debug_mode", False)
|
||||
resp = await client.get("/api/config")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"debug_mode": False}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminal_status_reports_disabled(self, client, auth_headers, monkeypatch):
|
||||
monkeypatch.setattr(settings, "debug_mode", False)
|
||||
with patch("app.services.terminal_service.terminal_service.check_ttyd_available", return_value=True):
|
||||
resp = await client.get("/api/terminal/status", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["available"] is True
|
||||
assert data["debug_mode"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminal_connect_allowed_when_debug_disabled(self, client, db_session, monkeypatch):
|
||||
monkeypatch.setattr(settings, "debug_mode", False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
sess = TerminalSession(
|
||||
id="s" * 64,
|
||||
host_id="host-1",
|
||||
host_name="test-host",
|
||||
host_ip="127.0.0.1",
|
||||
user_id="user-1",
|
||||
username="testuser",
|
||||
token_hash="x" * 64,
|
||||
ttyd_port=7680,
|
||||
ttyd_pid=123,
|
||||
mode="embedded",
|
||||
status=SESSION_STATUS_ACTIVE,
|
||||
created_at=now,
|
||||
last_seen_at=now,
|
||||
expires_at=now + timedelta(minutes=10),
|
||||
)
|
||||
db_session.add(sess)
|
||||
await db_session.commit()
|
||||
|
||||
with patch("app.services.terminal_service.terminal_service.verify_token", return_value=True), \
|
||||
patch("app.services.terminal_service.terminal_service.is_session_process_alive", new=AsyncMock(return_value=True)):
|
||||
resp = await client.get(f"/api/terminal/connect/{sess.id}?token=dummy")
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminal_probe_forbidden_when_debug_disabled(self, client, db_session, monkeypatch):
|
||||
monkeypatch.setattr(settings, "debug_mode", False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
sess = TerminalSession(
|
||||
id="t" * 64,
|
||||
host_id="host-1",
|
||||
host_name="test-host",
|
||||
host_ip="127.0.0.1",
|
||||
user_id="user-1",
|
||||
username="testuser",
|
||||
token_hash="x" * 64,
|
||||
ttyd_port=7681,
|
||||
ttyd_pid=123,
|
||||
mode="embedded",
|
||||
status=SESSION_STATUS_ACTIVE,
|
||||
created_at=now,
|
||||
last_seen_at=now,
|
||||
expires_at=now + timedelta(minutes=10),
|
||||
)
|
||||
db_session.add(sess)
|
||||
await db_session.commit()
|
||||
|
||||
with patch("app.services.terminal_service.terminal_service.verify_token", return_value=True):
|
||||
resp = await client.get(f"/api/terminal/sessions/{sess.id}/probe?token=dummy")
|
||||
|
||||
assert resp.status_code == 403
|
||||
assert resp.json()["detail"] == "Debug mode disabled"
|
||||
@@ -35,3 +35,36 @@ class TestTerminalConnectPage:
|
||||
assert resp.status_code == 200
|
||||
assert "<html" in resp.text.lower()
|
||||
assert "terminal" in resp.text.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_popout_page_allowed_when_debug_disabled(self, client, db_session, monkeypatch):
|
||||
from app.core.config import settings
|
||||
|
||||
monkeypatch.setattr(settings, "debug_mode", False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
sess = TerminalSession(
|
||||
id="p" * 64,
|
||||
host_id="host-1",
|
||||
host_name="test-host",
|
||||
host_ip="127.0.0.1",
|
||||
user_id="user-1",
|
||||
username="testuser",
|
||||
token_hash="x" * 64,
|
||||
ttyd_port=7680,
|
||||
ttyd_pid=123,
|
||||
mode="popout",
|
||||
status=SESSION_STATUS_ACTIVE,
|
||||
created_at=now,
|
||||
last_seen_at=now,
|
||||
expires_at=now + timedelta(minutes=10),
|
||||
)
|
||||
db_session.add(sess)
|
||||
await db_session.commit()
|
||||
|
||||
with patch("app.services.terminal_service.terminal_service.verify_token", return_value=True), \
|
||||
patch("app.services.terminal_service.terminal_service.is_session_process_alive", new=AsyncMock(return_value=True)):
|
||||
resp = await client.get(f"/api/terminal/popout/{sess.id}?token=dummy")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "<html" in resp.text.lower()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.services.terminal_service import TERMINAL_MAX_SESSIONS_PER_USER
|
||||
|
||||
|
||||
|
||||
class TestTerminalStatus:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
function loadMainJsAsGlobal() {
|
||||
const mainPath = path.resolve(process.cwd(), 'app', 'main.js');
|
||||
const source = fs.readFileSync(mainPath, 'utf8');
|
||||
const wrapped = `${source}\n;globalThis.__DashboardManager = DashboardManager;`;
|
||||
eval(wrapped);
|
||||
return globalThis.__DashboardManager;
|
||||
}
|
||||
|
||||
describe('DEBUG_MODE UI gating', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div class="desktop-nav-links">
|
||||
<div class="relative group"></div>
|
||||
</div>
|
||||
<div id="hosts-list"></div>
|
||||
`;
|
||||
|
||||
delete globalThis.__DashboardManager;
|
||||
delete globalThis.DashboardManager;
|
||||
});
|
||||
|
||||
it('shows DEBUG badge only when enabled', () => {
|
||||
const DashboardManager = loadMainJsAsGlobal();
|
||||
const dashboard = new DashboardManager();
|
||||
|
||||
dashboard.setDebugBadgeVisible(false);
|
||||
expect(document.getElementById('debug-mode-badge')).toBeNull();
|
||||
|
||||
dashboard.setDebugBadgeVisible(true);
|
||||
expect(document.getElementById('debug-mode-badge')?.textContent).toBe('DEBUG');
|
||||
|
||||
dashboard.setDebugBadgeVisible(false);
|
||||
expect(document.getElementById('debug-mode-badge')).toBeNull();
|
||||
});
|
||||
|
||||
it('mounts terminal buttons when debug is disabled', () => {
|
||||
const DashboardManager = loadMainJsAsGlobal();
|
||||
const dashboard = new DashboardManager();
|
||||
|
||||
dashboard.debugModeEnabled = false;
|
||||
dashboard.hosts = [
|
||||
{
|
||||
id: 'h1',
|
||||
name: 'host1',
|
||||
ip: '127.0.0.1',
|
||||
os: 'linux',
|
||||
status: 'online',
|
||||
bootstrap_ok: true,
|
||||
groups: ['env_prod', 'role_web'],
|
||||
last_seen: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
dashboard.renderHosts();
|
||||
|
||||
expect(document.querySelector('[data-action="terminal"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-action="terminal-popout"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('mounts terminal buttons when debug is enabled', () => {
|
||||
const DashboardManager = loadMainJsAsGlobal();
|
||||
const dashboard = new DashboardManager();
|
||||
|
||||
dashboard.debugModeEnabled = true;
|
||||
dashboard.hosts = [
|
||||
{
|
||||
id: 'h1',
|
||||
name: 'host1',
|
||||
ip: '127.0.0.1',
|
||||
os: 'linux',
|
||||
status: 'online',
|
||||
bootstrap_ok: true,
|
||||
groups: ['env_prod', 'role_web'],
|
||||
last_seen: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
dashboard.renderHosts();
|
||||
|
||||
expect(document.querySelector('[data-action="terminal"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-action="terminal-popout"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,7 @@ export function createMockResponse(data, status = 200) {
|
||||
export function setupFetchMock(responses = {}) {
|
||||
const defaultResponses = {
|
||||
'/api/auth/status': { setup_required: false, authenticated: true, user: { username: 'test' } },
|
||||
'/api/config': { debug_mode: false },
|
||||
'/api/hosts': [],
|
||||
'/api/tasks': [],
|
||||
'/api/schedules': [],
|
||||
|
||||
Reference in New Issue
Block a user