From ec6e8a916b4bc77caafad266bee6f25ce8fd5367 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 02:34:57 +0000 Subject: [PATCH 1/2] Add actor= parameter to datasette.client methods Plugins and tests can now pass actor={"id": "..."} to any datasette.client method as a shorthand for signing a ds_actor cookie via datasette.client.actor_cookie(). Passing both actor= and a ds_actor cookie raises TypeError. Also converts existing tests that went through DatasetteClient to use the new shorthand. Tests that use the synchronous TestClient (app_client-style) and tests that intentionally exercise the ds_actor cookie mechanism are left unchanged. --- datasette/app.py | 15 +++++ docs/internals.rst | 22 +++++++ tests/test_api.py | 3 +- tests/test_column_types.py | 4 +- tests/test_html.py | 20 +++---- tests/test_internals_datasette_client.py | 75 ++++++++++++++++++++++++ tests/test_permission_endpoints.py | 22 ++++--- tests/test_permissions.py | 23 ++++---- tests/test_plugins.py | 6 +- tests/test_schema_endpoints.py | 4 +- tests/test_search_tables.py | 10 ++-- 11 files changed, 153 insertions(+), 51 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index ed62c5282b..d474aecb11 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -2670,9 +2670,23 @@ def _fix(self, path, avoid_path_rewrites=False): path = f"http://localhost{path}" return path + def _apply_actor(self, kwargs): + """If ``actor=`` was supplied, convert it into a signed ds_actor cookie.""" + actor = kwargs.pop("actor", None) + if actor is None: + return + cookies = dict(kwargs.get("cookies") or {}) + if "ds_actor" in cookies: + raise TypeError( + "Cannot pass both actor= and a ds_actor cookie" + ) + cookies["ds_actor"] = self.actor_cookie(actor) + kwargs["cookies"] = cookies + async def _request(self, method, path, skip_permission_checks=False, **kwargs): from datasette.permissions import SkipPermissions + self._apply_actor(kwargs) with _DatasetteClientContext(): if skip_permission_checks: with SkipPermissions(): @@ -2738,6 +2752,7 @@ async def request(self, method, path, skip_permission_checks=False, **kwargs): from datasette.permissions import SkipPermissions avoid_path_rewrites = kwargs.pop("avoid_path_rewrites", None) + self._apply_actor(kwargs) with _DatasetteClientContext(): if skip_permission_checks: with SkipPermissions(): diff --git a/docs/internals.rst b/docs/internals.rst index 06a6b348ec..2c178e4b72 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1312,6 +1312,28 @@ These methods can be used with :ref:`internals_datasette_urls` - for example: For documentation on available ``**kwargs`` options and the shape of the HTTPX Response object refer to the `HTTPX Async documentation `__. +.. _internals_datasette_client_actor: + +Authenticating as an actor +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All ``datasette.client`` methods accept an optional ``actor=`` parameter. When set to a dictionary describing an actor, the request is made with a signed ``ds_actor`` cookie identifying that actor — as if the request had been made by a user who is signed in as that actor. + +This is a convenient shorthand equivalent to signing the cookie manually using ``datasette.client.actor_cookie()``. + +Example usage: + +.. code-block:: python + + response = await datasette.client.get( + "/-/actor.json", actor={"id": "root"} + ) + assert response.json() == {"actor": {"id": "root"}} + +This parameter works with all HTTP methods (``get``, ``post``, ``put``, ``patch``, ``delete``, ``options``, ``head``) and the generic ``request`` method. + +Passing both ``actor=`` and a ``ds_actor`` cookie via ``cookies=`` raises a ``TypeError``. Other unrelated cookies can be combined with ``actor=``. + Bypassing permission checks ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/test_api.py b/tests/test_api.py index 95958a7291..3676c1fb8d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -553,8 +553,7 @@ async def test_actions_json(ds_client): original_root_enabled = ds_client.ds.root_enabled try: ds_client.ds.root_enabled = True - cookies = {"ds_actor": ds_client.actor_cookie({"id": "root"})} - response = await ds_client.get("/-/actions.json", cookies=cookies) + response = await ds_client.get("/-/actions.json", actor={"id": "root"}) data = response.json() finally: ds_client.ds.root_enabled = original_root_enabled diff --git a/tests/test_column_types.py b/tests/test_column_types.py index 68b92a3908..6e89acb959 100644 --- a/tests/test_column_types.py +++ b/tests/test_column_types.py @@ -933,9 +933,7 @@ async def test_set_column_type_ui_data_includes_applicable_types( await ds_ct_editor_permission.invoke_startup() response = await ds_ct_editor_permission.client.get( "/data/posts", - cookies={ - "ds_actor": ds_ct_editor_permission.client.actor_cookie({"id": "editor"}) - }, + actor={"id": "editor"}, ) assert response.status_code == 200 data = _window_data_from_html(response.text, "_setColumnTypeData") diff --git a/tests/test_html.py b/tests/test_html.py index 39249c19e7..25ed08712a 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1003,10 +1003,10 @@ async def test_navigation_menu_links( # Enable root user if testing with root actor if actor_id == "root": ds_client.ds.root_enabled = True - cookies = {} + kwargs = {} if actor_id: - cookies = {"ds_actor": ds_client.actor_cookie({"id": actor_id})} - html = (await ds_client.get("/", cookies=cookies)).text + kwargs["actor"] = {"id": actor_id} + html = (await ds_client.get("/", **kwargs)).text soup = Soup(html, "html.parser") details = soup.find("nav").find("details") if not actor_id: @@ -1214,8 +1214,7 @@ async def test_actions_page(ds_client): original_root_enabled = ds_client.ds.root_enabled try: ds_client.ds.root_enabled = True - cookies = {"ds_actor": ds_client.actor_cookie({"id": "root"})} - response = await ds_client.get("/-/actions", cookies=cookies) + response = await ds_client.get("/-/actions", actor={"id": "root"}) assert response.status_code == 200 assert "Registered actions" in response.text assert "Name" in response.text @@ -1232,8 +1231,7 @@ async def test_actions_page_does_not_display_none_string(ds_client): original_root_enabled = ds_client.ds.root_enabled try: ds_client.ds.root_enabled = True - cookies = {"ds_actor": ds_client.actor_cookie({"id": "root"})} - response = await ds_client.get("/-/actions", cookies=cookies) + response = await ds_client.get("/-/actions", actor={"id": "root"}) assert response.status_code == 200 assert "None" not in response.text finally: @@ -1246,11 +1244,11 @@ async def test_permission_debug_tabs_with_query_string(ds_client): original_root_enabled = ds_client.ds.root_enabled try: ds_client.ds.root_enabled = True - cookies = {"ds_actor": ds_client.actor_cookie({"id": "root"})} + actor = {"id": "root"} # Test /-/allowed with query string response = await ds_client.get( - "/-/allowed?action=view-table&page_size=50", cookies=cookies + "/-/allowed?action=view-table&page_size=50", actor=actor ) assert response.status_code == 200 # Check that Rules and Check tabs have the query string @@ -1262,7 +1260,7 @@ async def test_permission_debug_tabs_with_query_string(ds_client): # Test /-/rules with query string response = await ds_client.get( - "/-/rules?action=view-database&parent=test", cookies=cookies + "/-/rules?action=view-database&parent=test", actor=actor ) assert response.status_code == 200 # Check that Allowed and Check tabs have the query string @@ -1270,7 +1268,7 @@ async def test_permission_debug_tabs_with_query_string(ds_client): assert 'href="/-/check?action=view-database&parent=test"' in response.text # Test /-/check with query string - response = await ds_client.get("/-/check?action=execute-sql", cookies=cookies) + response = await ds_client.get("/-/check?action=execute-sql", actor=actor) assert response.status_code == 200 # Check that Allowed and Rules tabs have the query string assert 'href="/-/allowed?action=execute-sql"' in response.text diff --git a/tests/test_internals_datasette_client.py b/tests/test_internals_datasette_client.py index 326fcdc0e1..bd88eb39bf 100644 --- a/tests/test_internals_datasette_client.py +++ b/tests/test_internals_datasette_client.py @@ -311,3 +311,78 @@ async def test_view(datasette): assert all(in_client_values), f"Expected all True, got {in_client_values}" finally: ds.pm.unregister(name="test_in_client_skip_plugin") + + +@pytest.mark.asyncio +async def test_actor_parameter_sets_cookie(datasette): + """Passing actor= should sign a ds_actor cookie and authenticate the request.""" + response = await datasette.client.get( + "/-/actor.json", actor={"id": "root"} + ) + assert response.status_code == 200 + assert response.json() == {"actor": {"id": "root"}} + + +@pytest.mark.asyncio +async def test_actor_parameter_works_with_request_method(datasette): + response = await datasette.client.request( + "GET", "/-/actor.json", actor={"id": "root"} + ) + assert response.status_code == 200 + assert response.json() == {"actor": {"id": "root"}} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "method", ["get", "post", "options", "head", "put", "patch", "delete"] +) +async def test_actor_parameter_all_http_methods(datasette, method): + """actor= should not cause errors on any HTTP verb wrapper.""" + client_method = getattr(datasette.client, method) + # Just verify no TypeError about unexpected 'actor' kwarg + response = await client_method("/", actor={"id": "root"}) + assert isinstance(response, httpx.Response) + + +@pytest.mark.asyncio +async def test_actor_parameter_conflicts_with_ds_actor_cookie(datasette): + """Passing both actor= and a ds_actor cookie should raise TypeError.""" + with pytest.raises(TypeError, match="actor"): + await datasette.client.get( + "/-/actor.json", + actor={"id": "root"}, + cookies={"ds_actor": datasette.client.actor_cookie({"id": "other"})}, + ) + + +@pytest.mark.asyncio +async def test_actor_parameter_merges_with_other_cookies(datasette): + """actor= should coexist with unrelated cookies.""" + response = await datasette.client.get( + "/-/actor.json", + actor={"id": "root"}, + cookies={"unrelated": "value"}, + ) + assert response.status_code == 200 + assert response.json() == {"actor": {"id": "root"}} + + +@pytest.mark.asyncio +async def test_actor_parameter_with_skip_permission_checks( + datasette_with_permissions, +): + """actor= should be compatible with skip_permission_checks.""" + ds = datasette_with_permissions + # Non-admin actor with skip_permission_checks=True should get 200 + response = await ds.client.get( + "/test_db.json", + actor={"id": "user"}, + skip_permission_checks=True, + ) + assert response.status_code == 200 + # Admin actor on its own should also get 200 + response = await ds.client.get("/test_db.json", actor={"id": "admin"}) + assert response.status_code == 200 + # Non-admin actor should get 403 + response = await ds.client.get("/test_db.json", actor={"id": "user"}) + assert response.status_code == 403 diff --git a/tests/test_permission_endpoints.py b/tests/test_permission_endpoints.py index 84f3370fb6..e25be23ec3 100644 --- a/tests/test_permission_endpoints.py +++ b/tests/test_permission_endpoints.py @@ -117,9 +117,7 @@ async def test_allowed_json_with_actor(ds_with_permissions): """Test /-/allowed.json includes actor information.""" response = await ds_with_permissions.client.get( "/-/allowed.json?action=view-table", - cookies={ - "ds_actor": ds_with_permissions.client.actor_cookie({"id": "test_user"}) - }, + actor={"id": "test_user"}, ) assert response.status_code == 200 data = response.json() @@ -252,7 +250,7 @@ async def test_rules_json_basic( # Use root actor for rules endpoint (requires permissions-debug) response = await ds_with_permissions.client.get( path, - cookies={"ds_actor": ds_with_permissions.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == expected_status data = response.json() @@ -264,7 +262,7 @@ async def test_rules_json_response_structure(ds_with_permissions): """Test that /-/rules.json returns the expected structure.""" response = await ds_with_permissions.client.get( "/-/rules.json?action=view-instance", - cookies={"ds_actor": ds_with_permissions.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -294,7 +292,7 @@ async def test_rules_json_includes_all_rules(ds_with_permissions): # Root user should see rules for everything response = await ds_with_permissions.client.get( "/-/rules.json?action=view-table", - cookies={"ds_actor": ds_with_permissions.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -326,7 +324,7 @@ async def test_rules_json_pagination(): # Test basic pagination structure - just verify it returns paginated results response = await ds.client.get( "/-/rules.json?action=view-table&page_size=2&page=1", - cookies={"ds_actor": ds.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -343,7 +341,7 @@ async def test_rules_json_with_actor(ds_with_permissions): # Use root actor (rules endpoint requires permissions-debug) response = await ds_with_permissions.client.get( "/-/rules.json?action=view-table", - cookies={"ds_actor": ds_with_permissions.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -374,7 +372,7 @@ async def test_root_user_respects_settings_deny(): # Root user should NOT see the denied database response = await ds.client.get( "/-/allowed.json?action=view-database", - cookies={"ds_actor": ds.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -415,7 +413,7 @@ async def test_root_user_respects_settings_deny_tables(): # Root user should NOT see tables from the content database response = await ds.client.get( "/-/allowed.json?action=view-table", - cookies={"ds_actor": ds.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() @@ -475,7 +473,7 @@ def permission_resources_sql(self, datasette, actor, action): # User should NOT have execute-sql permission because view-database is denied response = await ds.client.get( "/-/allowed.json?action=execute-sql", - cookies={"ds_actor": ds.client.actor_cookie({"id": "test_user"})}, + actor={"id": "test_user"}, ) assert response.status_code == 200 data = response.json() @@ -491,7 +489,7 @@ def permission_resources_sql(self, datasette, actor, action): # (may be 403 or 302 redirect to login/error page depending on middleware) response = await ds.client.get( "/secret?sql=SELECT+1", - cookies={"ds_actor": ds.client.actor_cookie({"id": "test_user"})}, + actor={"id": "test_user"}, ) assert response.status_code in (302, 403), ( f"Expected 302 or 403 when trying to execute SQL without view-database permission, " diff --git a/tests/test_permissions.py b/tests/test_permissions.py index f93037592c..09610a3b42 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1176,10 +1176,10 @@ async def test_api_explorer_visibility( try: prev_config = perms_ds.config perms_ds.config = config or {} - cookies = {} + kwargs = {} if is_logged_in: - cookies = {"ds_actor": perms_ds.client.actor_cookie({"id": "user"})} - response = await perms_ds.client.get("/-/api", cookies=cookies) + kwargs["actor"] = {"id": "user"} + response = await perms_ds.client.get("/-/api", **kwargs) if expected_visible_tables: assert response.status_code == 200 # Search HTML for stuff matching: @@ -1213,8 +1213,7 @@ async def test_view_table_token_cannot_gain_access_without_base_permission(perms # Restricted token claims access to perms_ds_two/t1 only "_r": {"r": {"perms_ds_two": {"t1": ["vt"]}}}, } - cookies = {"ds_actor": perms_ds.client.actor_cookie(actor)} - response = await perms_ds.client.get("/perms_ds_two/t1.json", cookies=cookies) + response = await perms_ds.client.get("/perms_ds_two/t1.json", actor=actor) assert response.status_code == 403 finally: perms_ds.config = previous_config @@ -1333,7 +1332,7 @@ async def test_actor_restrictions( if restrictions: actor["_r"] = restrictions method = getattr(perms_ds.client, verb) - kwargs = {"cookies": {"ds_actor": perms_ds.client.actor_cookie(actor)}} + kwargs = {"actor": actor} if body: kwargs["json"] = body perms_ds._permission_checks.clear() @@ -1464,7 +1463,7 @@ async def test_actor_restrictions_do_not_expand_allowed_resources(perms_ds): # And explicit permission checks should still deny response = await perms_ds.client.get( "/perms_ds_one/t1.json", - cookies={"ds_actor": perms_ds.client.actor_cookie(actor)}, + actor=actor, ) assert response.status_code == 403 finally: @@ -1532,18 +1531,17 @@ async def test_actor_restrictions_json_endpoints_show_filtered_listings(perms_ds """Test that /.json and /db.json show correct filtered listings - issue #2534""" actor = {"id": "user", "_r": {"r": {"perms_ds_one": {"t1": ["vt"]}}}} - cookies = {"ds_actor": perms_ds.client.actor_cookie(actor)} # /.json should be 403 (no view-instance permission) - response = await perms_ds.client.get("/.json", cookies=cookies) + response = await perms_ds.client.get("/.json", actor=actor) assert response.status_code == 403 # /perms_ds_one.json should be 403 (no view-database permission) - response = await perms_ds.client.get("/perms_ds_one.json", cookies=cookies) + response = await perms_ds.client.get("/perms_ds_one.json", actor=actor) assert response.status_code == 403 # /perms_ds_one/t1.json should be 200 - response = await perms_ds.client.get("/perms_ds_one/t1.json", cookies=cookies) + response = await perms_ds.client.get("/perms_ds_one/t1.json", actor=actor) assert response.status_code == 200 @@ -1552,10 +1550,9 @@ async def test_actor_restrictions_view_instance_only(perms_ds): """Test actor restricted to view-instance only - issue #2534""" actor = {"id": "user", "_r": {"a": ["vi"]}} - cookies = {"ds_actor": perms_ds.client.actor_cookie(actor)} # /.json should be 200 (has view-instance permission) - response = await perms_ds.client.get("/.json", cookies=cookies) + response = await perms_ds.client.get("/.json", actor=actor) assert response.status_code == 200 # But no databases should be visible (no view-database permission) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 4ce2c7c0e5..55af8ad3d3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1020,7 +1020,7 @@ async def test_hook_view_actions(ds_client): assert get_actions_links(response.text) == [] response_2 = await ds_client.get( "/fixtures/simple_view", - cookies={"ds_actor": ds_client.actor_cookie({"id": "bob"})}, + actor={"id": "bob"}, ) assert ">View actions<" in response_2.text assert sorted( @@ -1084,7 +1084,7 @@ async def test_hook_row_actions(ds_client): response_2 = await ds_client.get( "/fixtures/facet_cities/1", - cookies={"ds_actor": ds_client.actor_cookie({"id": "sam"})}, + actor={"id": "sam"}, ) assert get_actions_links(response_2.text) == [ { @@ -1113,7 +1113,7 @@ async def test_hook_homepage_actions(ds_client): assert "Homepage actions" not in response.text # Signed in user gets an action response2 = await ds_client.get( - "/", cookies={"ds_actor": ds_client.actor_cookie({"id": "troy"})} + "/", actor={"id": "troy"} ) assert "Homepage actions" in response2.text assert get_actions_links(response2.text) == [ diff --git a/tests/test_schema_endpoints.py b/tests/test_schema_endpoints.py index 50742df2cd..c95d8614f6 100644 --- a/tests/test_schema_endpoints.py +++ b/tests/test_schema_endpoints.py @@ -151,7 +151,7 @@ async def test_schema_permission_enforcement(schema_ds, url): # Authenticated user with permission should succeed response = await schema_ds.client.get( url, - cookies={"ds_actor": schema_ds.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 @@ -171,7 +171,7 @@ async def test_instance_schema_respects_database_permissions(schema_ds): # Authenticated user should see all databases response = await schema_ds.client.get( "/-/schema.json", - cookies={"ds_actor": schema_ds.client.actor_cookie({"id": "root"})}, + actor={"id": "root"}, ) assert response.status_code == 200 data = response.json() diff --git a/tests/test_search_tables.py b/tests/test_search_tables.py index 34b377067d..b901c0b370 100644 --- a/tests/test_search_tables.py +++ b/tests/test_search_tables.py @@ -86,7 +86,7 @@ async def test_tables_search_with_auth(ds_with_tables): # Editor user should see content.articles response = await ds_with_tables.client.get( "/-/tables.json?q=articles", - cookies={"ds_actor": ds_with_tables.client.actor_cookie({"id": "editor"})}, + actor={"id": "editor"}, ) assert response.status_code == 200 data = response.json() @@ -104,7 +104,7 @@ async def test_tables_search_partial_match(ds_with_tables): # Search for "com" should match "comments" response = await ds_with_tables.client.get( "/-/tables.json?q=com", - cookies={"ds_actor": ds_with_tables.client.actor_cookie({"id": "user"})}, + actor={"id": "user"}, ) assert response.status_code == 200 data = response.json() @@ -120,7 +120,7 @@ async def test_tables_search_respects_database_permissions(ds_with_tables): # Even authenticated users shouldn't see it because database is denied response = await ds_with_tables.client.get( "/-/tables.json?q=secrets", - cookies={"ds_actor": ds_with_tables.client.actor_cookie({"id": "user"})}, + actor={"id": "user"}, ) assert response.status_code == 200 data = response.json() @@ -135,7 +135,7 @@ async def test_tables_search_respects_table_permissions(ds_with_tables): # Regular authenticated user searching for "users" response = await ds_with_tables.client.get( "/-/tables.json?q=users", - cookies={"ds_actor": ds_with_tables.client.actor_cookie({"id": "regular"})}, + actor={"id": "regular"}, ) assert response.status_code == 200 data = response.json() @@ -150,7 +150,7 @@ async def test_tables_search_response_structure(ds_with_tables): """Test that response has correct structure.""" response = await ds_with_tables.client.get( "/-/tables.json?q=users", - cookies={"ds_actor": ds_with_tables.client.actor_cookie({"id": "user"})}, + actor={"id": "user"}, ) assert response.status_code == 200 data = response.json() From 5ed15afdda4e6ed4a05f7b0afacfd58835e89b6b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 14 Apr 2026 18:26:13 -0700 Subject: [PATCH 2/2] Apply black formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- datasette/app.py | 4 +--- tests/test_internals_datasette_client.py | 4 +--- tests/test_plugins.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 8db6737b49..16545cff98 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -2661,9 +2661,7 @@ def _apply_actor(self, kwargs): return cookies = dict(kwargs.get("cookies") or {}) if "ds_actor" in cookies: - raise TypeError( - "Cannot pass both actor= and a ds_actor cookie" - ) + raise TypeError("Cannot pass both actor= and a ds_actor cookie") cookies["ds_actor"] = self.actor_cookie(actor) kwargs["cookies"] = cookies diff --git a/tests/test_internals_datasette_client.py b/tests/test_internals_datasette_client.py index bd88eb39bf..ccac280b93 100644 --- a/tests/test_internals_datasette_client.py +++ b/tests/test_internals_datasette_client.py @@ -316,9 +316,7 @@ async def test_view(datasette): @pytest.mark.asyncio async def test_actor_parameter_sets_cookie(datasette): """Passing actor= should sign a ds_actor cookie and authenticate the request.""" - response = await datasette.client.get( - "/-/actor.json", actor={"id": "root"} - ) + response = await datasette.client.get("/-/actor.json", actor={"id": "root"}) assert response.status_code == 200 assert response.json() == {"actor": {"id": "root"}} diff --git a/tests/test_plugins.py b/tests/test_plugins.py index b7f21b4e06..083e23a01f 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1116,9 +1116,7 @@ async def test_hook_homepage_actions(ds_client): # No button for anonymous users assert "Homepage actions" not in response.text # Signed in user gets an action - response2 = await ds_client.get( - "/", actor={"id": "troy"} - ) + response2 = await ds_client.get("/", actor={"id": "troy"}) assert "Homepage actions" in response2.text assert get_actions_links(response2.text) == [ {