Files
TimeTracker/tests/test_ldap_setup_wizard.py
T
Dries Peeters 5d4e693a2b Add LDAP setup wizard on Integrations and admin routes
Introduce a guided LDAP configuration wizard mirroring the OIDC flow:
five-step UI with server/TLS, bind, directory layout, groups and
AUTH_METHOD, then optional connection test and .env / Docker Compose
generation for copy-paste deployment.

- Refactor LDAPService.test_connection to accept an optional config
  mapping so the wizard can test draft values without merging live env
  secrets; keep POST /admin/ldap/test on current_app.config.
- Add GET /admin/ldap/setup-wizard plus POST endpoints for test,
  validate, and generate-config (manage_settings, rate limited).
- Surface an LDAP card with status badge and wizard link on the
  integrations list for admins and manage_settings users.
- Add tests for validate, generate, and wizard test delegation.
2026-04-27 20:21:34 +02:00

181 lines
6.2 KiB
Python

"""Tests for LDAP setup wizard admin routes."""
from __future__ import annotations
from unittest.mock import patch
def test_ldap_setup_wizard_requires_login(client):
resp = client.get("/admin/ldap/setup-wizard", follow_redirects=False)
assert resp.status_code in (302, 401)
def test_ldap_setup_wizard_get_as_admin(admin_authenticated_client):
resp = admin_authenticated_client.get("/admin/ldap/setup-wizard")
assert resp.status_code == 200
assert b"LDAP Setup Wizard" in resp.data
def test_ldap_wizard_validate_missing_host(admin_authenticated_client):
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/validate-config",
json={"AUTH_METHOD": "ldap"},
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["valid"] is False
assert any(e.get("field") == "LDAP_HOST" for e in body.get("errors", []))
def test_ldap_wizard_validate_invalid_auth_method(admin_authenticated_client):
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/validate-config",
json={
"LDAP_HOST": "ldap.example.com",
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
"LDAP_BIND_PASSWORD": "x",
"LDAP_BASE_DN": "dc=example,dc=com",
"LDAP_USER_LOGIN_ATTR": "uid",
"AUTH_METHOD": "oidc",
},
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["valid"] is False
def test_ldap_wizard_validate_ok(admin_authenticated_client):
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/validate-config",
json={
"LDAP_HOST": "ldap.example.com",
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
"LDAP_BIND_PASSWORD": "secret",
"LDAP_BASE_DN": "dc=example,dc=com",
"LDAP_USER_LOGIN_ATTR": "uid",
"AUTH_METHOD": "all",
},
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["valid"] is True
assert body["errors"] == []
def test_ldap_wizard_generate_rejects_non_ldap_auth(admin_authenticated_client):
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/generate-config",
json={
"AUTH_METHOD": "oidc",
"LDAP_HOST": "ldap.example.com",
"LDAP_BIND_DN": "cn=x",
"LDAP_BIND_PASSWORD": "p",
"LDAP_BASE_DN": "dc=x",
},
content_type="application/json",
)
assert resp.status_code == 400
def test_ldap_wizard_generate_success(admin_authenticated_client):
payload = {
"AUTH_METHOD": "ldap",
"LDAP_HOST": "ldap.example.com",
"LDAP_PORT": 636,
"LDAP_USE_SSL": True,
"LDAP_USE_TLS": False,
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
"LDAP_BIND_PASSWORD": "s3cret",
"LDAP_BASE_DN": "dc=example,dc=com",
"LDAP_USER_DN": "ou=users",
"LDAP_USER_OBJECT_CLASS": "inetOrgPerson",
"LDAP_USER_LOGIN_ATTR": "uid",
"LDAP_USER_EMAIL_ATTR": "mail",
"LDAP_USER_FNAME_ATTR": "givenName",
"LDAP_USER_LNAME_ATTR": "sn",
"LDAP_GROUP_DN": "ou=groups",
"LDAP_GROUP_OBJECT_CLASS": "groupOfNames",
"LDAP_ADMIN_GROUP": "admins",
"LDAP_REQUIRED_GROUP": "",
"LDAP_TLS_CA_CERT_FILE": "",
"LDAP_TIMEOUT": 15,
}
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/generate-config",
json=payload,
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["success"] is True
assert "AUTH_METHOD=ldap" in body["env_content"]
assert "LDAP_HOST=ldap.example.com" in body["env_content"]
assert "LDAP_USE_SSL=true" in body["env_content"]
assert "LDAP_USE_TLS=false" in body["env_content"]
assert "LDAP_BIND_PASSWORD=s3cret" in body["env_content"]
assert "LDAP_PORT=636" in body["env_content"]
assert "- LDAP_HOST=" in body["docker_compose_content"]
def test_ldap_wizard_generate_requires_host(admin_authenticated_client):
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/generate-config",
json={
"AUTH_METHOD": "ldap",
"LDAP_HOST": "",
"LDAP_BIND_DN": "cn=x",
"LDAP_BIND_PASSWORD": "p",
"LDAP_BASE_DN": "dc=x",
},
content_type="application/json",
)
assert resp.status_code == 400
def test_ldap_wizard_test_connection_uses_service(admin_authenticated_client):
fake = {"success": True, "message": "Connected successfully", "user_count": 2}
with patch("app.services.ldap_service.LDAPService.test_connection", return_value=fake) as m:
resp = admin_authenticated_client.post(
"/admin/ldap/setup-wizard/test-connection",
json={
"LDAP_HOST": "ldap.internal",
"LDAP_PORT": 389,
"LDAP_BIND_DN": "cn=x,dc=y",
"LDAP_BIND_PASSWORD": "pw",
"LDAP_BASE_DN": "dc=y",
},
content_type="application/json",
)
assert resp.status_code == 200
assert resp.get_json() == fake
assert m.called
cfg_arg = m.call_args[0][0]
assert cfg_arg["LDAP_HOST"] == "ldap.internal"
assert cfg_arg["LDAP_BIND_PASSWORD"] == "pw"
def test_ldap_service_test_connection_accepts_cfg(app):
"""Explicit cfg dict is used instead of live app config when provided."""
from app.services.ldap_service import LDAPService
cfg = {
"LDAP_HOST": "draft.example",
"LDAP_PORT": 389,
"LDAP_USE_SSL": False,
"LDAP_USE_TLS": False,
"LDAP_BIND_DN": "",
"LDAP_BIND_PASSWORD": "",
"LDAP_BASE_DN": "dc=x",
"LDAP_USER_DN": "",
"LDAP_USER_OBJECT_CLASS": "inetOrgPerson",
"LDAP_TIMEOUT": 1,
}
with app.app_context():
with patch("app.services.ldap_service._service_connection", return_value=None):
result = LDAPService.test_connection(cfg)
assert result["success"] is False
assert "Could not create LDAP connection" in result["message"]