"""Unit tests for weekly_activity.aggregate — stubbed sources, no network.""" from __future__ import annotations import os import unittest from datetime import datetime from unittest import mock from weekly_activity.aggregate import ( SourceSpec, build_specs, collect_specs, format_combined, ) from weekly_activity.config import ( ActivityConfig, BtsSettings, GithubSettings, GitlabSettings, LaunchpadSettings, ) from weekly_activity.model import ActivityReport, Section class StubSource: def __init__(self, name: str = "stub", *, fail: bool = False) -> None: self.name = name self._fail = fail def collect(self, username: str, since: datetime, until: datetime) -> ActivityReport: if self._fail: raise RuntimeError("kaput") return ActivityReport( source=self.name, username=username, since=since, until=until, sections=[Section("Things", ["did a thing"])], warnings=["some query failed"], ) def window() -> tuple[datetime, datetime]: return datetime(2026, 8, 19), datetime(2026, 8, 26) class BuildSpecsTest(unittest.TestCase): def test_maps_every_section_in_order(self) -> None: config = ActivityConfig( launchpad=[LaunchpadSettings(username="lp")], github=[GithubSettings(username="gh")], gitlab=[GitlabSettings(username="gl")], bts=[BtsSettings(email="bts@mail")], ) specs = build_specs(config) self.assertEqual([s.name for s in specs], ["launchpad", "github", "gitlab", "bts"]) self.assertEqual([s.username for s in specs], ["lp", "gh", "gl", "bts@mail"]) def test_multiple_accounts_per_kind_in_listing_order(self) -> None: config = ActivityConfig( launchpad=[ LaunchpadSettings(username="lp-a"), LaunchpadSettings(username="lp-b", mode="credentials"), ], github=[ GithubSettings(username="gh-a", name="work"), GithubSettings(username="gh-b"), ], bts=[ BtsSettings(email="one@x.org"), BtsSettings(email="two@x.org", name="two"), ], ) specs = build_specs(config) # All launchpad accounts first, then github, then bts; listing order kept. self.assertEqual( [s.username for s in specs], ["lp-a", "lp-b", "gh-a", "gh-b", "one@x.org", "two@x.org"] ) self.assertEqual( [s.name for s in specs], [ "launchpad/lp-a", # siblings force identity into the label "launchpad/lp-b", "github/work", # explicit label wins even among siblings "github/gh-b", "bts/one@x.org", "bts/two", ], ) def test_single_unnamed_account_keeps_plain_kind_label(self) -> None: config = ActivityConfig(github=[GithubSettings(username="only-one")]) specs = build_specs(config) self.assertEqual(specs[0].name, "github") def test_disabled_sections_are_skipped(self) -> None: config = ActivityConfig(bts=[BtsSettings(email="only@one")]) self.assertEqual([s.name for s in build_specs(config)], ["bts"]) def test_launchpad_spec_expands_tilde_credentials_file(self) -> None: config = ActivityConfig(launchpad=[LaunchpadSettings(username="anon")]) config.launchpad.append( LaunchpadSettings(mode="credentials", username="u", credentials_file="~/creds.json") ) specs = build_specs(config) with mock.patch("weekly_activity.aggregate.LaunchpadSource") as fake_source: for spec in specs: spec.make() anon_kwargs, cred_kwargs = (call.kwargs for call in fake_source.call_args_list) self.assertTrue(anon_kwargs["anonymous"]) self.assertIsNone(anon_kwargs["credentials_file"]) self.assertFalse(cred_kwargs["anonymous"]) self.assertEqual(cred_kwargs["credentials_file"], os.path.expanduser("~") + "/creds.json") # The two accounts must not share one late-bound factory. self.assertNotEqual(anon_kwargs["credentials_file"], cred_kwargs["credentials_file"]) def test_github_token_env_resolved_lazily(self) -> None: config = ActivityConfig(github=[GithubSettings(username="u", token_env="MY_GH_TOKEN")]) spec = build_specs(config)[0] with ( mock.patch("weekly_activity.aggregate.GitHubSource") as fake_source, mock.patch.dict(os.environ, {"MY_GH_TOKEN": "env-token"}), ): spec.make() self.assertEqual(fake_source.call_args.kwargs["token"], "env-token") def test_github_stored_token_takes_precedence_over_env(self) -> None: config = ActivityConfig(github=[GithubSettings(username="u", token="tok")]) spec = build_specs(config)[0] with ( mock.patch("weekly_activity.aggregate.GitHubSource") as fake_source, mock.patch.dict(os.environ, {"MY_GH_TOKEN": "env-token"}), ): spec.make() self.assertEqual(fake_source.call_args.kwargs["token"], "tok") class CollectAndFormatTest(unittest.TestCase): def test_failure_isolated_per_source(self) -> None: since, until = window() specs = [ SourceSpec(name="alpha", username="a", make=lambda: StubSource("alpha")), SourceSpec(name="beta", username="b", make=lambda: StubSource("beta", fail=True)), ] collected = collect_specs(specs, since, until) self.assertIsNotNone(collected[0].report) self.assertIn("RuntimeError: kaput", collected[1].error or "") text = format_combined(collected, since, until) self.assertIn("Weekly activity report — 2 source(s)", text) self.assertIn("Activity report — alpha / ~a", text) self.assertIn("[skipped: beta — RuntimeError: kaput]", text) def test_heading_uses_account_label_not_bare_source_name(self) -> None: since, until = window() specs = [ SourceSpec(name="github/work", username="acme-jane", make=lambda: StubSource("github")) ] text = format_combined(collect_specs(specs, since, until), since, until) self.assertIn("Activity report — github/work / ~acme-jane", text) def test_warning_surfaced_inside_block(self) -> None: since, until = window() specs = [SourceSpec(name="gamma", username="g", make=lambda: StubSource("gamma"))] text = format_combined(collect_specs(specs, since, until), since, until) self.assertIn("Warnings (partial data — some queries failed):", text) self.assertIn("some query failed", text) if __name__ == "__main__": unittest.main()