chore: prepare repository for public release
This commit is contained in:
+10
-10
@@ -460,7 +460,7 @@ To validate the full configured-YOLO runtime path against Docker/Tower after a
|
||||
model is mounted and selected, run:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_model_asset_detection_workflow.sh http://192.168.10.150:1202
|
||||
bash scripts/verify_model_asset_detection_workflow.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The smoke uses the existing demo raster to generate a tile manifest, selects a
|
||||
@@ -477,7 +477,7 @@ the runtime host, then run:
|
||||
```bash
|
||||
REAL_RASTER_PATH=/mnt/user/appdata/geointel/data/orthophoto.tif \
|
||||
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/data/reference-buildings.geojson \
|
||||
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.168.10.150:1202
|
||||
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
This smoke refuses missing/unsupported inputs, uploads the raster and reference
|
||||
@@ -556,7 +556,7 @@ For model-quality calibration, run the confidence sweep wrapper:
|
||||
REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_orthophoto_wms_512.tif \
|
||||
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_grb_gbg_buildings.geojson \
|
||||
CALIBRATION_THRESHOLDS="0.50 0.35 0.25 0.15" \
|
||||
bash scripts/run_detection_calibration_sweep.sh http://192.168.10.150:1202
|
||||
bash scripts/run_detection_calibration_sweep.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The sweep creates one real persisted workflow run per threshold, fetches the
|
||||
@@ -576,7 +576,7 @@ QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
|
||||
QUALITY_TILE_SIZES="512 640" \
|
||||
QUALITY_TILE_OVERLAPS="64" \
|
||||
QUALITY_THRESHOLDS="0.50 0.15" \
|
||||
bash scripts/run_detection_quality_matrix.sh http://192.168.10.150:1202
|
||||
bash scripts/run_detection_quality_matrix.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The matrix creates one real persisted workflow run per combination and writes
|
||||
@@ -594,7 +594,7 @@ QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
|
||||
QUALITY_TILE_SIZES="512 640" \
|
||||
QUALITY_TILE_OVERLAPS="64" \
|
||||
QUALITY_THRESHOLDS="0.50 0.15" \
|
||||
bash scripts/run_multi_sample_detection_quality_matrix.sh http://192.168.10.150:1202
|
||||
bash scripts/run_multi_sample_detection_quality_matrix.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The combined `multi_sample_quality_summary.json` reports per-sample and overall
|
||||
@@ -611,7 +611,7 @@ QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8n-expanded160e50-pt geointel-bu
|
||||
QUALITY_TILE_SIZES="640" \
|
||||
QUALITY_TILE_OVERLAPS="64" \
|
||||
QUALITY_THRESHOLDS="0.25 0.15 0.05" \
|
||||
bash scripts/run_operator_hard_negative_detection_matrix.sh http://192.168.10.150:1202
|
||||
bash scripts/run_operator_hard_negative_detection_matrix.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
This path uploads only background rasters, runs configured-YOLO detection and
|
||||
@@ -624,7 +624,7 @@ evidence bundle:
|
||||
|
||||
```bash
|
||||
CALIBRATION_SUMMARY_PATH=/mnt/user/appdata/geointel/artifacts/detection-calibration/20260707T002103Z/calibration_summary.json \
|
||||
bash scripts/export_detection_calibration_evidence.sh http://192.168.10.150:1202
|
||||
bash scripts/export_detection_calibration_evidence.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The bundle writes combined QA evidence GeoJSON plus a standalone HTML/SVG review
|
||||
@@ -738,7 +738,7 @@ python scripts/seed_demo_workflow.py --json
|
||||
In Docker Compose on a LAN host:
|
||||
|
||||
```bash
|
||||
curl -X POST http://192.168.10.150:1202/api/v1/demo/workflow
|
||||
curl -X POST http://192.0.2.10:1202/api/v1/demo/workflow
|
||||
```
|
||||
|
||||
### QA/QC result listing
|
||||
@@ -823,7 +823,7 @@ After rebuilding a Docker/LAN deployment, verify the end-to-end demo and export
|
||||
flow through the browser-facing frontend proxy:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202
|
||||
bash scripts/verify_demo_export_workflow.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The script seeds the explicit demo workflow, verifies persisted QA/QC results,
|
||||
@@ -886,7 +886,7 @@ bash scripts/verify_gis_runtime.sh http://localhost:1202
|
||||
On a NAS or server host, use the published LAN URL:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202
|
||||
bash scripts/verify_gis_runtime.sh http://192.0.2.10:1202
|
||||
```
|
||||
|
||||
The script calls `/api/v1/system/capabilities` through the frontend proxy and
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from ipaddress import ip_address, ip_network
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -20,6 +21,43 @@ router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
COOKIE_NAME = "geointel_session"
|
||||
OIDC_FLOW_COOKIE_NAME = "geointel_oidc_flow"
|
||||
logger = logging.getLogger("geointel.auth")
|
||||
_TRUSTED_PROXY_NETWORKS = (
|
||||
ip_network("127.0.0.0/8"),
|
||||
ip_network("::1/128"),
|
||||
ip_network("172.16.0.0/12"),
|
||||
)
|
||||
|
||||
|
||||
def _peer_is_trusted_proxy(request: Request) -> bool:
|
||||
if request.client is None:
|
||||
return False
|
||||
try:
|
||||
peer_address = ip_address(request.client.host)
|
||||
except ValueError:
|
||||
return False
|
||||
return any(peer_address in network for network in _TRUSTED_PROXY_NETWORKS)
|
||||
|
||||
|
||||
def _request_is_https(request: Request) -> bool:
|
||||
if request.url.scheme == "https":
|
||||
return True
|
||||
if not _peer_is_trusted_proxy(request):
|
||||
return False
|
||||
forwarded_proto = request.headers.get("x-forwarded-proto", "").split(",", 1)[0].strip().lower()
|
||||
return forwarded_proto == "https"
|
||||
|
||||
|
||||
def _client_host(request: Request) -> str:
|
||||
peer = request.client.host if request.client else "unknown"
|
||||
if not _peer_is_trusted_proxy(request):
|
||||
return peer
|
||||
forwarded = request.headers.get("x-real-ip", "").strip()
|
||||
if not forwarded:
|
||||
return peer
|
||||
try:
|
||||
return str(ip_address(forwarded))
|
||||
except ValueError:
|
||||
return peer
|
||||
|
||||
|
||||
def _session_from_principal(
|
||||
@@ -73,13 +111,12 @@ def _set_session_cookie(
|
||||
token: str,
|
||||
max_age: int,
|
||||
) -> None:
|
||||
forwarded_proto = request.headers.get("x-forwarded-proto", "").split(",", 1)[0].strip().lower()
|
||||
response.set_cookie(
|
||||
key=COOKIE_NAME,
|
||||
value=token,
|
||||
max_age=max_age,
|
||||
httponly=True,
|
||||
secure=forwarded_proto == "https" or request.url.scheme == "https",
|
||||
secure=_request_is_https(request),
|
||||
samesite="strict",
|
||||
path="/",
|
||||
)
|
||||
@@ -99,7 +136,13 @@ def login(payload: AuthLoginRequest, request: Request, response: Response) -> Au
|
||||
message="Operator authentication is not enabled on this runtime",
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
client_host = request.client.host if request.client else "unknown"
|
||||
if settings.auth_require_https and not _request_is_https(request):
|
||||
raise AppError(
|
||||
code="AUTH_HTTPS_REQUIRED",
|
||||
message="Operator authentication requires HTTPS on this runtime",
|
||||
status_code=status.HTTP_426_UPGRADE_REQUIRED,
|
||||
)
|
||||
client_host = _client_host(request)
|
||||
throttle_key = f"{client_host}:{payload.username.casefold()}"
|
||||
retry_after = AuthService.retry_after_seconds(throttle_key)
|
||||
if retry_after:
|
||||
|
||||
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
|
||||
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
|
||||
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
|
||||
auth_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AUTH_ENABLED")
|
||||
auth_require_https: bool = Field(default=False, validation_alias="GEOINTEL_AUTH_REQUIRE_HTTPS")
|
||||
auth_username: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_USERNAME")
|
||||
auth_password_hash: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_PASSWORD_HASH")
|
||||
auth_session_secret: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_SESSION_SECRET")
|
||||
@@ -39,7 +40,7 @@ class Settings(BaseSettings):
|
||||
validation_alias="GEOINTEL_AUTH_SESSION_TTL_SECONDS",
|
||||
)
|
||||
guest_access_enabled: bool = Field(
|
||||
default=True,
|
||||
default=False,
|
||||
validation_alias="GEOINTEL_GUEST_ACCESS_ENABLED",
|
||||
)
|
||||
guest_display_name: str = Field(
|
||||
|
||||
@@ -20,23 +20,30 @@ from app.services.segmentation_service import SegmentationService
|
||||
from app.services.job_service import JobService
|
||||
|
||||
|
||||
def auth_client(monkeypatch, *, guest_access: bool = False) -> TestClient:
|
||||
def auth_client(
|
||||
monkeypatch,
|
||||
*,
|
||||
guest_access: bool = False,
|
||||
require_https: bool = False,
|
||||
base_url: str = "http://testserver",
|
||||
) -> TestClient:
|
||||
password_hash = AuthService.hash_password(
|
||||
"correct horse battery staple",
|
||||
salt=b"geointel-test-salt",
|
||||
iterations=100_000,
|
||||
)
|
||||
monkeypatch.setenv("GEOINTEL_AUTH_ENABLED", "true")
|
||||
monkeypatch.setenv("GEOINTEL_AUTH_REQUIRE_HTTPS", "true" if require_https else "false")
|
||||
monkeypatch.setenv("GEOINTEL_AUTH_USERNAME", "operator")
|
||||
monkeypatch.setenv("GEOINTEL_AUTH_PASSWORD_HASH", password_hash)
|
||||
monkeypatch.setenv("GEOINTEL_AUTH_SESSION_SECRET", "test-session-secret-that-is-long-enough")
|
||||
monkeypatch.setenv("GEOINTEL_GUEST_ACCESS_ENABLED", "true" if guest_access else "false")
|
||||
monkeypatch.setenv("GEOINTEL_GUEST_DISPLAY_NAME", "Gast")
|
||||
monkeypatch.setenv("GEOINTEL_GUEST_SESSION_TTL_SECONDS", "7200")
|
||||
return TestClient(create_app())
|
||||
return TestClient(create_app(), base_url=base_url)
|
||||
|
||||
|
||||
def test_guest_access_defaults_on_when_operator_authentication_is_enabled(monkeypatch) -> None:
|
||||
def test_guest_access_defaults_off_when_operator_authentication_is_enabled(monkeypatch) -> None:
|
||||
password_hash = AuthService.hash_password(
|
||||
"correct horse battery staple",
|
||||
salt=b"geointel-test-salt",
|
||||
@@ -53,7 +60,7 @@ def test_guest_access_defaults_on_when_operator_authentication_is_enabled(monkey
|
||||
|
||||
assert session.status_code == 200
|
||||
assert session.json()["data"]["authentication_required"] is True
|
||||
assert session.json()["data"]["guest_access_enabled"] is True
|
||||
assert session.json()["data"]["guest_access_enabled"] is False
|
||||
|
||||
|
||||
def test_guest_default_is_inactive_but_valid_when_operator_authentication_is_disabled(monkeypatch) -> None:
|
||||
@@ -135,6 +142,35 @@ def test_login_uses_http_only_session_cookie_and_logout_revokes_browser_access(m
|
||||
assert protected_after_logout.status_code == 401
|
||||
|
||||
|
||||
def test_operator_login_can_require_https(monkeypatch) -> None:
|
||||
insecure_client = auth_client(monkeypatch, require_https=True)
|
||||
rejected = insecure_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "operator", "password": "correct horse battery staple"},
|
||||
)
|
||||
spoofed = insecure_client.post(
|
||||
"/api/v1/auth/login",
|
||||
headers={"x-forwarded-proto": "https", "x-real-ip": "203.0.113.9"},
|
||||
json={"username": "operator", "password": "correct horse battery staple"},
|
||||
)
|
||||
|
||||
secure_client = auth_client(
|
||||
monkeypatch,
|
||||
require_https=True,
|
||||
base_url="https://testserver",
|
||||
)
|
||||
accepted = secure_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "operator", "password": "correct horse battery staple"},
|
||||
)
|
||||
|
||||
assert rejected.status_code == 426
|
||||
assert rejected.json()["error"] == "AUTH_HTTPS_REQUIRED"
|
||||
assert spoofed.status_code == 426
|
||||
assert accepted.status_code == 200
|
||||
assert "secure" in accepted.headers["set-cookie"].lower()
|
||||
|
||||
|
||||
def test_guest_login_exposes_models_but_rejects_management_and_cross_project_requests(monkeypatch) -> None:
|
||||
project_id = UUID("00000000-0000-0000-0000-000000000123")
|
||||
demo = DemoWorkflowResponse(
|
||||
@@ -460,6 +496,10 @@ def test_unraid_runtime_carries_only_hashed_operator_credentials() -> None:
|
||||
assert "GEOINTEL_AUTH_PASSWORD_HASH=" in example
|
||||
assert "GEOINTEL_AUTHENTIK_CLIENT_SECRET=" in example
|
||||
assert "GEOINTEL_AUTH_PASSWORD=" not in runner
|
||||
assert "GEOINTEL_GUEST_ACCESS_ENABLED=true" in example
|
||||
assert 'GEOINTEL_GUEST_ACCESS_ENABLED="${GEOINTEL_GUEST_ACCESS_ENABLED:-true}"' in runner
|
||||
assert "GEOINTEL_AUTH_ENABLED=true" in example
|
||||
assert "GEOINTEL_AUTH_REQUIRE_HTTPS=true" in example
|
||||
assert "GEOINTEL_GUEST_ACCESS_ENABLED=false" in example
|
||||
assert 'GEOINTEL_AUTH_ENABLED="${GEOINTEL_AUTH_ENABLED:-true}"' in runner
|
||||
assert 'GEOINTEL_AUTH_REQUIRE_HTTPS="${GEOINTEL_AUTH_REQUIRE_HTTPS:-true}"' in runner
|
||||
assert 'GEOINTEL_GUEST_ACCESS_ENABLED="${GEOINTEL_GUEST_ACCESS_ENABLED:-false}"' in runner
|
||||
assert "/api/v1/auth/session" in browser_smoke
|
||||
|
||||
@@ -128,8 +128,8 @@ def test_compose_exposes_frontend_on_configurable_host_port_with_cors_origin() -
|
||||
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
|
||||
assert '"${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose
|
||||
assert '"${GEOINTEL_BACKEND_PORT:-8000}:8000"' in compose
|
||||
assert '"${GEOINTEL_BIND_ADDRESS:-127.0.0.1}:${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose
|
||||
assert '"${GEOINTEL_BIND_ADDRESS:-127.0.0.1}:${GEOINTEL_BACKEND_PORT:-8000}:8000"' in compose
|
||||
assert "CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}" in compose
|
||||
assert "GEOINTEL_FRONTEND_PORT=1202" in env_example
|
||||
assert "GEOINTEL_BACKEND_PORT=8000" in env_example
|
||||
@@ -137,6 +137,18 @@ def test_compose_exposes_frontend_on_configurable_host_port_with_cors_origin() -
|
||||
assert "http://127.0.0.1:1202" in env_example
|
||||
|
||||
|
||||
def test_packaged_runtime_uses_fail_closed_authentication_defaults() -> None:
|
||||
compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
|
||||
env_example = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8")
|
||||
|
||||
assert "GEOINTEL_AUTH_ENABLED: ${GEOINTEL_AUTH_ENABLED:-true}" in compose
|
||||
assert "GEOINTEL_AUTH_REQUIRE_HTTPS: ${GEOINTEL_AUTH_REQUIRE_HTTPS:-true}" in compose
|
||||
assert "GEOINTEL_GUEST_ACCESS_ENABLED: ${GEOINTEL_GUEST_ACCESS_ENABLED:-false}" in compose
|
||||
assert "GEOINTEL_AUTH_ENABLED=true" in env_example
|
||||
assert "GEOINTEL_AUTH_REQUIRE_HTTPS=true" in env_example
|
||||
assert "GEOINTEL_GUEST_ACCESS_ENABLED=false" in env_example
|
||||
|
||||
|
||||
def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> None:
|
||||
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestUrlShape:
|
||||
"http://127.0.0.1:8000/internal",
|
||||
"http://localhost/internal",
|
||||
"http://10.1.2.3/internal",
|
||||
"http://192.168.10.150/internal",
|
||||
"http://192.168.123.45/internal",
|
||||
"http://172.16.0.9/internal",
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
"http://[::1]/internal",
|
||||
@@ -185,7 +185,7 @@ class TestTheGuardIsWiredIntoAcquisition:
|
||||
from app.services.orthophoto_acquisition_service import OrthophotoAcquisitionService
|
||||
|
||||
with pytest.raises(AppError) as exc_info:
|
||||
OrthophotoAcquisitionService._fetch("http://192.168.10.150/wms", self._settings())
|
||||
OrthophotoAcquisitionService._fetch("http://192.168.123.45/wms", self._settings())
|
||||
|
||||
assert exc_info.value.code == "OUTBOUND_URL_NOT_ALLOWED"
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_publication_hygiene_gate_passes_for_tracked_tree() -> None:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "check_repository_hygiene.py")],
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_checkpoint_inspector_uses_weights_only_deserialization() -> None:
|
||||
inspector = (ROOT / "scripts" / "inspect_torch_checkpoint.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "weights_only=True" in inspector
|
||||
assert "weights_only=False" not in inspector
|
||||
@@ -21,5 +21,5 @@ def test_evidence_bundle_script_accepts_browser_calibration_summary_export() ->
|
||||
assert "root_project_id = summary.get(\"project_id\")" in script
|
||||
assert "quality_check_ids" in script
|
||||
assert "Browser Detection Lab calibration summary" in readme
|
||||
assert "bash scripts/export_detection_calibration_evidence.sh http://192.168.10.150:1202 ./detection-calibration-summary.json" in readme
|
||||
assert "bash scripts/export_detection_calibration_evidence.sh http://192.168.123.45:1202 ./detection-calibration-summary.json" in readme
|
||||
assert "[x] Allow the evidence bundle script to consume Detection Lab calibration summary exports" in todo
|
||||
|
||||
@@ -279,7 +279,7 @@ def test_stage_and_apply_commands_are_separate_and_local_only(tmp_path: Path) ->
|
||||
assert "--force" not in apply and "--fetch-only" not in apply
|
||||
assert stage[stage.index("--archive-url") + 1] == release.archive_url
|
||||
with pytest.raises(RuntimeError, match="inside GeoIntel"):
|
||||
MANAGER.internal_base_url("http://192.168.10.150:1202/api/v1")
|
||||
MANAGER.internal_base_url("http://192.168.123.45:1202/api/v1")
|
||||
|
||||
|
||||
def test_staged_plan_binds_source_schema_codelist_scope_and_baseline(tmp_path: Path) -> None:
|
||||
|
||||
@@ -388,7 +388,7 @@ def test_approved_upload_uses_canonical_dataset_route_and_complete_provenance(
|
||||
def test_apply_target_must_be_loopback_api() -> None:
|
||||
assert MANAGER.internal_api_url("http://127.0.0.1:8000/api/v1").endswith("/api/v1")
|
||||
with pytest.raises(RuntimeError, match="local /api/v1"):
|
||||
MANAGER.internal_api_url("http://192.168.10.150:1202/api/v1")
|
||||
MANAGER.internal_api_url("http://192.168.123.45:1202/api/v1")
|
||||
with pytest.raises(RuntimeError, match="local /api/v1"):
|
||||
MANAGER.internal_api_url("http://127.0.0.1:8000/not-api")
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_unraid_template_documents_editable_runtime_settings() -> None:
|
||||
assert "Belgium and Belgian North Sea workbench" in template
|
||||
assert "<Repository>geointel-all-in-one:latest</Repository>" in template
|
||||
assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
|
||||
assert "<Icon>http://192.168.10.150:1202/geointel-icon.png</Icon>" in template
|
||||
assert "gitea.example.com/owner/geointel/raw/branch/main" in template
|
||||
assert "<ExtraParams>--add-host=host.docker.internal:host-gateway</ExtraParams>" in template
|
||||
assert 'Target="80"' in template
|
||||
assert 'Target="/app/storage"' in template
|
||||
@@ -36,6 +36,9 @@ def test_unraid_env_template_matches_single_container_compose_variables() -> Non
|
||||
"GEOINTEL_POSTGRES_PASSWORD",
|
||||
"GEOINTEL_CORS_ORIGINS",
|
||||
"GEOINTEL_MAX_UPLOAD_MB",
|
||||
"GEOINTEL_AUTH_ENABLED",
|
||||
"GEOINTEL_AUTH_REQUIRE_HTTPS",
|
||||
"GEOINTEL_GUEST_ACCESS_ENABLED",
|
||||
):
|
||||
assert key in compose
|
||||
assert f"{key}=" in env_template
|
||||
|
||||
Reference in New Issue
Block a user