audit-labs/audit-tools
A collection of scripts, queries, and other goodies you can use in an audit.
clone: git clone https://gitbay.org/audit-labs/audit-tools.git
v1.0.0: tui/tests/test_app.py · raw
1"""Headless smoke tests for the Textual app.
2
3Drives the app through its screens with a Pilot, stubbing the network-bound
4run_audit so no real GitHub calls are made. Uses asyncio.run so the suite does
5not require the pytest-asyncio plugin.
6"""
7
8import asyncio
9
10from textual.widgets import Button, Input
11
12from tui import github_runner as gh
13from tui.app import AuditApp, ChecksScreen, ConfigScreen, MenuScreen, RunScreen
14
15
16def _run(coro):
17 asyncio.run(coro)
18
19
20def test_full_navigation(monkeypatch):
21 def fake_run_audit(*, org, token, output_dir, branch, selected_keys, on_event):
22 on_event(gh.ProgressEvent("start", "Member roster"))
23 on_event(gh.ProgressEvent("done", "Member roster", count=3))
24 on_event(gh.ProgressEvent("summary", output_dir, count=3))
25 return [("Member roster", 3)]
26
27 monkeypatch.setattr("tui.github_runner.run_audit", fake_run_audit)
28
29 async def scenario():
30 app = AuditApp()
31 async with app.run_test(size=(120, 40)) as pilot:
32 await pilot.pause()
33 assert isinstance(app.screen, MenuScreen)
34
35 await pilot.click("#github")
36 await pilot.pause()
37 assert isinstance(app.screen, ConfigScreen)
38
39 app.screen.query_one("#org", Input).value = "acme"
40 app.screen.query_one("#token", Input).value = "tok"
41 await pilot.click("#continue")
42 await pilot.pause()
43 assert isinstance(app.screen, ChecksScreen)
44 assert app.settings["org"] == "acme"
45
46 await pilot.click("#run")
47 await pilot.pause()
48 assert isinstance(app.screen, RunScreen)
49
50 await app.workers.wait_for_complete()
51 await pilot.pause()
52
53 # When the run finishes, the exit buttons become enabled.
54 assert app.screen.query_one("#menu", Button).disabled is False
55 assert app.screen.query_one("#quit", Button).disabled is False
56
57 _run(scenario())
58
59
60def test_config_requires_org_and_token(monkeypatch):
61 # Make sure env vars don't pre-fill the fields for this test.
62 monkeypatch.delenv("GITHUB_ORG", raising=False)
63 monkeypatch.delenv("GITHUB_TOKEN", raising=False)
64
65 async def scenario():
66 app = AuditApp()
67 async with app.run_test(size=(120, 40)) as pilot:
68 await pilot.click("#github")
69 await pilot.pause()
70 # Continue with empty fields -> stays on ConfigScreen with an error.
71 await pilot.click("#continue")
72 await pilot.pause()
73 assert isinstance(app.screen, ConfigScreen)
74 error_text = str(app.screen.query_one("#form-error").render())
75 assert "provide" in error_text.lower()
76
77 _run(scenario())
78
79
80def test_gitlab_is_enabled():
81 async def scenario():
82 app = AuditApp()
83 async with app.run_test(size=(120, 40)) as pilot:
84 await pilot.pause()
85 assert app.screen.query_one("#gitlab", Button).disabled is False
86
87 _run(scenario())
88
89
90def test_gitlab_navigation(monkeypatch):
91 monkeypatch.delenv("GITLAB_GROUP", raising=False)
92 monkeypatch.delenv("GITLAB_TOKEN", raising=False)
93
94 def fake_run_audit(*, group, token, base_url, output_dir, selected_keys, on_event):
95 on_event(gh.ProgressEvent("done", "Group members", count=7))
96 on_event(gh.ProgressEvent("summary", output_dir, count=7))
97 return [("Group members", 7)]
98
99 monkeypatch.setattr("tui.gitlab_runner.run_audit", fake_run_audit)
100
101 async def scenario():
102 app = AuditApp()
103 async with app.run_test(size=(120, 40)) as pilot:
104 await pilot.pause()
105 await pilot.click("#gitlab")
106 await pilot.pause()
107 assert isinstance(app.screen, ConfigScreen)
108
109 app.screen.query_one("#group", Input).value = "my-group"
110 app.screen.query_one("#token", Input).value = "glpat-x"
111 await pilot.click("#continue")
112 await pilot.pause()
113 assert isinstance(app.screen, ChecksScreen)
114 assert app.settings["group"] == "my-group"
115 # Self-hosted URL defaults to gitlab.com.
116 assert app.settings["base_url"] == "https://gitlab.com/api/v4"
117
118 await pilot.click("#run")
119 await pilot.pause()
120 assert isinstance(app.screen, RunScreen)
121 assert "gitlab_audit_my-group" in app.screen.output_dir
122
123 await app.workers.wait_for_complete()
124 await pilot.pause()
125 assert app.screen.query_one("#menu", Button).disabled is False
126
127 _run(scenario())
128
129
130def test_aws_navigation(monkeypatch):
131 for var in ("AWS_PROFILE", "AWS_DEFAULT_REGION", "AWS_AUDIT_ACCOUNT"):
132 monkeypatch.delenv(var, raising=False)
133
134 def fake_run_audit(
135 *, profile, region, account, output_dir, selected_keys, on_event
136 ):
137 on_event(gh.ProgressEvent("done", "IAM users", count=5))
138 on_event(gh.ProgressEvent("summary", output_dir, count=5))
139 return [("IAM users", 5)]
140
141 monkeypatch.setattr("tui.aws_runner.run_audit", fake_run_audit)
142
143 async def scenario():
144 app = AuditApp()
145 async with app.run_test(size=(120, 40)) as pilot:
146 await pilot.pause()
147 await pilot.click("#aws")
148 await pilot.pause()
149 assert isinstance(app.screen, ConfigScreen)
150
151 # AWS has no required fields — continue with defaults (default chain).
152 await pilot.click("#continue")
153 await pilot.pause()
154 assert isinstance(app.screen, ChecksScreen)
155 assert app.settings["profile"] == ""
156
157 await pilot.click("#run")
158 await pilot.pause()
159 assert isinstance(app.screen, RunScreen)
160 # Empty profile renders as "default" in the folder name.
161 assert "aws_audit_default" in app.screen.output_dir
162
163 await app.workers.wait_for_complete()
164 await pilot.pause()
165 assert app.screen.query_one("#menu", Button).disabled is False
166
167 _run(scenario())