mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Add Engine API tests for Stage 4
This commit is contained in:
21
Borealis.ps1
21
Borealis.ps1
@@ -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
|
||||||
|
|||||||
19
Borealis.sh
19
Borealis.sh
@@ -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}"
|
||||||
|
|||||||
@@ -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 3 — Introduce API blueprints and service adapters (completed)
|
- **Stage:** Stage 4 — Build unit and smoke tests for Engine APIs (completed)
|
||||||
- **Active Task:** Awaiting next stage instructions.
|
- **Active Task:** Awaiting next stage instructions.
|
||||||
|
|||||||
1
Data/Engine/Unit_Tests/__init__.py
Normal file
1
Data/Engine/Unit_Tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Unit and smoke tests for the Borealis Engine runtime."""
|
||||||
130
Data/Engine/Unit_Tests/conftest.py
Normal file
130
Data/Engine/Unit_Tests/conftest.py
Normal 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)
|
||||||
206
Data/Engine/Unit_Tests/test_enrollment_api.py
Normal file
206
Data/Engine/Unit_Tests/test_enrollment_api.py
Normal 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
|
||||||
92
Data/Engine/Unit_Tests/test_tokens_api.py
Normal file
92
Data/Engine/Unit_Tests/test_tokens_api.py
Normal 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"
|
||||||
Reference in New Issue
Block a user