import base64 import json import logging import pytest from config import Config, ConfigError, load_json_secret from conftest import make_config from logging_config import RedactSecrets def _rsa_pem() -> str: from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa key = rsa.generate_private_key(public_exponent=65537, key_size=2048) return key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption() ).decode() SA_INFO = { "type": "service_account", "project_id": "example-project", "private_key_id": "abc123", "client_email": "uploader@example-project.iam.gserviceaccount.com", "client_id": "1", "token_uri": "https://oauth2.googleapis.com/token", } TOKEN_INFO = { "token": "ya29.placeholder-access-token", "refresh_token": "1//placeholder-refresh-token", "client_id": "placeholder.apps.googleusercontent.com", "client_secret": "placeholder-client-secret", "token_uri": "https://oauth2.googleapis.com/token", "expiry": "2999-01-01T00:00:00Z", } def test_defaults_and_mapped_names(tmp_path): c = make_config(tmp_path, MAX_RETRIES="4", RETRY_DELAY="15", DUPLICATE_POLICY="suffix_message_id", BATCH_SUBFOLDERS_ENABLED="yes", GOOGLE_DRIVE_ROOT_FOLDER_ID="FOLDER123") assert c.max_attempts == 5 and c.retry_base_delay == 15 assert c.duplicate_name_policy == "suffix_message_id" assert c.folder_structure == "batch_topic" assert c.drive_root_folder_id == "FOLDER123" assert c.source_channel == "-1001234567890" assert c.fallback_topic == "Uncategorized" assert c.share_mode == "none" # never public by default assert c.drive_scope == "drive.file" def test_invalid_values_are_all_reported(tmp_path): with pytest.raises(ConfigError) as err: make_config(tmp_path, MAX_RETRIES="many", PROCESS_MODE="sometimes", UPLOAD_CHUNK_SIZE="1000", KEEP_LOCAL_FILES="maybe") text = str(err.value) for name in ("MAX_RETRIES", "PROCESS_MODE", "UPLOAD_CHUNK_SIZE", "KEEP_LOCAL_FILES"): assert name in text def test_missing_required_telegram_settings(): config = Config.from_env({}) with pytest.raises(ConfigError) as err: config.validate("telegram") text = str(err.value) assert "TELEGRAM_API_ID" in text and "SOURCE_CHANNEL_ID" in text def test_heroku_requires_durable_database_and_session_string(tmp_path): c = make_config(tmp_path, DYNO="worker.1", TELEGRAM_MODE="user") problems = c.database_problems() + c.telegram_problems() + c.drive_problems() joined = " ".join(problems) assert "DATABASE_URL" in joined assert "TELEGRAM_SESSION_STRING" in joined assert "GOOGLE_OAUTH_TOKEN_JSON" in joined ok = make_config( tmp_path, DYNO="worker.1", TELEGRAM_MODE="user", TELEGRAM_SESSION_STRING="1AaBbCc-placeholder", DATABASE_URL="postgres://u:p@db.example.com:5432/d", GOOGLE_OAUTH_TOKEN_JSON=json.dumps(TOKEN_INFO), ) ok.validate("telegram", "drive", "database") assert ok.database_url == "postgresql://u:p@db.example.com:5432/d?sslmode=require" assert ok.instance_lock_wait > 0 def test_ephemeral_database_must_be_explicit(tmp_path): c = make_config(tmp_path, DYNO="worker.1", ALLOW_EPHEMERAL_DATABASE="true") assert c.database_problems() == [] def test_json_secrets_raw_multiline_and_base64(): raw = json.dumps(TOKEN_INFO, indent=2) assert load_json_secret(raw)["refresh_token"] == TOKEN_INFO["refresh_token"] encoded = base64.b64encode(raw.encode()).decode() assert load_json_secret(encoded)["client_id"] == TOKEN_INFO["client_id"] with pytest.raises(ValueError): load_json_secret("{not json") def test_bad_credential_json_reported(tmp_path): c = make_config(tmp_path, GOOGLE_AUTH_MODE="service_account", GOOGLE_SERVICE_ACCOUNT_JSON='{"type": "service_account"}') assert any("missing keys" in p for p in c.drive_problems()) def test_credentials_load_from_env_without_files(tmp_path): from drive_client import load_credentials sa = dict(SA_INFO, private_key=_rsa_pem()) c = make_config(tmp_path, GOOGLE_AUTH_MODE="service_account", GOOGLE_SERVICE_ACCOUNT_JSON=json.dumps(sa)) assert not c.google_token_file.exists() and not c.google_client_secrets_file.exists() c.validate("drive") creds = load_credentials(c) assert creds.service_account_email == SA_INFO["client_email"] assert creds.scopes == ["https://www.googleapis.com/auth/drive.file"] c = make_config(tmp_path, GOOGLE_OAUTH_TOKEN_JSON=base64.b64encode(json.dumps(TOKEN_INFO).encode()).decode()) c.validate("drive") creds = load_credentials(c) assert creds.refresh_token == TOKEN_INFO["refresh_token"] and creds.valid assert not c.google_token_file.exists() # nothing written to the ephemeral disk def test_secrets_are_redacted_from_logs(tmp_path, caplog): sa = dict(SA_INFO, private_key="-----BEGIN PRIVATE KEY-----placeholder") c = make_config(tmp_path, GOOGLE_SERVICE_ACCOUNT_JSON=json.dumps(sa), GOOGLE_OAUTH_TOKEN_JSON=json.dumps(TOKEN_INFO)) secrets = c.secrets() assert c.bot_token in secrets and c.api_hash in secrets assert TOKEN_INFO["refresh_token"] in secrets and TOKEN_INFO["client_secret"] in secrets record = logging.LogRecord("x", logging.INFO, __file__, 1, "token=%s refresh=%s", (c.bot_token, TOKEN_INFO["refresh_token"]), None) RedactSecrets(secrets).filter(record) assert c.bot_token not in record.getMessage() assert TOKEN_INFO["refresh_token"] not in record.getMessage() def test_main_exits_with_clear_error_when_unconfigured(monkeypatch, tmp_path): import main monkeypatch.setenv("LOG_DIR", str(tmp_path / "logs")) for name in ("TELEGRAM_API_ID", "TELEGRAM_API_HASH", "TELEGRAM_BOT_TOKEN", "SOURCE_CHANNEL_ID", "DYNO", "DATABASE_URL"): monkeypatch.delenv(name, raising=False) assert main.main(["check-config"]) == 2