"""Tests for furtka.https — fingerprint extraction + force-HTTPS toggle. The fingerprint case uses a throwaway self-signed EC cert with a known reference fingerprint (computed once via `openssl x509 -fingerprint -sha256 -noout`) so we verify the PEM → DER → SHA256 path without a runtime subprocess dependency. The toggle cases stub the caddy reload so we assert the snippet file is written / removed and that reload failures roll state back. """ import subprocess import pytest from furtka import https # Self-signed test-only cert. Don't trust it anywhere; it's here because # we need a real PEM whose fingerprint we can pre-compute. _TEST_CERT_PEM = """-----BEGIN CERTIFICATE----- MIIBjjCCATOgAwIBAgIUGIKx2BGMvNQwAcZvjwJiaJO1GvEwCgYIKoZIzj0EAwIw HDEaMBgGA1UEAwwRRnVydGthIFRlc3QgTG9jYWwwHhcNMjYwNDE3MTAxNTMxWhcN MzYwNDE0MTAxNTMxWjAcMRowGAYDVQQDDBFGdXJ0a2EgVGVzdCBMb2NhbDBZMBMG ByqGSM49AgEGCCqGSM49AwEHA0IABIfWX2oVXrw+iv4lCcIIceoX24bvRdlEECB5 QoMYphmlOoI492tRCGHxA8eaIwIYqFn1DzBKBRSL0H3xcu+4Pg6jUzBRMB0GA1Ud DgQWBBSMizCL5Kh+SLE5n12oKV05L9bJXjAfBgNVHSMEGDAWgBSMizCL5Kh+SLE5 n12oKV05L9bJXjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDp 6etGEuj7AGD5zzyzDSpmRiMEgBp1k6fVoLYW7N2K3AIhAK8khUp3gKPo4UqtWNK9 Cs/B0mzRy2MUPGdZ5QU6LoDz -----END CERTIFICATE----- """ _TEST_CERT_FP_SHA256 = ( "40:A7:98:2E:8D:1F:4C:0D:9B:E6:87:ED:91:FA:6F:B1:" "3D:8A:10:06:79:7C:08:A9:8F:AD:71:0C:B8:29:87:28" ) def test_ca_fingerprint_matches_openssl(tmp_path): cert = tmp_path / "root.crt" cert.write_text(_TEST_CERT_PEM) fp_hex = https._ca_fingerprint(cert) assert fp_hex is not None assert https._format_fingerprint(fp_hex) == _TEST_CERT_FP_SHA256 def test_ca_fingerprint_missing_file(tmp_path): assert https._ca_fingerprint(tmp_path / "nope.crt") is None def test_ca_fingerprint_no_pem_block(tmp_path): garbage = tmp_path / "root.crt" garbage.write_text("not a certificate") assert https._ca_fingerprint(garbage) is None def test_status_no_ca_no_snippet(tmp_path): s = https.status(ca_path=tmp_path / "root.crt", snippet=tmp_path / "redirect.caddyfile") assert s == { "ca_available": False, "fingerprint_sha256": None, "force_https": False, "ca_download_url": "/rootCA.crt", } def test_status_with_ca_and_snippet(tmp_path): ca = tmp_path / "root.crt" ca.write_text(_TEST_CERT_PEM) snippet = tmp_path / "redirect.caddyfile" snippet.write_text(https.REDIRECT_CONTENT) s = https.status(ca_path=ca, snippet=snippet) assert s["ca_available"] is True assert s["fingerprint_sha256"] == _TEST_CERT_FP_SHA256 assert s["force_https"] is True def test_set_force_enable_writes_snippet_and_reloads(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet = snippet_dir / "redirect.caddyfile" calls = [] def fake_reload(): calls.append("reload") result = https.set_force_https( True, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=fake_reload ) assert result is True assert snippet.read_text() == https.REDIRECT_CONTENT assert calls == ["reload"] def test_set_force_disable_removes_snippet(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet_dir.mkdir() snippet = snippet_dir / "redirect.caddyfile" snippet.write_text(https.REDIRECT_CONTENT) result = https.set_force_https( False, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=lambda: None ) assert result is False assert not snippet.exists() def test_set_force_disable_is_idempotent_when_already_off(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet = snippet_dir / "redirect.caddyfile" result = https.set_force_https( False, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=lambda: None ) assert result is False assert not snippet.exists() def test_reload_failure_rolls_back_enable(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet = snippet_dir / "redirect.caddyfile" def failing_reload(): raise subprocess.CalledProcessError(1, ["systemctl"], stderr="bad config") with pytest.raises(https.HttpsError, match="caddy reload failed: bad config"): https.set_force_https( True, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=failing_reload ) # Rollback: since snippet didn't exist before, it must not exist after. assert not snippet.exists() def test_reload_failure_rolls_back_disable(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet_dir.mkdir() snippet = snippet_dir / "redirect.caddyfile" original = "redir https://{host}{uri} permanent\n# marker\n" snippet.write_text(original) def failing_reload(): raise subprocess.CalledProcessError(1, ["systemctl"], stderr="bad config") with pytest.raises(https.HttpsError): https.set_force_https( False, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=failing_reload ) # Rollback: snippet is restored to its exact prior contents. assert snippet.read_text() == original def test_systemctl_missing_raises_and_rolls_back(tmp_path): snippet_dir = tmp_path / "furtka.d" snippet = snippet_dir / "redirect.caddyfile" def missing_systemctl(): raise FileNotFoundError(2, "No such file", "systemctl") with pytest.raises(https.HttpsError, match="systemctl not available"): https.set_force_https( True, snippet_dir=snippet_dir, snippet=snippet, reload_caddy=missing_systemctl ) assert not snippet.exists() def test_redirect_snippet_content_is_caddy_redir_directive(): # Lock the exact directive. A regression here silently stops the # redirect from taking effect even though the file-swap looks fine. assert https.REDIRECT_CONTENT.strip() == "redir https://{host}{uri} permanent"