Add Engine API tests for Stage 4

This commit is contained in:
2025-10-26 01:30:52 -06:00
parent 73f6d5745f
commit 68c7c772c0
7 changed files with 475 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ param(
[switch]$Vite, [switch]$Vite,
[switch]$Flask, [switch]$Flask,
[switch]$Quick, [switch]$Quick,
[switch]$EngineTests,
[string]$InstallerCode = '' [string]$InstallerCode = ''
) )
@@ -17,6 +18,26 @@ $choice = $null
$modeChoice = $null $modeChoice = $null
$agentSubChoice = $null $agentSubChoice = $null
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
if ($EngineTests) {
Set-Location -Path $scriptDir
$env:BOREALIS_PROJECT_ROOT = $scriptDir
$python = Get-Command python3 -ErrorAction SilentlyContinue
if (-not $python) {
$python = Get-Command python -ErrorAction SilentlyContinue
}
if (-not $python) {
Write-Host "Python interpreter not found. Install Python 3 to run Engine tests." -ForegroundColor Red
exit 1
}
& $python.Source -m pytest 'Data/Engine/Unit_Tests'
exit $LASTEXITCODE
}
if ($Server -and $Agent) { if ($Server -and $Agent) {
Write-Host "Cannot use -Server and -Agent together." -ForegroundColor Red Write-Host "Cannot use -Server and -Agent together." -ForegroundColor Red
exit 1 exit 1

View File

@@ -25,6 +25,7 @@ AGENT_ACTION=""
VITE_FLAG=0 VITE_FLAG=0
FLASK_FLAG=0 FLASK_FLAG=0
QUICK_FLAG=0 QUICK_FLAG=0
ENGINE_TESTS_FLAG=0
while (( "$#" )); do while (( "$#" )); do
case "$1" in case "$1" in
@@ -34,11 +35,29 @@ while (( "$#" )); do
-Vite|--vite) VITE_FLAG=1 ;; -Vite|--vite) VITE_FLAG=1 ;;
-Flask|--flask) FLASK_FLAG=1 ;; -Flask|--flask) FLASK_FLAG=1 ;;
-Quick|--quick) QUICK_FLAG=1 ;; -Quick|--quick) QUICK_FLAG=1 ;;
-EngineTests|--engine-tests) ENGINE_TESTS_FLAG=1 ;;
*) ;; # ignore unknown for flexibility *) ;; # ignore unknown for flexibility
esac esac
shift || true shift || true
done done
if (( ENGINE_TESTS_FLAG )); then
cd "$SCRIPT_DIR"
export BOREALIS_PROJECT_ROOT="${SCRIPT_DIR}"
if command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="$(command -v python3)"
elif command -v python >/dev/null 2>&1; then
PYTHON_BIN="$(command -v python)"
else
echo -e "${RED}Python interpreter not found. Install Python 3 to run Engine tests.${RESET}" >&2
exit 1
fi
"$PYTHON_BIN" -m pytest Data/Engine/Unit_Tests
exit $?
fi
# ---- Banner ---- # ---- Banner ----
clear || true clear || true
echo -e "${BOREALIS_BLUE}" echo -e "${BOREALIS_BLUE}"

View File

@@ -22,11 +22,11 @@ Lastly, everytime that you complete a stage, you will create a pull request name
- [x] Create domain-focused API blueprints and register_api entry point. - [x] Create domain-focused API blueprints and register_api entry point.
- [x] Mirror route behaviour from the legacy server via service adapters. - [x] Mirror route behaviour from the legacy server via service adapters.
- [x] Add configuration toggles for enabling API groups incrementally. - [x] Add configuration toggles for enabling API groups incrementally.
- [ ] **Stage 4 — Build unit and smoke tests for Engine APIs** - [x] **Stage 4 — Build unit and smoke tests for Engine APIs**
- [ ] Add pytest modules under Data/Engine/Unit_Tests exercising API blueprints. - [x] Add pytest modules under Data/Engine/Unit_Tests exercising API blueprints.
- [ ] Provide fixtures that mirror the legacy SQLite schema and seed data. - [x] Provide fixtures that mirror the legacy SQLite schema and seed data.
- [ ] Assert HTTP status codes, payloads, and side effects for parity. - [x] Assert HTTP status codes, payloads, and side effects for parity.
- [ ] Integrate Engine API tests into CI/local workflows. - [x] Integrate Engine API tests into CI/local workflows.
- [ ] **Stage 5 — Bridge the legacy server to Engine APIs** - [ ] **Stage 5 — Bridge the legacy server to Engine APIs**
- [ ] Delegate API blueprint registration to the Engine factory from the legacy server. - [ ] Delegate API blueprint registration to the Engine factory from the legacy server.
- [ ] Replace legacy API routes with Engine-provided blueprints gated by a flag. - [ ] Replace legacy API routes with Engine-provided blueprints gated by a flag.
@@ -43,5 +43,5 @@ Lastly, everytime that you complete a stage, you will create a pull request name
- [ ] Update legacy server to consume Engine WebSocket registration. - [ ] Update legacy server to consume Engine WebSocket registration.
## Current Status ## Current Status
- **Stage:** Stage 3Introduce API blueprints and service adapters (completed) - **Stage:** Stage 4Build unit and smoke tests for Engine APIs (completed)
- **Active Task:** Awaiting next stage instructions. - **Active Task:** Awaiting next stage instructions.

