Files
weekly-activity/tests/test_config.py
kosmos 85ddfb4a71 Add arrow-key menus and report loading status
wizard: when stdin and stdout are both TTYs, menus render inline and
are driven by keys: up/down/j/k/Home/End move the bold '>' highlight,
Enter accepts, Esc/Ctrl-C/q back out of cancellable menus while Ctrl-C
on fixed menus still aborts. Rows redraw with erase+CR across the
option block only and clear before returning so prompts stay aligned.
Non-TTY stdio keeps the original numbered prompt verbatim.

report: each configured source shows a transient "# Generating
report: pulling <name> data..." line that is erased on completion,
only when stdout is a TTY; piped output remains exactly the report.

tests: cover the numbered fallback, row rendering, status suppression
and show+clear parity; reformat stray test files to satisfy ruff.
2026-08-27 07:46:48 +00:00

259 lines
9.5 KiB
Python

"""Unit tests for weekly_activity.config — pure logic, no network."""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from weekly_activity.config import (
ActivityConfig,
BtsSettings,
ConfigError,
GithubSettings,
GitlabSettings,
LaunchpadSettings,
default_config_path,
format_toml,
load_config,
save_config,
)
def load_config_from_text(text: str) -> ActivityConfig:
"""Helper: parse TOML through the public loader without touching disk."""
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "weekly-activity.toml"
path.write_text(text, encoding="utf-8")
return load_config(path)
def sample_v2_config() -> ActivityConfig:
return ActivityConfig(
launchpad=[
LaunchpadSettings(
username="lp-user",
mode="credentials",
credentials_file="/tmp/lp-creds.json",
service="staging",
name="work",
),
LaunchpadSettings(username="anon-lp"),
],
github=[
GithubSettings(username='gh"user', token="tok\\en"),
GithubSettings(username="acme-jane", token_env="WORK_GH_TOKEN", name="work"),
],
gitlab=[
GitlabSettings(
username="gl-user",
url="https://salsa.debian.org",
token_env="SALSA_TOKEN",
name="salsa",
)
],
bts=[BtsSettings(email="dev@example.org")],
)
class ConfigRoundTripTest(unittest.TestCase):
def test_round_trip_preserves_all_accounts(self) -> None:
config = sample_v2_config()
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "weekly-activity.toml"
save_config(config, path)
self.assertEqual(load_config(path), config)
def test_save_writes_v2_version_header(self) -> None:
text = format_toml(ActivityConfig(github=[GithubSettings(username="g")]))
self.assertIn("version = 2", text)
self.assertIn("[[github.accounts]]", text)
def test_save_creates_owner_only_file(self) -> None:
config = ActivityConfig(bts=[BtsSettings(email="x@example.org")])
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "nested" / "cfg.toml"
save_config(config, path)
self.assertEqual(os.stat(path).st_mode & 0o777, 0o600)
def test_empty_config_emits_header_only(self) -> None:
text = format_toml(ActivityConfig())
self.assertEqual(load_config_from_text(text), ActivityConfig())
self.assertNotIn("[[", text)
def test_empty_optionals_round_trip_to_defaults(self) -> None:
config = ActivityConfig(
github=[GithubSettings(username="octocat")],
bts=[BtsSettings(email="a@b.c")],
)
reparsed = load_config_from_text(format_toml(config))
self.assertEqual(reparsed.github, [GithubSettings(username="octocat")])
self.assertEqual(reparsed.bts, [BtsSettings(email="a@b.c")])
self.assertEqual(reparsed.launchpad, [])
def test_escapes_quotes_backslashes_and_newlines(self) -> None:
config = ActivityConfig(github=[GithubSettings(username='a"b\\c\nd')])
reparsed = load_config_from_text(format_toml(config))
self.assertEqual(reparsed.github[0].username, 'a"b\\c\nd')
def test_load_does_not_rewrite_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "weekly-activity.toml"
original = "[github]\nusername = 'octo'\n"
path.write_text(original, encoding="utf-8")
before = path.read_bytes()
config = load_config(path)
self.assertEqual(config.github, [GithubSettings(username="octo")])
self.assertEqual(path.read_bytes(), before)
class ConfigLoadingTest(unittest.TestCase):
def test_missing_file_raises_filenotfound(self) -> None:
with self.assertRaises(FileNotFoundError):
load_config(Path("/nonexistent/weekly-activity.toml"))
def test_unknown_section_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\n\n[[gitea.accounts]]\nusername = "x"\n')
def test_unknown_top_level_key_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text("version = 2\nsources = 3\n")
def test_unknown_account_key_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('[github]\naccounts = [{username = "x", tokken = "t"}]\n')
def test_section_must_hold_only_accounts_table(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('[version]\nwrong = "shape"\n') # wrong type below
with self.assertRaises(ConfigError):
load_config_from_text(
'version = 2\n\n[github]\nusername = "legacy"\n' # v2 without .accounts
)
def test_accounts_must_be_list_of_tables(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\n\n[github]\naccounts = "nope"\n')
with self.assertRaises(ConfigError):
load_config_from_text("version = 2\n\n[gitlab]\naccounts = [42]\n")
def test_non_string_value_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text("version = 2\n\n[[github.accounts]]\nusername = 42\n")
def test_bad_launchpad_mode_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text(
'version = 2\n\n[[launchpad.accounts]]\nusername = "u"\nmode = "oauth2"\n'
)
def test_bad_launchpad_service_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text(
'version = 2\n\n[[launchpad.accounts]]\nusername = "u"\nservice = "qa"\n'
)
def test_v2_section_without_accounts_table_rejected(self) -> None:
# A version-tagged file must use the [[kind.accounts]] shape.
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\n\n[github]\nusername = "legacy"\n')
def test_account_errors_carry_position(self) -> None:
text = (
"version = 2\n"
'\n[[launchpad.accounts]]\nusername = "ok"\n'
'\n[[launchpad.accounts]]\nusername = "bad"\nservice = "oops"\n'
)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "weekly-activity.toml"
path.write_text(text, encoding="utf-8")
with self.assertRaisesRegex(ConfigError, r"launchpad\.accounts\[1\]"):
load_config(path)
def test_enabled_sources_stable_order(self) -> None:
config = ActivityConfig(
bts=[BtsSettings(email="e")],
github=[GithubSettings(username="g")],
)
self.assertEqual(config.enabled_sources(), ["github", "bts"])
class ConfigMigrationTest(unittest.TestCase):
V1_TEXT = """\
[launchpad]
username = "lp-user"
mode = "credentials"
credentials_file = "~/creds.json"
[github]
username = "octo"
token = "t0k"
[gitlab]
url = "https://salsa.debian.org"
username = "gl"
token_env = "SALSA_TOKEN"
[bts]
email = "dev@example.org"
"""
def test_v1_file_migrates_to_single_unnamed_accounts_in_memory(self) -> None:
config = load_config_from_text(self.V1_TEXT)
self.assertEqual(config.enabled_sources(), ["launchpad", "github", "gitlab", "bts"])
self.assertEqual(
config.launchpad,
[
LaunchpadSettings(
username="lp-user",
mode="credentials",
credentials_file="~/creds.json",
)
],
)
self.assertEqual(config.github, [GithubSettings(username="octo", token="t0k")])
self.assertEqual(
config.gitlab,
[
GitlabSettings(
url="https://salsa.debian.org",
username="gl",
token_env="SALSA_TOKEN",
)
],
)
self.assertEqual(config.bts, [BtsSettings(email="dev@example.org")])
def test_saving_migrated_config_persists_v2_shape(self) -> None:
config = load_config_from_text(self.V1_TEXT)
text = format_toml(config)
self.assertIn("version = 2", text)
self.assertEqual(load_config_from_text(text), config)
def test_v1_unknown_keys_still_strict(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('[github]\nusername = "u"\ntokken = "t"\n')
def test_v2_shaped_file_without_version_rejected(self) -> None:
with self.assertRaisesRegex(ConfigError, "version"):
load_config_from_text('[[github.accounts]]\nusername = "u"\n')
class ConfigPathTest(unittest.TestCase):
def test_default_path_uses_xdg_or_home(self) -> None:
old = os.environ.get("XDG_CONFIG_HOME")
try:
os.environ["XDG_CONFIG_HOME"] = "/custom/cfg"
self.assertEqual(default_config_path(), Path("/custom/cfg/weekly-activity.toml"))
del os.environ["XDG_CONFIG_HOME"]
self.assertTrue(str(default_config_path()).endswith("/.config/weekly-activity.toml"))
finally:
if old is not None:
os.environ["XDG_CONFIG_HOME"] = old
if __name__ == "__main__":
unittest.main()