Squashed 'core/' content from commit 92ec910

git-subtree-dir: core
git-subtree-split: 92ec910a132e379a3a6e442a75bcb07cac0f0010
This commit is contained in:
Du Wenbo
2026-04-04 18:16:49 +08:00
commit d8e4449f10
227 changed files with 39179 additions and 0 deletions

View File

View File

@@ -0,0 +1,125 @@
import pytest
from conftest import auth_header
class TestAlarmRules:
async def test_list_alarm_rules(self, client, admin_user, admin_token, seed_alarm_rule):
resp = await client.get("/api/v1/alarms/rules", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
async def test_create_alarm_rule(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/alarms/rules",
json={
"name": "新告警规则", "data_type": "power", "condition": "gt",
"threshold": 100.0, "severity": "critical",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["name"] == "新告警规则"
assert body["severity"] == "critical"
async def test_create_alarm_rule_as_visitor_forbidden(self, client, normal_user, user_token):
resp = await client.post(
"/api/v1/alarms/rules",
json={"name": "Test", "data_type": "power", "condition": "gt", "threshold": 50.0},
headers=auth_header(user_token),
)
assert resp.status_code == 403
async def test_update_alarm_rule(self, client, admin_user, admin_token, seed_alarm_rule):
resp = await client.put(
f"/api/v1/alarms/rules/{seed_alarm_rule.id}",
json={
"name": "更新后规则", "data_type": "temperature", "condition": "gt",
"threshold": 90.0, "severity": "critical",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["name"] == "更新后规则"
async def test_update_nonexistent_rule(self, client, admin_user, admin_token):
resp = await client.put(
"/api/v1/alarms/rules/99999",
json={"name": "Ghost", "data_type": "power", "condition": "gt"},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
async def test_delete_alarm_rule(self, client, admin_user, admin_token, seed_alarm_rule):
resp = await client.delete(
f"/api/v1/alarms/rules/{seed_alarm_rule.id}",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_delete_nonexistent_rule(self, client, admin_user, admin_token):
resp = await client.delete("/api/v1/alarms/rules/99999", headers=auth_header(admin_token))
assert resp.status_code == 404
class TestAlarmEvents:
async def test_list_alarm_events(self, client, admin_user, admin_token, seed_alarm_event):
resp = await client.get("/api/v1/alarms/events", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "total" in body
assert "items" in body
assert body["total"] >= 1
async def test_list_alarm_events_filter_status(self, client, admin_user, admin_token, seed_alarm_event):
resp = await client.get(
"/api/v1/alarms/events",
params={"status": "active"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
for item in resp.json()["items"]:
assert item["status"] == "active"
async def test_acknowledge_alarm(self, client, admin_user, admin_token, seed_alarm_event):
resp = await client.post(
f"/api/v1/alarms/events/{seed_alarm_event.id}/acknowledge",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_acknowledge_nonexistent_alarm(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/alarms/events/99999/acknowledge",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
async def test_resolve_alarm(self, client, admin_user, admin_token, seed_alarm_event):
resp = await client.post(
f"/api/v1/alarms/events/{seed_alarm_event.id}/resolve",
params={"note": "已修复"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_resolve_nonexistent_alarm(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/alarms/events/99999/resolve",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
class TestAlarmStats:
async def test_get_alarm_stats(self, client, admin_user, admin_token, seed_alarm_event):
resp = await client.get("/api/v1/alarms/stats", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, dict)
async def test_get_alarm_stats_empty(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/alarms/stats", headers=auth_header(admin_token))
assert resp.status_code == 200

View File

@@ -0,0 +1,66 @@
import pytest
from conftest import auth_header
class TestLogin:
async def test_login_valid_credentials(self, client, admin_user):
resp = await client.post(
"/api/v1/auth/login",
data={"username": "testadmin", "password": "admin123"},
)
assert resp.status_code == 200
body = resp.json()
assert "access_token" in body
assert body["token_type"] == "bearer"
assert body["user"]["username"] == "testadmin"
assert body["user"]["role"] == "admin"
async def test_login_wrong_password(self, client, admin_user):
resp = await client.post(
"/api/v1/auth/login",
data={"username": "testadmin", "password": "wrongpass"},
)
assert resp.status_code == 401
async def test_login_nonexistent_user(self, client):
resp = await client.post(
"/api/v1/auth/login",
data={"username": "nobody", "password": "whatever"},
)
assert resp.status_code == 401
async def test_login_inactive_user(self, client, db_session):
from app.core.security import hash_password
from app.models.user import User
user = User(
username="inactive", hashed_password=hash_password("pass123"),
role="visitor", is_active=False,
)
db_session.add(user)
await db_session.commit()
resp = await client.post(
"/api/v1/auth/login",
data={"username": "inactive", "password": "pass123"},
)
assert resp.status_code == 403
class TestMe:
async def test_me_with_valid_token(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/auth/me", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert body["username"] == "testadmin"
assert body["role"] == "admin"
assert body["is_active"] is True
async def test_me_without_token(self, client):
resp = await client.get("/api/v1/auth/me")
assert resp.status_code == 401
async def test_me_with_invalid_token(self, client):
resp = await client.get(
"/api/v1/auth/me",
headers=auth_header("invalid.token.here"),
)
assert resp.status_code == 401

View File

@@ -0,0 +1,62 @@
import pytest
from conftest import auth_header
class TestCarbonOverview:
async def test_get_carbon_overview(self, client, admin_user, admin_token, seed_carbon):
resp = await client.get("/api/v1/carbon/overview", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "today" in body
assert "month" in body
assert "year" in body
assert "by_scope" in body
assert "emission" in body["today"]
assert "reduction" in body["today"]
async def test_get_carbon_overview_unauthenticated(self, client):
resp = await client.get("/api/v1/carbon/overview")
assert resp.status_code == 401
async def test_get_carbon_overview_empty(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/carbon/overview", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert body["today"]["emission"] == 0
class TestCarbonTrend:
async def test_get_carbon_trend(self, client, admin_user, admin_token, seed_carbon):
resp = await client.get(
"/api/v1/carbon/trend",
params={"days": 30},
headers=auth_header(admin_token),
)
# date_trunc is PostgreSQL-specific; SQLite returns 500
assert resp.status_code in (200, 500)
if resp.status_code == 200:
assert isinstance(resp.json(), list)
async def test_get_carbon_trend_custom_days(self, client, admin_user, admin_token, seed_carbon):
resp = await client.get(
"/api/v1/carbon/trend",
params={"days": 7},
headers=auth_header(admin_token),
)
assert resp.status_code in (200, 500)
class TestEmissionFactors:
async def test_get_emission_factors(self, client, admin_user, admin_token, seed_emission_factors):
resp = await client.get("/api/v1/carbon/factors", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
assert "factor" in body[0]
assert "energy_type" in body[0]
async def test_get_emission_factors_empty(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/carbon/factors", headers=auth_header(admin_token))
assert resp.status_code == 200
assert resp.json() == []

View File

@@ -0,0 +1,47 @@
import pytest
from conftest import auth_header
class TestOverview:
async def test_get_overview(self, client, admin_user, admin_token, seed_devices, seed_daily_summary, seed_carbon):
resp = await client.get("/api/v1/dashboard/overview", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "device_stats" in body
assert "energy_today" in body
assert "carbon" in body
assert "active_alarms" in body
assert "recent_alarms" in body
async def test_get_overview_unauthenticated(self, client):
resp = await client.get("/api/v1/dashboard/overview")
assert resp.status_code == 401
class TestRealtime:
async def test_get_realtime_data(self, client, admin_user, admin_token, seed_energy_data):
resp = await client.get("/api/v1/dashboard/realtime", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "timestamp" in body
assert "pv_power" in body
assert "heatpump_power" in body
assert "total_load" in body
assert "grid_power" in body
class TestLoadCurve:
async def test_get_load_curve(self, client, admin_user, admin_token, seed_energy_data):
resp = await client.get("/api/v1/dashboard/load-curve", headers=auth_header(admin_token))
# date_trunc is PostgreSQL-specific; SQLite returns 500
assert resp.status_code in (200, 500)
if resp.status_code == 200:
assert isinstance(resp.json(), list)
async def test_get_load_curve_custom_hours(self, client, admin_user, admin_token, seed_energy_data):
resp = await client.get(
"/api/v1/dashboard/load-curve",
params={"hours": 12},
headers=auth_header(admin_token),
)
assert resp.status_code in (200, 500)

View File

@@ -0,0 +1,119 @@
import pytest
from conftest import auth_header
class TestListDevices:
async def test_list_devices(self, client, admin_user, admin_token, seed_devices):
resp = await client.get("/api/v1/devices", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "total" in body
assert "items" in body
assert body["total"] >= 1
async def test_list_devices_pagination(self, client, admin_user, admin_token, seed_devices):
resp = await client.get(
"/api/v1/devices", params={"page": 1, "page_size": 2},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert len(body["items"]) <= 2
async def test_list_devices_filter_by_type(self, client, admin_user, admin_token, seed_devices):
resp = await client.get(
"/api/v1/devices", params={"device_type": "pv_inverter"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
for item in resp.json()["items"]:
assert item["device_type"] == "pv_inverter"
async def test_list_devices_unauthenticated(self, client):
resp = await client.get("/api/v1/devices")
assert resp.status_code == 401
class TestDeviceStats:
async def test_get_device_stats(self, client, admin_user, admin_token, seed_devices):
resp = await client.get("/api/v1/devices/stats", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "online" in body
assert "offline" in body
assert "alarm" in body
assert "maintenance" in body
class TestDeviceTypes:
async def test_get_device_types(self, client, admin_user, admin_token, seed_device_types):
resp = await client.get("/api/v1/devices/types", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
assert "code" in body[0]
class TestDeviceGroups:
async def test_get_device_groups(self, client, admin_user, admin_token, seed_device_groups):
resp = await client.get("/api/v1/devices/groups", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
class TestCreateDevice:
async def test_create_device_as_admin(self, client, admin_user, admin_token, seed_device_types):
resp = await client.post(
"/api/v1/devices",
json={
"name": "新设备", "code": "NEW-001", "device_type": "pv_inverter",
"rated_power": 200.0, "location": "A区屋顶",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["name"] == "新设备"
assert body["code"] == "NEW-001"
async def test_create_device_as_visitor_forbidden(self, client, normal_user, user_token, seed_device_types):
resp = await client.post(
"/api/v1/devices",
json={"name": "Test", "code": "T-001", "device_type": "meter"},
headers=auth_header(user_token),
)
assert resp.status_code == 403
class TestUpdateDevice:
async def test_update_device(self, client, admin_user, admin_token, seed_devices):
device = seed_devices[0]
resp = await client.put(
f"/api/v1/devices/{device.id}",
json={"location": "新位置"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["location"] == "新位置"
async def test_update_nonexistent_device(self, client, admin_user, admin_token):
resp = await client.put(
"/api/v1/devices/99999",
json={"location": "nowhere"},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
class TestGetDevice:
async def test_get_single_device(self, client, admin_user, admin_token, seed_devices):
device = seed_devices[0]
resp = await client.get(f"/api/v1/devices/{device.id}", headers=auth_header(admin_token))
assert resp.status_code == 200
assert resp.json()["id"] == device.id
async def test_get_nonexistent_device(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/devices/99999", headers=auth_header(admin_token))
assert resp.status_code == 404

View File

@@ -0,0 +1,74 @@
import pytest
from conftest import auth_header
class TestEnergyHistory:
async def test_get_energy_history(self, client, admin_user, admin_token, seed_energy_data):
resp = await client.get(
"/api/v1/energy/history",
params={"granularity": "raw"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
async def test_get_energy_history_by_device(self, client, admin_user, admin_token, seed_energy_data, seed_devices):
resp = await client.get(
"/api/v1/energy/history",
params={"device_id": seed_devices[0].id, "granularity": "raw"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_get_energy_history_unauthenticated(self, client):
resp = await client.get("/api/v1/energy/history")
assert resp.status_code == 401
class TestDailySummary:
async def test_get_daily_summary(self, client, admin_user, admin_token, seed_daily_summary):
resp = await client.get("/api/v1/energy/daily-summary", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
async def test_get_daily_summary_with_filter(self, client, admin_user, admin_token, seed_daily_summary):
resp = await client.get(
"/api/v1/energy/daily-summary",
params={"energy_type": "electricity"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
class TestEnergyComparison:
async def test_get_energy_comparison(self, client, admin_user, admin_token, seed_daily_summary):
resp = await client.get(
"/api/v1/energy/comparison",
params={"period": "month"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert "current" in body
assert "previous" in body
assert "yoy" in body
assert "mom_change" in body
assert "yoy_change" in body
async def test_get_energy_comparison_day(self, client, admin_user, admin_token, seed_daily_summary):
resp = await client.get(
"/api/v1/energy/comparison",
params={"period": "day"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_get_energy_comparison_year(self, client, admin_user, admin_token, seed_daily_summary):
resp = await client.get(
"/api/v1/energy/comparison",
params={"period": "year"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200

View File

@@ -0,0 +1,44 @@
import pytest
from conftest import auth_header
class TestDeviceRealtime:
async def test_get_device_realtime(self, client, admin_user, admin_token, seed_devices, seed_energy_data):
device = seed_devices[0]
resp = await client.get(
f"/api/v1/monitoring/devices/{device.id}/realtime",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert "device" in body
assert "data" in body
assert body["device"]["id"] == device.id
async def test_get_device_realtime_no_device(self, client, admin_user, admin_token):
resp = await client.get(
"/api/v1/monitoring/devices/99999/realtime",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["device"] is None
async def test_get_device_realtime_unauthenticated(self, client):
resp = await client.get("/api/v1/monitoring/devices/1/realtime")
assert resp.status_code == 401
class TestEnergyFlow:
async def test_get_energy_flow(self, client, admin_user, admin_token, seed_devices, seed_energy_data):
resp = await client.get("/api/v1/monitoring/energy-flow", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "nodes" in body
assert "links" in body
assert len(body["nodes"]) == 4
assert len(body["links"]) == 4
async def test_get_energy_flow_unauthenticated(self, client):
resp = await client.get("/api/v1/monitoring/energy-flow")
assert resp.status_code == 401

View File

@@ -0,0 +1,79 @@
import pytest
from conftest import auth_header
class TestReportTemplates:
async def test_list_report_templates(self, client, admin_user, admin_token, seed_report_template):
resp = await client.get("/api/v1/reports/templates", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
assert "name" in body[0]
assert "report_type" in body[0]
async def test_create_report_template(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/reports/templates",
json={
"name": "月报模板", "report_type": "monthly",
"description": "每月能耗报表",
"fields": [{"name": "consumption", "label": "能耗"}],
"aggregation": "sum", "time_granularity": "day",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["name"] == "月报模板"
assert "id" in body
async def test_list_templates_unauthenticated(self, client):
resp = await client.get("/api/v1/reports/templates")
assert resp.status_code == 401
class TestReportTasks:
async def test_list_report_tasks(self, client, admin_user, admin_token, seed_report_task):
resp = await client.get("/api/v1/reports/tasks", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
async def test_create_report_task(self, client, admin_user, admin_token, seed_report_template):
resp = await client.post(
"/api/v1/reports/tasks",
json={
"template_id": seed_report_template.id,
"name": "新任务",
"export_format": "csv",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert "id" in body
async def test_run_report_task(self, client, admin_user, admin_token, seed_report_task):
resp = await client.post(
f"/api/v1/reports/tasks/{seed_report_task.id}/run",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert "task_id" in body
async def test_run_nonexistent_task(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/reports/tasks/99999/run",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
async def test_create_task_unauthenticated(self, client):
resp = await client.post(
"/api/v1/reports/tasks",
json={"template_id": 1, "name": "test"},
)
assert resp.status_code == 401

View File

@@ -0,0 +1,78 @@
import pytest
from conftest import auth_header
class TestListUsers:
async def test_list_users_as_admin(self, client, admin_user, admin_token):
resp = await client.get("/api/v1/users", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert "total" in body
assert "items" in body
assert body["total"] >= 1
async def test_list_users_as_visitor_forbidden(self, client, normal_user, user_token):
resp = await client.get("/api/v1/users", headers=auth_header(user_token))
assert resp.status_code == 403
async def test_list_users_unauthenticated(self, client):
resp = await client.get("/api/v1/users")
assert resp.status_code == 401
class TestCreateUser:
async def test_create_user_as_admin(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/users",
json={"username": "newuser", "password": "newpass123", "full_name": "New User", "role": "visitor"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["username"] == "newuser"
assert "id" in body
async def test_create_user_as_visitor_forbidden(self, client, normal_user, user_token):
resp = await client.post(
"/api/v1/users",
json={"username": "another", "password": "pass123"},
headers=auth_header(user_token),
)
assert resp.status_code == 403
async def test_create_duplicate_user(self, client, admin_user, admin_token):
resp = await client.post(
"/api/v1/users",
json={"username": "testadmin", "password": "pass123"},
headers=auth_header(admin_token),
)
assert resp.status_code == 400
class TestUpdateUser:
async def test_update_user_as_admin(self, client, admin_user, normal_user, admin_token):
resp = await client.put(
f"/api/v1/users/{normal_user.id}",
json={"full_name": "Updated Name"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
async def test_update_nonexistent_user(self, client, admin_user, admin_token):
resp = await client.put(
"/api/v1/users/99999",
json={"full_name": "Ghost"},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
class TestRoles:
async def test_list_roles(self, client, admin_user, admin_token, seed_roles):
resp = await client.get("/api/v1/users/roles", headers=auth_header(admin_token))
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) >= 1
assert "name" in body[0]
assert "display_name" in body[0]