View File

@@ -0,0 +1 @@
"""Unit and smoke tests for the Borealis Engine runtime."""

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
import pytest
from flask import Flask
from Data.Engine.server import create_app
_SCHEMA_DEFINITION = """
CREATE TABLE IF NOT EXISTS devices (
guid TEXT PRIMARY KEY,
hostname TEXT,
created_at INTEGER,
last_seen INTEGER,
ssl_key_fingerprint TEXT,
token_version INTEGER,
status TEXT,
key_added_at TEXT
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id TEXT PRIMARY KEY,
guid TEXT,
token_hash TEXT,
dpop_jkt TEXT,
created_at TEXT,
expires_at TEXT,
revoked_at TEXT,
last_used_at TEXT
);
CREATE TABLE IF NOT EXISTS enrollment_install_codes (
id TEXT PRIMARY KEY,
code TEXT UNIQUE,
expires_at TEXT,
used_at TEXT,
used_by_guid TEXT,
max_uses INTEGER,
use_count INTEGER,
last_used_at TEXT
);
CREATE TABLE IF NOT EXISTS device_approvals (
id TEXT PRIMARY KEY,
approval_reference TEXT UNIQUE,
guid TEXT,
hostname_claimed TEXT,
ssl_key_fingerprint_claimed TEXT,
enrollment_code_id TEXT,
status TEXT,
client_nonce TEXT,
server_nonce TEXT,
agent_pubkey_der BLOB,
created_at TEXT,
updated_at TEXT,
approved_by_user_id TEXT
);
CREATE TABLE IF NOT EXISTS device_keys (
id TEXT PRIMARY KEY,
guid TEXT,
ssl_key_fingerprint TEXT,
added_at TEXT,
retired_at TEXT
);
"""
@dataclass
class EngineTestHarness:
app: Flask
db_path: Path
bundle_contents: str
def _initialise_legacy_schema(db_path: Path) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path))
try:
conn.executescript(_SCHEMA_DEFINITION)
conn.commit()
finally:
conn.close()
@pytest.fixture()
def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[EngineTestHarness]:
project_root = Path(__file__).resolve().parents[3]
monkeypatch.setenv("BOREALIS_PROJECT_ROOT", str(project_root))
runtime_dir = tmp_path / "runtime"
runtime_dir.mkdir()
cert_root = tmp_path / "certificates"
cert_root.mkdir()
monkeypatch.setenv("BOREALIS_SERVER_ROOT", str(runtime_dir))
monkeypatch.setenv("BOREALIS_CERT_ROOT", str(cert_root))
monkeypatch.setenv("BOREALIS_SERVER_CERT_ROOT", str(cert_root / "Server"))
monkeypatch.setenv("BOREALIS_AGENT_CERT_ROOT", str(cert_root / "Agent"))
db_path = tmp_path / "database" / "engine.sqlite3"
_initialise_legacy_schema(db_path)
tls_dir = tmp_path / "tls"
tls_dir.mkdir()
bundle_contents = "-----BEGIN CERTIFICATE-----\nengine-test\n-----END CERTIFICATE-----\n"
cert_path = tls_dir / "server-cert.pem"
key_path = tls_dir / "server-key.pem"
bundle_path = tls_dir / "server-bundle.pem"
cert_path.write_text(bundle_contents, encoding="utf-8")
key_path.write_text("test-key", encoding="utf-8")
bundle_path.write_text(bundle_contents, encoding="utf-8")
log_path = tmp_path / "logs" / "server.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
config = {
"DATABASE_PATH": str(db_path),
"TLS_CERT_PATH": str(cert_path),
"TLS_KEY_PATH": str(key_path),
"TLS_BUNDLE_PATH": str(bundle_path),
"LOG_FILE": str(log_path),
"API_GROUPS": ("tokens", "enrollment"),
}
app, _socketio, _context = create_app(config)
app.config.update(TESTING=True)
yield EngineTestHarness(app=app, db_path=db_path, bundle_contents=bundle_contents)

