Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions campus_cli/audit/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ def traces_search(
api_key_id: Optional[str] = typer.Option(
None, "--api-key-id", help="Filter by audit API key ID"
),
journey: Optional[str] = typer.Option(
None, "--journey", help="Filter by login-journey ID"
),
since: Optional[str] = typer.Option(
None, "--since", help="ISO 8601 lower bound on trace time"
),
Expand All @@ -191,11 +194,9 @@ def traces_search(
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
dry_run: bool = dry_run_option(),
) -> None:
"""
Search traces by endpoint, outcome, principal, or time.
"""Search traces by journey, endpoint, outcome, principal, or time.

Filters combine (AND). The server's login-journey filter is not
exposed here yet — campus-api-python's search() does not carry it.
Filters combine (AND).
"""
if dry_run:
_dry_run(
Expand All @@ -204,6 +205,7 @@ def traces_search(
"path": path, "status": status,
"user_id": user, "client_id": client,
"api_key_id": api_key_id,
"journey_id": journey,
"since": since, "until": until,
"limit": limit, "cursor": cursor,
}),
Expand All @@ -218,6 +220,7 @@ def traces_search(
user_id=user,
client_id=client,
api_key_id=api_key_id,
journey_id=journey,
since=since,
until=until,
limit=limit,
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions tests/integration/test_dry_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,16 @@ def test_audit_traces_search_dry_run():
assert "path='/auth/v1/users/'" in result.stdout
assert "status=500" in result.stdout
assert "user_id='alice@school.edu'" in result.stdout
assert "journey_id" not in result.stdout


def test_audit_traces_search_journey_dry_run():
"""audit traces search --dry-run carries the journey filter (#63)."""
result = _invoke_audit_dry_run(
"audit", "traces", "search", "--journey", "jrn-orientation-2026",
)

assert "journey_id='jrn-orientation-2026'" in result.stdout


def test_dry_run_flag_documented_in_help():
Expand Down Expand Up @@ -396,6 +406,7 @@ def test_audit_dry_run_snippets_are_valid_python():
("audit", "traces", "list"),
("audit", "traces", "list", "--limit", "10", "--since", "2026-10-01"),
("audit", "traces", "search", "--path", "/x", "--status", "500"),
("audit", "traces", "search", "--journey", "jrn-orientation-2026"),
]
for case in cases:
result = _invoke_audit_dry_run(*case)
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,3 +736,33 @@ def test_traces_list_json_keeps_the_envelope(self):
result = CliRunner().invoke(app, ["audit", "traces", "list", "--json"])
assert result.exit_code == 0, result.output
assert json.loads(result.stdout) == server_response


class TestTracesSearchJourney:
"""traces search forwards --journey as the journey_id kwarg (#63)."""

def _invoke_search(self, args):
api = mock.Mock()
api.traces.search.return_value = {"traces": []}
with (
mock.patch("campus_cli.audit.traces.get_audit_client", return_value=api),
mock.patch.dict("os.environ", {"CAMPUS_AUDIT_URL": AUDIT_URL}),
):
result = CliRunner().invoke(app, ["audit", "traces", "search", *args])
return result, api

def test_journey_is_forwarded_as_journey_id(self):
result, api = self._invoke_search([
"--journey", "jrn-orientation-2026", "--path", "/auth/v1/token",
])

assert result.exit_code == 0, result.output
assert api.traces.search.call_args.kwargs["journey_id"] == \
"jrn-orientation-2026"

def test_absent_journey_is_none(self):
"""Without --journey, journey_id is passed as None like the rest."""
result, api = self._invoke_search(["--user", "alice@school.edu"])

assert result.exit_code == 0, result.output
assert api.traces.search.call_args.kwargs["journey_id"] is None
Loading