recon-github 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {recon_github-0.1.5 → recon_github-0.1.7}/PKG-INFO +7 -8
  2. {recon_github-0.1.5 → recon_github-0.1.7}/README.md +6 -7
  3. {recon_github-0.1.5 → recon_github-0.1.7}/app/commands/repo.py +15 -22
  4. {recon_github-0.1.5 → recon_github-0.1.7}/app/ui/repo.py +132 -2
  5. {recon_github-0.1.5 → recon_github-0.1.7}/pyproject.toml +1 -1
  6. recon_github-0.1.7/tests/test_cli_commands.py +176 -0
  7. {recon_github-0.1.5 → recon_github-0.1.7}/uv.lock +1 -1
  8. recon_github-0.1.5/tests/test_cli_commands.py +0 -80
  9. {recon_github-0.1.5 → recon_github-0.1.7}/.agents/rich-ui-architect.md +0 -0
  10. {recon_github-0.1.5 → recon_github-0.1.7}/.github/workflows/cd.yml +0 -0
  11. {recon_github-0.1.5 → recon_github-0.1.7}/.github/workflows/ci.yml +0 -0
  12. {recon_github-0.1.5 → recon_github-0.1.7}/.gitignore +0 -0
  13. {recon_github-0.1.5 → recon_github-0.1.7}/.python-version +0 -0
  14. {recon_github-0.1.5 → recon_github-0.1.7}/LICENSE +0 -0
  15. {recon_github-0.1.5 → recon_github-0.1.7}/ROADMAP.md +0 -0
  16. {recon_github-0.1.5 → recon_github-0.1.7}/app/cli.py +0 -0
  17. {recon_github-0.1.5 → recon_github-0.1.7}/app/commands/auth.py +0 -0
  18. {recon_github-0.1.5 → recon_github-0.1.7}/app/commands/me.py +0 -0
  19. {recon_github-0.1.5 → recon_github-0.1.7}/app/services/auth.py +0 -0
  20. {recon_github-0.1.5 → recon_github-0.1.7}/app/services/github.py +0 -0
  21. {recon_github-0.1.5 → recon_github-0.1.7}/app/services/github_errors.py +0 -0
  22. {recon_github-0.1.5 → recon_github-0.1.7}/app/services/storage.py +0 -0
  23. {recon_github-0.1.5 → recon_github-0.1.7}/app/ui/avatar.py +0 -0
  24. {recon_github-0.1.5 → recon_github-0.1.7}/app/ui/display.py +0 -0
  25. {recon_github-0.1.5 → recon_github-0.1.7}/main.py +0 -0
  26. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_auth_service.py +0 -0
  27. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_cli.py +0 -0
  28. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_github_errors.py +0 -0
  29. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_github_service.py +0 -0
  30. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_repo_helpers.py +0 -0
  31. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_storage.py +0 -0
  32. {recon_github-0.1.5 → recon_github-0.1.7}/tests/test_ui_repo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: recon-github
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -11,7 +11,6 @@ Requires-Dist: rich>=15.0.0
11
11
  Requires-Dist: typer>=0.27.1
12
12
  Description-Content-Type: text/markdown
13
13
 
14
-
15
14
  # Recon CLI
16
15
 
17
16
  Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal.