View File

@@ -0,0 +1,206 @@
from __future__ import annotations
import base64
import os
import sqlite3
import uuid
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from flask.testing import FlaskClient
from Modules.crypto import keys as crypto_keys
from .conftest import EngineTestHarness
def _now() -> datetime:
return datetime.now(tz=timezone.utc)
def _iso(dt: datetime) -> str:
return dt.astimezone(timezone.utc).isoformat()
def _seed_install_code(db_path: os.PathLike[str], code: str) -> str:
record_id = str(uuid.uuid4())
expires_at = _iso(_now() + timedelta(days=1))
with sqlite3.connect(str(db_path)) as conn:
conn.execute(
"""
INSERT INTO enrollment_install_codes (
id, code, expires_at, used_at, used_by_guid, max_uses, use_count, last_used_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(record_id, code, expires_at, None, None, 1, 0, None),
)
conn.commit()
return record_id
def _generate_agent_material() -> tuple[ed25519.Ed25519PrivateKey, bytes, str]:
private_key = ed25519.Ed25519PrivateKey.generate()
public_der = private_key.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
public_b64 = base64.b64encode(public_der).decode("ascii")
return private_key, public_der, public_b64
def test_enrollment_request_creates_pending_approval(engine_harness: EngineTestHarness) -> None:
harness = engine_harness
client: FlaskClient = harness.app.test_client()
install_code = "INSTALL-CODE-001"
install_code_id = _seed_install_code(harness.db_path, install_code)
private_key, public_der, public_b64 = _generate_agent_material()
client_nonce_bytes = os.urandom(32)
client_nonce_b64 = base64.b64encode(client_nonce_bytes).decode("ascii")
response = client.post(
"/api/agent/enroll/request",
json={
"hostname": "agent-node-01",
"enrollment_code": install_code,
"agent_pubkey": public_b64,
"client_nonce": client_nonce_b64,
},
headers={"X-Borealis-Agent-Context": "interactive"},
)
assert response.status_code == 200
payload = response.get_json()
assert payload["status"] == "pending"
assert payload["server_certificate"] == harness.bundle_contents
approval_reference = payload["approval_reference"]
with sqlite3.connect(str(harness.db_path)) as conn:
cur = conn.cursor()
cur.execute(
"""
SELECT hostname_claimed, ssl_key_fingerprint_claimed, client_nonce, status, enrollment_code_id
FROM device_approvals
WHERE approval_reference = ?
""",
(approval_reference,),
)
row = cur.fetchone()
assert row is not None
hostname_claimed, fingerprint, stored_client_nonce, status, stored_code_id = row
assert hostname_claimed == "agent-node-01"
assert stored_client_nonce == client_nonce_b64
assert status == "pending"
assert stored_code_id == install_code_id
expected_fingerprint = crypto_keys.fingerprint_from_spki_der(public_der)
assert fingerprint == expected_fingerprint
def test_enrollment_poll_finalizes_when_approved(engine_harness: EngineTestHarness) -> None:
harness = engine_harness
client: FlaskClient = harness.app.test_client()
install_code = "INSTALL-CODE-002"
install_code_id = _seed_install_code(harness.db_path, install_code)
private_key, public_der, public_b64 = _generate_agent_material()
client_nonce_bytes = os.urandom(32)
client_nonce_b64 = base64.b64encode(client_nonce_bytes).decode("ascii")
request_response = client.post(
"/api/agent/enroll/request",
json={
"hostname": "agent-node-02",
"enrollment_code": install_code,
"agent_pubkey": public_b64,
"client_nonce": client_nonce_b64,
},
headers={"X-Borealis-Agent-Context": "system"},
)
assert request_response.status_code == 200
request_payload = request_response.get_json()
approval_reference = request_payload["approval_reference"]
server_nonce_b64 = request_payload["server_nonce"]
approved_at = _iso(_now())
with sqlite3.connect(str(harness.db_path)) as conn:
conn.execute(
"""
UPDATE device_approvals
SET status = 'approved',
updated_at = ?,
approved_by_user_id = 'operator'
WHERE approval_reference = ?
""",
(approved_at, approval_reference),
)
conn.commit()
message = base64.b64decode(server_nonce_b64, validate=True) + approval_reference.encode("utf-8") + client_nonce_bytes
proof_sig = private_key.sign(message)
proof_sig_b64 = base64.b64encode(proof_sig).decode("ascii")
poll_response = client.post(
"/api/agent/enroll/poll",
json={
"approval_reference": approval_reference,
"client_nonce": client_nonce_b64,
"proof_sig": proof_sig_b64,
},
)
assert poll_response.status_code == 200
poll_payload = poll_response.get_json()
assert poll_payload["status"] == "approved"
assert poll_payload["token_type"] == "Bearer"
assert poll_payload["server_certificate"] == harness.bundle_contents
final_guid = poll_payload["guid"]
assert isinstance(final_guid, str) and len(final_guid) == 36
with sqlite3.connect(str(harness.db_path)) as conn:
cur = conn.cursor()
cur.execute(
"SELECT guid, status FROM device_approvals WHERE approval_reference = ?",
(approval_reference,),
)
approval_row = cur.fetchone()
cur.execute(
"SELECT hostname, ssl_key_fingerprint, token_version FROM devices WHERE guid = ?",
(final_guid,),
)
device_row = cur.fetchone()
cur.execute(
"SELECT COUNT(*) FROM refresh_tokens WHERE guid = ?",
(final_guid,),
)
refresh_count = cur.fetchone()[0]
cur.execute(
"SELECT use_count, used_by_guid FROM enrollment_install_codes WHERE id = ?",
(install_code_id,),
)
install_row = cur.fetchone()
cur.execute(
"SELECT COUNT(*) FROM device_keys WHERE guid = ?",
(final_guid,),
)
key_count = cur.fetchone()[0]
assert approval_row is not None
approval_guid, approval_status = approval_row
assert approval_status == "completed"
assert approval_guid == final_guid
assert device_row is not None
hostname, fingerprint, token_version = device_row
assert hostname == "agent-node-02"
assert fingerprint == crypto_keys.fingerprint_from_spki_der(public_der)
assert token_version >= 1
assert refresh_count == 1
assert install_row is not None
use_count, used_by_guid = install_row
assert use_count == 1
assert used_by_guid == final_guid
assert key_count == 1

View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import hashlib
import sqlite3
from datetime import datetime, timedelta, timezone
from flask.testing import FlaskClient
from .conftest import EngineTestHarness
def _iso(dt: datetime) -> str:
return dt.astimezone(timezone.utc).isoformat()
def test_refresh_token_success(engine_harness: EngineTestHarness) -> None:
harness = engine_harness
client: FlaskClient = harness.app.test_client()
guid = "54E8C9E2-6B3D-4B51-A456-4ACB94C45F00"
refresh_token = "refresh-token-value"
token_hash = hashlib.sha256(refresh_token.encode("utf-8")).hexdigest()
now = datetime.now(tz=timezone.utc)
expires_at = now + timedelta(days=1)
with sqlite3.connect(str(harness.db_path)) as conn:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO devices (guid, hostname, created_at, last_seen, ssl_key_fingerprint,
token_version, status, key_added_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
guid,
"device-one",
int(now.timestamp()),
int(now.timestamp()),
"fingerprint",
1,
"active",
_iso(now),
),
)
cur.execute(
"""
INSERT INTO refresh_tokens (id, guid, token_hash, created_at, expires_at, revoked_at, last_used_at)
VALUES (?, ?, ?, ?, ?, NULL, NULL)
""",
(
"token-row",
guid,
token_hash,
_iso(now),
_iso(expires_at),
),
)
conn.commit()
response = client.post(
"/api/agent/token/refresh",
json={"guid": guid, "refresh_token": refresh_token},
)
assert response.status_code == 200
payload = response.get_json()
assert payload["token_type"] == "Bearer"
assert payload["expires_in"] == 900
assert isinstance(payload["access_token"], str) and payload["access_token"]
with sqlite3.connect(str(harness.db_path)) as conn:
cur = conn.cursor()
cur.execute(
"SELECT last_used_at, revoked_at FROM refresh_tokens WHERE guid = ?",
(guid,),
)
row = cur.fetchone()
assert row is not None
last_used_at, revoked_at = row
assert last_used_at is not None
assert revoked_at is None
def test_refresh_token_requires_payload(engine_harness: EngineTestHarness) -> None:
client: FlaskClient = engine_harness.app.test_client()
response = client.post(
"/api/agent/token/refresh",
json={"guid": "", "refresh_token": ""},
)
assert response.status_code == 400
payload = response.get_json()
assert payload["error"] == "invalid_request"