First Build alpha 0.1

This commit is contained in:
2026-02-07 12:18:20 +01:00
parent 29e83436b2
commit 42a3cc9d9f
36 changed files with 4982 additions and 51 deletions

220
tests/test_customer_api.py Normal file
View File

@@ -0,0 +1,220 @@
"""Unit and API tests for customer management."""
import os
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
os.environ["SECRET_KEY"] = "test-secret-key-for-unit-tests"
os.environ["DATABASE_PATH"] = ":memory:"
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
from app.models import Customer, User, SystemConfig
from app.utils.security import hash_password, encrypt_value
from app.dependencies import get_current_user
# ---------------------------------------------------------------------------
# Test fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def test_db():
"""Create a test database."""
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
@event.listens_for(engine, "connect")
def _set_pragma(dbapi_conn, _):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
session = Session()
# Seed data
admin = User(username="admin", password_hash=hash_password("testpassword123"), email="admin@test.com")
session.add(admin)
config = SystemConfig(
id=1,
base_domain="test.example.com",
admin_email="admin@test.com",
npm_api_url="http://localhost:81/api",
npm_api_token_encrypted=encrypt_value("test-npm-token"),
)
session.add(config)
session.commit()
yield session
session.close()
@pytest.fixture()
def client(test_db):
"""Create a test client with overridden dependencies."""
admin = test_db.query(User).filter(User.username == "admin").first()
def override_get_db():
yield test_db
def override_get_user():
return admin
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_user
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestCustomerList:
"""Tests for GET /api/customers."""
def test_empty_list(self, client: TestClient):
"""List returns empty when no customers exist."""
resp = client.get("/api/customers")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 0
assert data["items"] == []
def test_list_with_customers(self, client: TestClient, test_db):
"""List returns customers after creating them."""
for i in range(3):
test_db.add(Customer(name=f"Customer {i}", subdomain=f"cust{i}", email=f"c{i}@test.com"))
test_db.commit()
resp = client.get("/api/customers")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 3
assert len(data["items"]) == 3
def test_search_filter(self, client: TestClient, test_db):
"""Search filters customers by name/subdomain/email."""
test_db.add(Customer(name="Alpha Corp", subdomain="alpha", email="alpha@test.com"))
test_db.add(Customer(name="Beta Inc", subdomain="beta", email="beta@test.com"))
test_db.commit()
resp = client.get("/api/customers?search=alpha")
data = resp.json()
assert data["total"] == 1
assert data["items"][0]["name"] == "Alpha Corp"
def test_status_filter(self, client: TestClient, test_db):
"""Status filter returns only matching customers."""
test_db.add(Customer(name="Active", subdomain="active1", email="a@t.com", status="active"))
test_db.add(Customer(name="Error", subdomain="error1", email="e@t.com", status="error"))
test_db.commit()
resp = client.get("/api/customers?status=error")
data = resp.json()
assert data["total"] == 1
assert data["items"][0]["status"] == "error"
class TestCustomerCreate:
"""Tests for POST /api/customers."""
@patch("app.services.netbird_service.deploy_customer", new_callable=AsyncMock)
def test_create_customer(self, mock_deploy, client: TestClient):
"""Creating a customer returns 201 and triggers deployment."""
mock_deploy.return_value = {"success": True, "setup_url": "https://new.test.example.com"}
resp = client.post("/api/customers", json={
"name": "New Customer",
"subdomain": "newcust",
"email": "new@test.com",
})
assert resp.status_code == 201
data = resp.json()
assert data["name"] == "New Customer"
assert data["subdomain"] == "newcust"
def test_duplicate_subdomain(self, client: TestClient, test_db):
"""Duplicate subdomain returns 409."""
test_db.add(Customer(name="Existing", subdomain="taken", email="e@test.com"))
test_db.commit()
resp = client.post("/api/customers", json={
"name": "Another",
"subdomain": "taken",
"email": "a@test.com",
})
assert resp.status_code == 409
def test_invalid_subdomain(self, client: TestClient):
"""Invalid subdomain format returns 422."""
resp = client.post("/api/customers", json={
"name": "Bad",
"subdomain": "UPPER_CASE!",
"email": "b@test.com",
})
assert resp.status_code == 422
def test_invalid_email(self, client: TestClient):
"""Invalid email returns 422."""
resp = client.post("/api/customers", json={
"name": "Bad Email",
"subdomain": "bademail",
"email": "not-an-email",
})
assert resp.status_code == 422
class TestCustomerDetail:
"""Tests for GET/PUT/DELETE /api/customers/{id}."""
def test_get_customer(self, client: TestClient, test_db):
"""Get customer returns full details."""
cust = Customer(name="Detail Test", subdomain="detail", email="d@test.com")
test_db.add(cust)
test_db.commit()
test_db.refresh(cust)
resp = client.get(f"/api/customers/{cust.id}")
assert resp.status_code == 200
assert resp.json()["name"] == "Detail Test"
def test_get_nonexistent(self, client: TestClient):
"""Get nonexistent customer returns 404."""
resp = client.get("/api/customers/999")
assert resp.status_code == 404
def test_update_customer(self, client: TestClient, test_db):
"""Update customer fields."""
cust = Customer(name="Before", subdomain="update1", email="u@test.com")
test_db.add(cust)
test_db.commit()
test_db.refresh(cust)
resp = client.put(f"/api/customers/{cust.id}", json={"name": "After"})
assert resp.status_code == 200
assert resp.json()["name"] == "After"
@patch("app.services.netbird_service.undeploy_customer", new_callable=AsyncMock)
def test_delete_customer(self, mock_undeploy, client: TestClient, test_db):
"""Delete customer returns success."""
mock_undeploy.return_value = {"success": True}
cust = Customer(name="ToDelete", subdomain="del1", email="del@test.com")
test_db.add(cust)
test_db.commit()
test_db.refresh(cust)
resp = client.delete(f"/api/customers/{cust.id}")
assert resp.status_code == 200
# Verify deleted
resp = client.get(f"/api/customers/{cust.id}")
assert resp.status_code == 404