First Build alpha 0.1
This commit is contained in:
220
tests/test_customer_api.py
Normal file
220
tests/test_customer_api.py
Normal 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
|
||||
Reference in New Issue
Block a user