Add project scaffolding and documentation
Add .env.example with configuration for database, Redis, security, SMTP, workers, and plugins. Add .gitignore for Python, Node.js, Next.js, Docker volumes, and IDE files. Add MIT License. Update README.md with feature overview, quick start guide, architecture description, plugin system documentation, security details, backup/restore instructions, and developer setup. Add Alembic configuration files and placeholder directories for API, web, worker, and plugin components
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from apps.api.src.database import Base, get_db
|
||||
from apps.api.src.main import app
|
||||
|
||||
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost:5432/nexadash_test"
|
||||
|
||||
engine = create_async_engine(TEST_DATABASE_URL, echo=False, future=True)
|
||||
TestingSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session", autouse=True)
|
||||
async def prepare_db():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db():
|
||||
async with TestingSessionLocal() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(db):
|
||||
async def override_get_db():
|
||||
yield db
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
yield TestClient(app)
|
||||
app.dependency_overrides.clear()
|
||||
@@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_response(client):
|
||||
return client.post("/api/v1/auth/setup", json={
|
||||
"email": "owner@nexadash.local",
|
||||
"password": "StrongPassword123!",
|
||||
"first_name": "Owner",
|
||||
"last_name": "User",
|
||||
})
|
||||
|
||||
|
||||
def test_setup_status(client):
|
||||
r = client.get("/api/v1/auth/setup-status")
|
||||
assert r.status_code == 200
|
||||
assert "setup_required" in r.json()
|
||||
|
||||
|
||||
def test_setup(client, setup_response):
|
||||
assert setup_response.status_code in (200, 400)
|
||||
if setup_response.status_code == 200:
|
||||
assert "access_token" in setup_response.json()
|
||||
|
||||
|
||||
def test_login(client, setup_response):
|
||||
r = client.post("/api/v1/auth/login", json={
|
||||
"email": "owner@nexadash.local",
|
||||
"password": "StrongPassword123!",
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert "access_token" in r.json()
|
||||
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
from apps.api.src.plugins import registry
|
||||
from apps.api.src.plugins.connectors.proxmox_ve import ProxmoxVEConnector
|
||||
from apps.api.src.plugins.connectors.adguard_home import AdGuardHomeConnector
|
||||
from apps.api.src.plugins.connectors.generic_http import GenericHTTPConnector
|
||||
|
||||
|
||||
def test_builtin_connectors_registered():
|
||||
assert "proxmox-ve" in registry.list_connectors()
|
||||
assert "adguard-home" in registry.list_connectors()
|
||||
assert "generic-http" in registry.list_connectors()
|
||||
|
||||
|
||||
def test_connector_classes():
|
||||
assert registry.get_connector("proxmox-ve") is ProxmoxVEConnector
|
||||
assert registry.get_connector("adguard-home") is AdGuardHomeConnector
|
||||
assert registry.get_connector("generic-http") is GenericHTTPConnector
|
||||
|
||||
|
||||
def test_registry_missing():
|
||||
assert registry.get_connector("unknown") is None
|
||||
@@ -0,0 +1,29 @@
|
||||
from apps.api.src.security.password import hash_password, verify_password
|
||||
from apps.api.src.security.encryption import encrypt_value, decrypt_value, mask_secret
|
||||
from apps.api.src.security.ssrf import is_safe_url
|
||||
|
||||
|
||||
def test_password_hash():
|
||||
pw = "super-secret-password"
|
||||
hashed = hash_password(pw)
|
||||
assert verify_password(pw, hashed)
|
||||
assert not verify_password("wrong", hashed)
|
||||
|
||||
|
||||
def test_encryption():
|
||||
value = "api-token-secret"
|
||||
encrypted = encrypt_value(value)
|
||||
decrypted = decrypt_value(encrypted)
|
||||
assert decrypted == value
|
||||
|
||||
|
||||
def test_mask_secret():
|
||||
assert mask_secret("1234567890", 4) == "******7890"
|
||||
|
||||
|
||||
def test_ssrf_protection():
|
||||
assert not is_safe_url("http://localhost:8000")
|
||||
assert not is_safe_url("http://127.0.0.1/admin")
|
||||
assert not is_safe_url("http://169.254.169.254")
|
||||
assert not is_safe_url("ftp://example.com")
|
||||
assert is_safe_url("https://example.com/api")
|
||||
Reference in New Issue
Block a user