@@ -25,9 +24,9 @@ Recon is a powerful CLI companion for developers and tools for agents, turning G
25
24
  [![License](https://img.shields.io/github/license/owenpalfreymandev/reconcli)](https://github.com/owenpalfreymandev/reconcli/blob/main/LICENSE)
26
25
  ## Overview
27
26
 
28
- Recon allows for developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interect with the service without having browser capabilities.
27
+ Recon allows developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interact with the service without having browser capabilities.
29
28
 
30
- It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and it's prebuilt components.
29
+ It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and its prebuilt components.
31
30
  ## Features
32
31
 
33
32
  * **GitHub Authentication** — Securely authenticate with GitHub using OAuth device flow.
@@ -41,7 +40,7 @@ It is powered by [Typer](github.com/fastapi/typer), a library for building fast
41
40
 
42
41
  ## Installation
43
42
 
44
- Since we want you to be able to use Recon from anywhere - not just one directory - we recomend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
43
+ Since we want you to be able to use Recon from anywhere - not just one directory - we recommend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
45
44
 
46
45
  ```bash
47
46
  uv tool install recon-github
@@ -68,7 +67,7 @@ Example output:
68
67
  ╭────── Owen Palfreyman ───────╮ ╭──────────────────────────────╮
69
68
  │ Repositories 2 │ │ │
70
69
  │ Followers 7 │ │ │
71
- │ Following 2 │ │ │
70
+ │ Following 2 │ │ │
72
71
  │ Location United Kingdom │ │ │
73
72
  │ Company — │ │ │
74
73
  │ Joined 2023-11-02 │ │ │
@@ -213,7 +212,7 @@ uv run pytest
213
212
  uv run pyright
214
213
  ```
215
214
 
216
- Be sure to lint before commiting, or it will fail CI/CD:
215
+ Be sure to lint before committing, or it will fail CI/CD:
217
216
  ```bash
218
217
  uv run ruff check --fix .
219
218
  ```
@@ -237,4 +236,4 @@ Recon is developed by Owen Palfreyman.
237
236
  GitHub: @owenpalfreymandev
238
237
  Repository: owenpalfreymandev/reconcli
239
238
 
240
- *or could could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
239
+ *or could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
@@ -1,4 +1,3 @@
1
-
2
1
  # Recon CLI
3
2
 
4
3
  Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal.
@@ -12,9 +11,9 @@ Recon is a powerful CLI companion for developers and tools for agents, turning G
12
11
  [![License](https://img.shields.io/github/license/owenpalfreymandev/reconcli)](https://github.com/owenpalfreymandev/reconcli/blob/main/LICENSE)
13
12
  ## Overview
14
13
 
15
- Recon allows for developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interect with the service without having browser capabilities.
14
+ Recon allows developers to view basic GitHub analytics in the terminal quickly, rather than opening up their browser. This means it allows agents to interact with the service without having browser capabilities.
16
15
 
17
- It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and it's prebuilt components.
16
+ It is powered by [Typer](github.com/fastapi/typer), a library for building fast CLI tools, and made pretty by [Rich](https://github.com/textualize/rich) and its prebuilt components.
18
17
  ## Features
19
18
 
20
19
  * **GitHub Authentication** — Securely authenticate with GitHub using OAuth device flow.
@@ -28,7 +27,7 @@ It is powered by [Typer](github.com/fastapi/typer), a library for building fast
28
27
 
29
28
  ## Installation
30
29
 
31
- Since we want you to be able to use Recon from anywhere - not just one directory - we recomend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
30
+ Since we want you to be able to use Recon from anywhere - not just one directory - we recommend you install it with [uv package manager](https://docs.astral.sh/uv/getting-started/installation/).
32
31
 
33
32
  ```bash
34
33
  uv tool install recon-github
@@ -55,7 +54,7 @@ Example output:
55
54
  ╭────── Owen Palfreyman ───────╮ ╭──────────────────────────────╮
56
55
  │ Repositories 2 │ │ │
57
56
  │ Followers 7 │ │ │
58
- │ Following 2 │ │ │
57
+ │ Following 2 │ │ │
59
58
  │ Location United Kingdom │ │ │
60
59
  │ Company — │ │ │
61
60
  │ Joined 2023-11-02 │ │ │
@@ -200,7 +199,7 @@ uv run pytest
200
199
  uv run pyright
201
200
  ```
202
201
 
203
- Be sure to lint before commiting, or it will fail CI/CD:
202
+ Be sure to lint before committing, or it will fail CI/CD:
204
203
  ```bash
205
204
  uv run ruff check --fix .
206
205
  ```
@@ -224,4 +223,4 @@ Recon is developed by Owen Palfreyman.
224
223
  GitHub: @owenpalfreymandev
225
224
  Repository: owenpalfreymandev/reconcli
226
225
 
227
- *or could could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
226
+ *or could just run `recon details owenpalfreymandev reconcli --contributors` wink wink*
@@ -67,31 +67,24 @@ def format_languages(languages: dict[str, int], max_languages: int = 5) -> list[
67
67
 
68
68
 
69
69
  @app.command()
70
- def list():
70
+ def list(
71
+ boxy: bool = typer.Option(
72
+ False,
73
+ "--boxy",
74
+ help="Render each repository as a panel (not recommended for long lists).",
75
+ ),
76
+ ):
71
77
  """See a list of all your repos."""
72
- from app.services.github import get_user_repos
78
+ from app.services.github import get_authenticated_user, get_user_repos
79
+ from app.ui.repo import display_repository_list
73
80
 
81
+ user = get_authenticated_user()
74
82
  repos = get_user_repos()
75
-
76
- for repo in repos:
77
- typer.echo(f"{repo['full_name']}")
78
- description = repo.get("description") or "—"
79
- typer.echo(f"description: {format_description(description)}")
80
- typer.echo(
81
- f"visibility: {repo.get('visibility') or ('private' if repo.get('private') else 'public')}"
82
- )
83
- typer.echo(f"language: {repo.get('language') or '—'}")
84
- typer.echo(f"default branch: {repo.get('default_branch') or '—'}")
85
- typer.echo(
86
- f"stars: {repo.get('stargazers_count', 0)} forks: {repo.get('forks_count', 0)} open issues: {repo.get('open_issues_count', 0)}"
87
- )
88
- typer.echo(f" url: {repo['html_url']}")
89
-
90
- topics = repo.get("topics") or []
91
- if topics:
92
- typer.echo(f" topics: {format_topics(topics)}")
93
-
94
- typer.echo("")
83
+ display_repository_list(
84
+ repos,
85
+ user.get("login", "Unknown user"),
86
+ boxy=boxy,
87
+ )
95
88
 
96
89
 
97
90
  @app.command()
@@ -7,13 +7,143 @@ from rich.text import Text
7
7
  console = Console()
8
8
 
9
9
 
10
- def build_view_header(title: str, subtitle: str) -> Panel:
10
+ def build_repository_table(repositories: list[dict]) -> Table:
11
+ """Build the compact repository listing used by ``recon list``."""
12
+ table_width = _repository_table_width()
13
+ repository_width = max(13, min(32, table_width - 66))
14
+ table = Table(
15
+ title=f"Repositories ({len(repositories)})",
16
+ title_style="bold cyan",
17
+ header_style="bold cyan",
18
+ box=None,
19
+ show_edge=False,
20
+ pad_edge=False,
21
+ padding=(0, 1),
22
+ collapse_padding=False,
23
+ width=table_width,
24
+ )
25
+ table.add_column(
26
+ "Repository",
27
+ no_wrap=True,
28
+ overflow="ellipsis",
29
+ width=repository_width,
30
+ max_width=repository_width,
31
+ )
32
+ table.add_column(
33
+ "Visibility", no_wrap=True, overflow="ellipsis", width=10, max_width=10
34
+ )
35
+ table.add_column(
36
+ "Language", no_wrap=True, overflow="ellipsis", width=9, max_width=9
37
+ )
38
+ table.add_column(
39
+ "Updated", no_wrap=True, overflow="ellipsis", width=10, max_width=10
40
+ )
41
+ table.add_column("Branch", no_wrap=True, overflow="ellipsis", width=6, max_width=6)
42
+ table.add_column("Stars", justify="right", no_wrap=True, width=5)
43
+ table.add_column("Forks", justify="right", no_wrap=True, width=5)
44
+ table.add_column("Issues", justify="right", no_wrap=True, width=5)
45
+
46
+ for repository in repositories:
47
+ visibility = repository.get("visibility") or (
48
+ "private" if repository.get("private") else "public"
49
+ )
50
+ table.add_row(
51
+ str(repository.get("full_name") or repository.get("name") or "—"),
52
+ str(visibility or "—"),
53
+ str(repository.get("language") or "—"),
54
+ _format_updated(repository.get("updated_at")),
55
+ str(repository.get("default_branch") or "—"),
56
+ _format_count(repository.get("stargazers_count")),
57
+ _format_count(repository.get("forks_count")),
58
+ _format_count(repository.get("open_issues_count")),
59
+ )
60
+
61
+ if not repositories:
62
+ table.add_row("No repositories found.", *["—"] * 7)
63
+
64
+ return table
65
+
66
+
67
+ def display_repository_list(
68
+ repositories: list[dict], username: str, boxy: bool = False
69
+ ) -> None:
70
+ """Render a compact, scan-friendly table of repositories."""
71
+ width = _repository_box_width() if boxy else _repository_table_width()
72
+ console.print(build_view_header("List", username, width=width))
73
+ if boxy:
74
+ for repository in repositories:
75
+ console.print(build_repository_panel(repository))
76
+ if not repositories:
77
+ console.print(Panel("No repositories found.", width=width))
78
+ return
79
+ console.print(build_repository_table(repositories))
80
+
81
+
82
+ def build_repository_panel(repository: dict) -> Panel:
83
+ """Build the box-style repository summary used by ``recon list --boxy``."""
84
+ from app.commands.repo import format_topics
85
+
86
+ visibility = repository.get("visibility") or (
87
+ "private" if repository.get("private") else "public"
88
+ )
89
+ language = repository.get("language") or "—"
90
+ branch = repository.get("default_branch") or "—"
91
+ topics = repository.get("topics") or []
92
+ topic_text = (
93
+ format_topics([str(topic) for topic in topics]).replace(", ", " · ")
94
+ if topics
95
+ else "—"
96
+ )
97
+ content = Text()
98
+ content.append(
99
+ f"{str(visibility).capitalize()} · {language} · {branch}\n",
100
+ style="cyan",
101
+ )
102
+ content.append(
103
+ f"Stars {_format_count(repository.get('stargazers_count'))} "
104
+ f"Forks {_format_count(repository.get('forks_count'))} "
105
+ f"Issues {_format_count(repository.get('open_issues_count'))}\n"
106
+ )
107
+ content.append(topic_text, style="dim")
108
+ return Panel(
109
+ content,
110
+ title=str(repository.get("full_name") or repository.get("name") or "—"),
111
+ width=_repository_box_width(),
112
+ border_style="cyan",
113
+ )
114
+
115
+
116
+ def _repository_table_width() -> int:
117
+ """Calculate the shared width for the list table and header."""
118
+ return min(max(1, console.width - 1), 98)
119
+
120
+
121
+ def _repository_box_width() -> int:
122
+ """Calculate the narrower width used by boxy repository summaries."""
123
+ return min(max(1, console.width - 1), 72)
124
+
125
+
126
+ def _format_count(value: object) -> str:
127
+ """Format repository counters while keeping missing values predictable."""
128
+ if value is None:
129
+ return "0"
130
+ return f"{value:,}" if isinstance(value, int) else str(value)
131
+
132
+
133
+ def _format_updated(value: object) -> str:
134
+ """Show the date portion of GitHub's ISO timestamp."""
135
+ if not value:
136
+ return "—"
137
+ return str(value).split("T", 1)[0]
138
+
139
+
140
+ def build_view_header(title: str, subtitle: str, width: int = 60) -> Panel:
11
141
  """Create the shared header used by focused repository views."""
12
142
  content = Text(justify="center")
13
143
  content.append(title.upper(), style="bold cyan")
14
144
  content.append("\n")
15
145
  content.append(subtitle, style="bold")
16
- return Panel(content, width=60, border_style="cyan")
146
+ return Panel(content, width=width, border_style="cyan")
17
147
 
18
148
 
19
149
  def display_view_header(title: str, subtitle: str) -> None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "recon-github"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "Recon is a powerful CLI companion for developers and tools for agents, turning GitHub activity and repository data into useful insights from your terminal"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,176 @@
1
+ from unittest.mock import Mock
2
+
3
+ from typer.testing import CliRunner
4
+
5
+ from app.cli import app
6
+ from app.commands import auth as auth_command
7
+ from app.commands import me as me_command
8
+ from app.ui.repo import build_repository_table
9
+
10
+ runner = CliRunner()
11
+
12
+
13
+ def test_login_logout_delegate(monkeypatch):
14
+ login, logout = Mock(), Mock()
15
+ monkeypatch.setattr(auth_command.auth, "login", login)
16
+ monkeypatch.setattr(auth_command.auth, "logout", logout)
17
+ assert runner.invoke(app, ["login"]).exit_code == 0
18
+ assert runner.invoke(app, ["logout"]).exit_code == 0
19
+ login.assert_called_once_with()
20
+ logout.assert_called_once_with()
21
+
22
+
23
+ def user():
24
+ return {
25
+ "login": "octo",
26
+ "name": "Octo",
27
+ "public_repos": 1,
28
+ "followers": 2,
29
+ "following": 3,
30
+ "created_at": "2020-01-01T00:00:00Z",
31
+ }
32
+
33
+
34
+ def test_me_and_scout_display_user(monkeypatch):
35
+ get_me, get_user, display = (
36
+ Mock(return_value=user()),
37
+ Mock(return_value=user()),
38
+ Mock(),
39
+ )
40
+ monkeypatch.setattr("app.services.github.get_authenticated_user", get_me)
41
+ monkeypatch.setattr("app.services.github.get_user", get_user)
42
+ monkeypatch.setattr(me_command, "display_user", display)
43
+ assert runner.invoke(app, ["me"]).exit_code == 0
44
+ assert runner.invoke(app, ["scout", "octo"]).exit_code == 0
45
+ get_me.assert_called_once_with()
46
+ get_user.assert_called_once_with("octo")
47
+ assert display.call_count == 2
48
+
49
+
50
+ def test_list_handles_empty_and_missing_optional_fields(monkeypatch):
51
+ repo = {"full_name": "o/r", "html_url": "https://github.com/o/r"}
52
+ monkeypatch.setattr(
53
+ "app.services.github.get_authenticated_user",
54
+ Mock(return_value={"login": "octo"}),
55
+ )
56
+ monkeypatch.setattr(
57
+ "app.services.github.get_user_repos", Mock(side_effect=[[], [repo]])
58
+ )
59
+ assert runner.invoke(app, ["list"]).exit_code == 0
60
+ result = runner.invoke(app, ["list"])
61
+ assert result.exit_code == 0
62
+ assert "o/r" in result.output
63
+ assert "LIST" in result.output
64
+ assert "octo" in result.output
65
+ assert "Repositories (1)" in result.output
66
+ assert "—" in result.output
67
+
68
+
69
+ def test_list_renders_repository_columns(monkeypatch):
70
+ monkeypatch.setattr(
71
+ "app.services.github.get_authenticated_user",
72
+ Mock(return_value={"login": "octo"}),
73
+ )
74
+ monkeypatch.setattr(
75
+ "app.services.github.get_user_repos",
76
+ Mock(
77
+ return_value=[
78
+ {
79
+ "full_name": "octo/a-really-long-repository-name",
80
+ "visibility": "public",
81
+ "language": "Python",
82
+ "updated_at": "2026-09-25T12:34:56Z",
83
+ "default_branch": "main",
84
+ "stargazers_count": 1_234,
85
+ "forks_count": 56,
86
+ "open_issues_count": 7,
87
+ }
88
+ ]
89
+ ),
90
+ )
91
+
92
+ result = runner.invoke(app, ["list"])
93
+
94
+ assert result.exit_code == 0
95
+ assert [column.header for column in build_repository_table([{}]).columns] == [
96
+ "Repository",
97
+ "Visibility",
98
+ "Language",
99
+ "Updated",
100
+ "Branch",
101
+ "Stars",
102
+ "Forks",
103
+ "Issues",
104
+ ]
105
+ assert "1,234" in result.output
106
+ assert "2026-09-25" in result.output
107
+ assert "Visibility" in result.output
108
+ assert "Language" in result.output
109
+
110
+
111
+ def test_list_boxy_renders_repository_panels(monkeypatch):
112
+ monkeypatch.setattr(
113
+ "app.services.github.get_authenticated_user",
114
+ Mock(return_value={"login": "octo"}),
115
+ )
116
+ monkeypatch.setattr(
117
+ "app.services.github.get_user_repos",
118
+ Mock(
119
+ return_value=[
120
+ {
121
+ "full_name": "octo/reconcli",
122
+ "visibility": "public",
123
+ "language": "Python",
124
+ "default_branch": "main",
125
+ "stargazers_count": 0,
126
+ "forks_count": 0,
127
+ "open_issues_count": 0,
128
+ "topics": [
129
+ "cli",
130
+ "developer-tools",
131
+ "github",
132
+ "python",
133
+ "rich-cli",
134
+ "terminal",
135
+ ],
136
+ }
137
+ ]
138
+ ),
139
+ )
140
+
141
+ result = runner.invoke(app, ["list", "--boxy"])
142
+
143
+ assert result.exit_code == 0
144
+ assert "LIST" in result.output
145
+ assert "octo/reconcli" in result.output
146
+ assert "Public · Python · main" in result.output
147
+ assert "Stars 0 Forks 0 Issues 0" in result.output
148
+ assert (
149
+ "cli · developer-tools · github · python · rich-cli + 1 more..."
150
+ in result.output
151
+ )
152
+ assert "Repositories (" not in result.output
153
+ assert result.output.count("─" * 70) >= 2
154
+
155
+
156
+ def test_details_rejects_both_views():
157
+ result = runner.invoke(app, ["details", "o", "r", "--contributors", "--languages"])
158
+ assert result.exit_code != 0
159
+ assert "Choose either" in result.output
160
+
161
+
162
+ def test_details_default_calls_services_and_prints(monkeypatch):
163
+ details = {"full_name": "o/r", "description": "desc", "size": 1}
164
+ monkeypatch.setattr(
165
+ "app.services.github.get_repo_details", Mock(return_value=details)
166
+ )
167
+ monkeypatch.setattr(
168
+ "app.services.github.get_languages", Mock(return_value={"Python": 1})
169
+ )
170
+ monkeypatch.setattr(
171
+ "app.services.github.get_top_contributors", Mock(return_value=[])
172
+ )
173
+ result = runner.invoke(app, ["details", "o", "r"])
174
+ assert result.exit_code == 0
175
+ assert "Repository" in result.output
176
+ assert "Languages" in result.output
@@ -423,7 +423,7 @@ wheels = [
423
423
 
424
424
  [[package]]
425
425
  name = "recon-github"
426
- version = "0.1.5"
426
+ version = "0.1.7"
427
427
  source = { editable = "." }
428
428
  dependencies = [
429
429
  { name = "dotenv" },
@@ -1,80 +0,0 @@
1
- from unittest.mock import Mock
2
-
3
- from typer.testing import CliRunner
4
-
5
- from app.cli import app
6
- from app.commands import auth as auth_command
7
- from app.commands import me as me_command
8
-
9
- runner = CliRunner()
10
-
11
-
12
- def test_login_logout_delegate(monkeypatch):
13
- login, logout = Mock(), Mock()
14
- monkeypatch.setattr(auth_command.auth, "login", login)
15
- monkeypatch.setattr(auth_command.auth, "logout", logout)
16
- assert runner.invoke(app, ["login"]).exit_code == 0
17
- assert runner.invoke(app, ["logout"]).exit_code == 0
18
- login.assert_called_once_with()
19
- logout.assert_called_once_with()
20
-
21
-
22
- def user():
23
- return {
24
- "login": "octo",
25
- "name": "Octo",
26
- "public_repos": 1,
27
- "followers": 2,
28
- "following": 3,
29
- "created_at": "2020-01-01T00:00:00Z",
30
- }
31
-
32
-
33
- def test_me_and_scout_display_user(monkeypatch):
34
- get_me, get_user, display = (
35
- Mock(return_value=user()),
36
- Mock(return_value=user()),
37
- Mock(),
38
- )
39
- monkeypatch.setattr("app.services.github.get_authenticated_user", get_me)
40
- monkeypatch.setattr("app.services.github.get_user", get_user)
41
- monkeypatch.setattr(me_command, "display_user", display)
42
- assert runner.invoke(app, ["me"]).exit_code == 0
43
- assert runner.invoke(app, ["scout", "octo"]).exit_code == 0
44
- get_me.assert_called_once_with()
45
- get_user.assert_called_once_with("octo")
46
- assert display.call_count == 2
47
-
48
-
49
- def test_list_handles_empty_and_missing_optional_fields(monkeypatch):
50
- repo = {"full_name": "o/r", "html_url": "https://github.com/o/r"}
51
- monkeypatch.setattr(
52
- "app.services.github.get_user_repos", Mock(side_effect=[[], [repo]])
53
- )
54
- assert runner.invoke(app, ["list"]).exit_code == 0
55
- result = runner.invoke(app, ["list"])
56
- assert result.exit_code == 0
57
- assert "o/r" in result.output
58
-
59
-
60
- def test_details_rejects_both_views():
61
- result = runner.invoke(app, ["details", "o", "r", "--contributors", "--languages"])
62
- assert result.exit_code != 0
63
- assert "Choose either" in result.output
64
-
65
-
66
- def test_details_default_calls_services_and_prints(monkeypatch):
67
- details = {"full_name": "o/r", "description": "desc", "size": 1}
68
- monkeypatch.setattr(
69
- "app.services.github.get_repo_details", Mock(return_value=details)
70
- )
71
- monkeypatch.setattr(
72
- "app.services.github.get_languages", Mock(return_value={"Python": 1})
73
- )
74
- monkeypatch.setattr(
75
- "app.services.github.get_top_contributors", Mock(return_value=[])
76
- )
77
- result = runner.invoke(app, ["details", "o", "r"])
78
- assert result.exit_code == 0
79
- assert "Repository" in result.output
80
- assert "Languages" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes