Package built frontend assets in wheels and force clean upgrades. Run API-triggered updates in a transient unit with a safe delay.
254 lines
9.0 KiB
Python
254 lines
9.0 KiB
Python
import pytest
|
|
|
|
|
|
def test_health_contains_build_info(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
payload = main.health()
|
|
assert payload["status"] == "ok"
|
|
assert payload["name"] == "Importarr"
|
|
assert payload["version"]
|
|
|
|
|
|
def test_build_date_is_rendered_in_local_time(monkeypatch):
|
|
import importarr.build_info as build_info
|
|
|
|
monkeypatch.setenv("TZ", "Europe/Copenhagen")
|
|
import time
|
|
|
|
time.tzset()
|
|
|
|
monkeypatch.setenv("IMPORTARR_BUILD_DATE", "2026-07-29T12:00:00Z")
|
|
|
|
assert build_info.build_info()["build_date"] == "2026-07-29T14:00:00+02:00"
|
|
|
|
|
|
def test_build_info_prefers_installed_release_provenance(monkeypatch):
|
|
import importarr.build_info as build_info
|
|
|
|
monkeypatch.setenv("IMPORTARR_VERSION", "v1.2.3")
|
|
monkeypatch.setenv("IMPORTARR_GIT_SHA", "abc123")
|
|
|
|
assert build_info.build_info()["version"] == "v1.2.3"
|
|
assert build_info.build_info()["git_sha"] == "abc123"
|
|
|
|
|
|
def test_status_contains_service_configuration(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
payload = main.status()
|
|
assert payload["build"]["name"] == "Importarr"
|
|
assert "download_root" in payload
|
|
assert "movies_root" in payload
|
|
assert "tv_root" in payload
|
|
assert "auth_enabled" in payload
|
|
|
|
|
|
def test_index_serves_react_application(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
response = TestClient(main.app).get("/")
|
|
|
|
assert response.status_code == 200
|
|
assert '<div id="root"></div>' in response.text
|
|
assert '/static/assets/app.js' in response.text
|
|
assert '/static/assets/app.css' in response.text
|
|
|
|
|
|
def test_frontend_uses_required_stack_and_capabilities():
|
|
from pathlib import Path
|
|
|
|
app = Path("frontend/src/main.jsx").read_text()
|
|
package = Path("frontend/package.json").read_text()
|
|
|
|
assert "lucide-react" in package
|
|
assert "@radix-ui/react-dialog" in package
|
|
assert "tailwindcss" in package
|
|
for endpoint in ("/api/jobs", "/api/settings", "/api/manual-batches", "/api/control/update", "/api/import/run-now"):
|
|
assert endpoint in app
|
|
|
|
|
|
def test_frontend_defines_exact_light_and_dark_tokens():
|
|
from pathlib import Path
|
|
|
|
css = Path("frontend/src/globals.css").read_text()
|
|
for value in ("#F8FAFC", "#FFFFFF", "#4A43EC", "#7171FF", "#2AD1ED", "#1A1D2E", "#64748B", "#E2E8F0", "#252B42", "#303753", "#23283B", "#42ECF5", "#8B95B7", "#3D4668"):
|
|
assert value in css
|
|
assert ".dark" in css
|
|
|
|
|
|
def test_settings_endpoint_persists_arr_connection_values(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
response = TestClient(main.app).post(
|
|
"/api/settings",
|
|
json={
|
|
"sab_url": "http://sab:8080",
|
|
"sab_api_key": "sab-secret",
|
|
"radarr_url": "http://radarr:7878",
|
|
"radarr_api_key": "radarr-secret",
|
|
"sonarr_url": "http://sonarr:8989",
|
|
"sonarr_api_key": "sonarr-secret",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"sab_url": "http://sab:8080",
|
|
"sab_api_key_configured": True,
|
|
"radarr_url": "http://radarr:7878",
|
|
"radarr_api_key_configured": True,
|
|
"sonarr_url": "http://sonarr:8989",
|
|
"sonarr_api_key_configured": True,
|
|
}
|
|
assert main.settings.sab_api_key == "sab-secret"
|
|
assert main.state.get_app_state("radarr_api_key") == "radarr-secret"
|
|
|
|
|
|
def test_sab_connection_test_reports_success(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
async def fake_queue(self):
|
|
return {"queue": {"slots": [{"name": "one"}, {"name": "two"}]}}
|
|
|
|
monkeypatch.setattr(main.SabnzbdClient, "queue", fake_queue)
|
|
|
|
response = TestClient(main.app).post(
|
|
"/api/settings/test-connection",
|
|
json={"service": "sabnzbd", "url": "http://sab:8080", "api_key": "secret"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ok": True, "service": "sabnzbd", "message": "Connected to SABnzbd; 2 queued jobs visible."}
|
|
|
|
|
|
def test_connection_test_rejects_unknown_service(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
response = TestClient(main.app).post(
|
|
"/api/settings/test-connection",
|
|
json={"service": "lidarr", "url": "http://lidarr:8686"},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_control_endpoints_require_configured_bearer_token(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
monkeypatch.setattr(main.settings, "auth_token", "test-token")
|
|
client = TestClient(main.app)
|
|
|
|
for method, endpoint in (("get", "/api/control/update-check"), ("post", "/api/control/update"), ("post", "/api/control/restart")):
|
|
assert getattr(client, method)(endpoint).status_code == 401
|
|
assert getattr(client, method)(endpoint, headers={"Authorization": "Bearer wrong"}).status_code == 401
|
|
|
|
|
|
def test_write_endpoint_fails_closed_without_token_on_non_loopback_bind(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
from fastapi.testclient import TestClient
|
|
|
|
monkeypatch.setattr(main.settings, "auth_token", None)
|
|
monkeypatch.setattr(main.settings, "bind_host", "0.0.0.0")
|
|
assert TestClient(main.app).post("/api/control/restart").status_code == 503
|
|
|
|
|
|
def test_tokenless_local_development_remains_available(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
monkeypatch.setattr(main.settings, "auth_token", None)
|
|
monkeypatch.setattr(main.settings, "bind_host", "127.0.0.1")
|
|
main.require_write_auth()
|
|
|
|
|
|
def test_update_schedules_exact_latest_release_tag(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
monkeypatch.setattr(main, "check_update_available", lambda: {"status": "update_available", "current_version": "v1.0.0", "latest_version": "v1.2.3", "update_available": True})
|
|
monkeypatch.setattr(main.settings, "update_command", ["/opt/importarr/repo-upgrade.sh"])
|
|
scheduled = []
|
|
monkeypatch.setattr(main, "_schedule_update", scheduled.append)
|
|
|
|
response = main.update_service(expected_tag="v1.2.3")
|
|
assert scheduled == [["/opt/importarr/repo-upgrade.sh", "v1.2.3"]]
|
|
assert response["status"] == "update_scheduled"
|
|
|
|
|
|
def test_update_is_scheduled_outside_service_cgroup(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
|
|
calls = []
|
|
|
|
def fake_run(command, **kwargs):
|
|
calls.append((command, kwargs))
|
|
return main.subprocess.CompletedProcess(command, 0, stdout="Running as unit", stderr="")
|
|
|
|
monkeypatch.setattr(main.subprocess, "run", fake_run)
|
|
main._schedule_update(["/bin/sh", "/opt/importarr/repo-upgrade.sh", "v1.2.3"])
|
|
|
|
assert calls[0][0] == [
|
|
"systemd-run",
|
|
"--unit=importarr-update",
|
|
"--collect",
|
|
"--no-block",
|
|
"--on-active=2s",
|
|
"--",
|
|
"/bin/sh",
|
|
"/opt/importarr/repo-upgrade.sh",
|
|
"v1.2.3",
|
|
]
|
|
|
|
|
|
def test_update_endpoint_requires_expected_tag(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
from fastapi.testclient import TestClient
|
|
|
|
monkeypatch.setattr(main.settings, "auth_token", "test-token")
|
|
monkeypatch.setattr(main, "check_update_available", lambda: pytest.fail("release lookup must not run"))
|
|
|
|
response = TestClient(main.app).post(
|
|
"/api/control/update",
|
|
headers={"Authorization": "Bearer test-token"},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"][0]["loc"] == ["query", "expected_tag"]
|
|
|
|
|
|
def test_update_rejects_unexpected_latest_release(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
|
import importarr.main as main
|
|
from fastapi import HTTPException
|
|
|
|
monkeypatch.setattr(main, "check_update_available", lambda: {"latest_version": "v1.2.4", "update_available": True})
|
|
scheduled = []
|
|
monkeypatch.setattr(main, "_schedule_update", scheduled.append)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
main.update_service(expected_tag="v1.2.3")
|
|
|
|
assert exc_info.value.status_code == 409
|
|
assert scheduled == []
|