141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
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
|