From f8d29814dc68e707445695bfdf5775b6b0772fef Mon Sep 17 00:00:00 2001 From: JS Date: Tue, 20 Oct 2026 22:00:47 +0800 Subject: [PATCH] feat(cli): --journey flag for campus audit traces search (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #55 omitted the server's login-journey filter because campus-api-python's Traces.search() did not carry it, leaving the --dry-run snippets unable to mention journey_id honestly. The SDK now ships the keyword-only journey_id filter (campus-api-python#110, merged via #111), matching /audit/v1/traces/search?journey_id=… (campus#803, campus#812 on weekly). - add --journey to traces search, forwarded as journey_id= - include journey_id in --dry-run kwargs (None dropped by _query_operation, like the other filters) - drop the "not exposed here yet" docstring caveat - relock campus-api-python to dad0442 (old pin 89df17e rejects the kwarg; targeted `poetry update`, campus-suite untouched) Tests: unit passthrough via mocked client (journey_id set / None when absent) plus dry-run snippet assertions mirroring the existing search coverage. Suite: 296 passed; ruff clean. --- campus_cli/audit/traces.py | 11 +++++++---- poetry.lock | 2 +- tests/integration/test_dry_run.py | 11 +++++++++++ tests/unit/test_audit.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/campus_cli/audit/traces.py b/campus_cli/audit/traces.py index 5bc444c..1e102cd 100644 --- a/campus_cli/audit/traces.py +++ b/campus_cli/audit/traces.py @@ -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" ), @@ -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( @@ -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, }), @@ -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, diff --git a/poetry.lock b/poetry.lock index a6200c1..5e78c04 100644 --- a/poetry.lock +++ b/poetry.lock @@ -176,7 +176,7 @@ flask = "^3.0.0" type = "git" url = "https://github.com/nyjc-computing/campus-api-python.git" reference = "main" -resolved_reference = "89df17e5774ebd978cb84b103ee8be6642f758bc" +resolved_reference = "dad044252ec9256cf99e5a0f7d9b29ecf81e88fc" [[package]] name = "campus-suite" diff --git a/tests/integration/test_dry_run.py b/tests/integration/test_dry_run.py index ec0a407..59d092a 100644 --- a/tests/integration/test_dry_run.py +++ b/tests/integration/test_dry_run.py @@ -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(): @@ -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) diff --git a/tests/unit/test_audit.py b/tests/unit/test_audit.py index 47c3934..a9b1db6 100644 --- a/tests/unit/test_audit.py +++ b/tests/unit/test_audit.py @@ -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