Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

166 lines
6.5 KiB
Python

"""CLI integration tests for the ``share`` command group.
These tests exercise the full CLI -> Client -> RPC path using VCR cassettes,
covering the sharing happy paths flagged by issue #1316. The issue names them
``add`` / ``list`` / ``revoke``; the real CLI surface (``cli/share_cmd.py``)
exposes them as:
* ``share add <email>`` -> add a collaborator (issue's "add")
* ``share status`` -> list collaborators (issue's "list")
* ``share remove <email>`` -> revoke access (issue's "revoke")
RPC fan-out per command
-----------------------
``client.sharing`` issues these RPCs (see ``src/notebooklm/_sharing.py``):
* ``status`` -> one ``GET_SHARE_STATUS`` (``JFMDGd``).
* ``add`` -> ``SHARE_NOTEBOOK`` (``QDyure``) then ``GET_SHARE_STATUS`` to
re-read the updated user list.
* ``remove`` -> ``SHARE_NOTEBOOK`` (``QDyure``) then ``GET_SHARE_STATUS``.
Each command resolves its notebook via a full UUID passed with ``-n`` so
``resolve_notebook_id`` skips the ``LIST_NOTEBOOKS`` preflight (mirrors the
``mock_context`` docstring in ``conftest.py``). The cassettes therefore hold
only the sharing RPC chain above.
Privacy
-------
Sharing responses embed real account email addresses + display names. The
cassettes here were recorded against a throwaway notebook and then scrubbed:
the recording owner's address collapses to ``SCRUBBED_EMAIL@example.com`` and
the display name to ``SCRUBBED_NAME`` via the email/display-name scrubbers in
``tests/cassette_patterns.py``. The synthetic collaborator used for
``add`` / ``remove`` is an ``@example.com`` address (a reserved,
non-routable domain), recorded with ``--no-notify`` so no real email is sent.
Because the email/name scrubbers rewrite the recorded user list, replay
assertions deliberately stay structural (exit code + JSON shape) rather than
asserting on a specific scrubbed email value.
Recording (maintainer, with a valid profile)::
NOTEBOOKLM_VCR_RECORD=1 uv run pytest \\
tests/integration/cli_vcr/test_share.py -m vcr
"""
import pytest
from notebooklm.notebooklm_cli import cli
from ._fixtures import VCR_SHARE_NOTEBOOK_ID
from .conftest import assert_command_success, notebooklm_vcr, parse_json_output, skip_no_cassettes
pytestmark = [pytest.mark.vcr, skip_no_cassettes]
# ``VCR_SHARE_NOTEBOOK_ID`` is an alias of ``MUTATION_NOTEBOOK_ID`` in
# ``_fixtures`` (the same alias ``test_notebook.py`` uses); a full UUID passed
# with ``-n`` keeps ``resolve_notebook_id`` on its fast path so each cassette
# captures only the sharing RPC chain. The value is never matched against the
# recorded body (VCR matches batchexecute on ``rpcids`` + decoded shape).
# Synthetic, non-routable collaborator address (RFC 2606 reserved domain).
VCR_SHARE_EMAIL = "vcr-share-test@example.com"
class TestShareStatusCommand:
"""Test ``notebooklm share status`` (lists collaborators)."""
@notebooklm_vcr.use_cassette("cli_share_status.yaml")
def test_share_status(self, runner, mock_auth_for_vcr):
"""``share status`` renders the sharing summary from one RPC."""
result = runner.invoke(cli, ["share", "status", "-n", VCR_SHARE_NOTEBOOK_ID])
assert_command_success(result, allow_no_context=False)
@notebooklm_vcr.use_cassette("cli_share_status.yaml")
def test_share_status_json(self, runner, mock_auth_for_vcr):
"""``share status --json`` emits the machine-readable sharing payload."""
result = runner.invoke(cli, ["share", "status", "-n", VCR_SHARE_NOTEBOOK_ID, "--json"])
assert_command_success(result, allow_no_context=False)
data = parse_json_output(result.output)
assert isinstance(data, dict), f"Expected JSON object, got: {result.output!r}"
assert data.get("notebook_id") == VCR_SHARE_NOTEBOOK_ID
assert "is_public" in data
assert isinstance(data.get("shared_users"), list)
class TestShareAddCommand:
"""Test ``notebooklm share add <email>``."""
@notebooklm_vcr.use_cassette("cli_share_add.yaml")
def test_share_add(self, runner, mock_auth_for_vcr):
"""``share add --no-notify`` runs SHARE_NOTEBOOK + GET_SHARE_STATUS."""
result = runner.invoke(
cli,
[
"share",
"add",
VCR_SHARE_EMAIL,
"-n",
VCR_SHARE_NOTEBOOK_ID,
"--permission",
"viewer",
"--no-notify",
],
)
assert_command_success(result, allow_no_context=False)
@notebooklm_vcr.use_cassette("cli_share_add.yaml")
def test_share_add_json(self, runner, mock_auth_for_vcr):
"""``share add --json`` reports the added user + permission."""
result = runner.invoke(
cli,
[
"share",
"add",
VCR_SHARE_EMAIL,
"-n",
VCR_SHARE_NOTEBOOK_ID,
"--permission",
"viewer",
"--no-notify",
"--json",
],
)
assert_command_success(result, allow_no_context=False)
data = parse_json_output(result.output)
assert isinstance(data, dict), f"Expected JSON object, got: {result.output!r}"
assert data.get("added_user") == VCR_SHARE_EMAIL
assert data.get("permission") == "viewer"
assert data.get("notified") is False
class TestShareRemoveCommand:
"""Test ``notebooklm share remove <email>`` (revoke access)."""
@notebooklm_vcr.use_cassette("cli_share_remove.yaml")
def test_share_remove(self, runner, mock_auth_for_vcr):
"""``share remove --yes`` runs SHARE_NOTEBOOK + GET_SHARE_STATUS."""
result = runner.invoke(
cli,
["share", "remove", VCR_SHARE_EMAIL, "-n", VCR_SHARE_NOTEBOOK_ID, "--yes"],
)
assert_command_success(result, allow_no_context=False)
@notebooklm_vcr.use_cassette("cli_share_remove.yaml")
def test_share_remove_json(self, runner, mock_auth_for_vcr):
"""``share remove --yes --json`` reports the revoked user."""
result = runner.invoke(
cli,
[
"share",
"remove",
VCR_SHARE_EMAIL,
"-n",
VCR_SHARE_NOTEBOOK_ID,
"--yes",
"--json",
],
)
assert_command_success(result, allow_no_context=False)
data = parse_json_output(result.output)
assert isinstance(data, dict), f"Expected JSON object, got: {result.output!r}"
assert data.get("removed_user") == VCR_SHARE_EMAIL