From 015c5213c8f61505ff521c111cc45ba4288f233a Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 26 Apr 2024 16:17:17 -0700 Subject: [PATCH] Make compatible with <1.0 and >1.0a (#16) * Test on both major Datasette versions * Don't pin to >=1.0a13 * Get working on both Datasette versions, refs #15 * datasette-test>=0.3.2 --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 2 ++ datasette_secrets/__init__.py | 31 +++++++++++++++--------- pyproject.toml | 4 ++-- tests/test_secrets.py | 44 +++++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4b95a4b..bd20811 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ permissions: jobs: test: - runs-on: ubicloud + runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] @@ -28,7 +28,7 @@ jobs: run: | pytest deploy: - runs-on: ubicloud + runs-on: ubuntu-latest needs: [test] environment: release permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 772803b..d20252c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + datasette-version: ["<1.0", ">=1.0a13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -22,6 +23,7 @@ jobs: - name: Install dependencies run: | pip install '.[test]' + pip install "datasette${{ matrix.datasette-version }}" - name: Run tests run: | if [ -d tests/ ]; then diff --git a/datasette_secrets/__init__.py b/datasette_secrets/__init__.py index d733b6a..efe1085 100644 --- a/datasette_secrets/__init__.py +++ b/datasette_secrets/__init__.py @@ -1,7 +1,7 @@ import click from cryptography.fernet import Fernet import dataclasses -from datasette import hookimpl, Forbidden, Permission, Response +from datasette import hookimpl, Forbidden, Response from datasette.plugins import pm from datasette.utils import await_me_maybe, sqlite3 import os @@ -89,7 +89,7 @@ class Secret: def get_database(datasette): plugin_config = datasette.plugin_config("datasette-secrets") or {} database = plugin_config.get("database") or "_internal" - if database == "_internal": + if database == "_internal" and hasattr(datasette, "get_internal_database"): return datasette.get_internal_database() return datasette.get_database(database) @@ -108,6 +108,8 @@ def get_config(datasette): @hookimpl def register_permissions(datasette): + from datasette import Permission + return [ Permission( name="manage-secrets", @@ -187,15 +189,22 @@ async def secrets_index(datasette, request): ) existing_secrets = {row["name"]: dict(row) for row in existing_secrets_result.rows} # Try to turn updated_by into actors - actors = await datasette.actors_from_ids( - {row["updated_by"] for row in existing_secrets.values() if row["updated_by"]} - ) - for secret in existing_secrets.values(): - if secret["updated_by"]: - actor = actors.get(secret["updated_by"]) - if actor: - display = actor.get("username") or actor.get("name") or actor.get("id") - secret["updated_by"] = display + if hasattr(datasette, "actors_from_ids"): + actors = await datasette.actors_from_ids( + { + row["updated_by"] + for row in existing_secrets.values() + if row["updated_by"] + } + ) + for secret in existing_secrets.values(): + if secret["updated_by"]: + actor = actors.get(secret["updated_by"]) + if actor: + display = ( + actor.get("username") or actor.get("name") or actor.get("id") + ) + secret["updated_by"] = display unset_secrets = [ secret for secret in all_secrets diff --git a/pyproject.toml b/pyproject.toml index 45734f5..9d13d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ classifiers=[ ] requires-python = ">=3.8" dependencies = [ - "datasette>=1.0a13", + "datasette", "cryptography" ] @@ -25,7 +25,7 @@ CI = "https://github.com/datasette/datasette-secrets/actions" secrets = "datasette_secrets" [project.optional-dependencies] -test = ["pytest", "pytest-asyncio"] +test = ["pytest", "pytest-asyncio", "datasette-test>=0.3.2"] [tool.pytest.ini_options] asyncio_mode = "strict" diff --git a/tests/test_secrets.py b/tests/test_secrets.py index 5cad008..b22fd1f 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -1,9 +1,9 @@ from click.testing import CliRunner from cryptography.fernet import Fernet from datasette import hookimpl -from datasette.app import Datasette from datasette.cli import cli from datasette.plugins import pm +from datasette_test import Datasette, actor_cookie from datasette_secrets import get_secret, Secret, startup, get_config import pytest from unittest.mock import ANY @@ -11,6 +11,13 @@ TEST_ENCRYPTION_KEY = "-LujHtwFWGaBpznrV1zduoZBmCnMOW7J0H5hmeXgAVo=" +def get_internal_database(ds): + if hasattr(ds, "get_internal_database"): + return ds.get_internal_database() + else: + return ds.get_database("_internal") + + def test_generate_command(): runner = CliRunner() result = runner.invoke(cli, ["secrets", "generate-encryption-key"]) @@ -89,15 +96,13 @@ def register_secrets(self): @pytest.fixture def ds(): return Datasette( - config={ - "plugins": { - "datasette-secrets": { - "database": "_internal", - "encryption-key": TEST_ENCRYPTION_KEY, - } - }, - "permissions": {"manage-secrets": {"id": "admin"}}, - } + plugin_config={ + "datasette-secrets": { + "database": "_internal", + "encryption-key": TEST_ENCRYPTION_KEY, + } + }, + permissions={"manage-secrets": {"id": "admin"}}, ) @@ -115,7 +120,7 @@ async def test_permissions(ds, path, verb, data, user): kwargs = {} if user: kwargs["cookies"] = { - "ds_actor": ds.client.actor_cookie({"id": user}), + "ds_actor": actor_cookie(ds, {"id": user}), } if data: kwargs["data"] = data @@ -131,7 +136,7 @@ async def test_permissions(ds, path, verb, data, user): @pytest.mark.asyncio async def test_set_secret(ds, use_actors_plugin): - cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})} + cookies = {"ds_actor": actor_cookie(ds, {"id": "admin"})} get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies) csrftoken = get_response.cookies["ds_csrftoken"] cookies["ds_csrftoken"] = csrftoken @@ -142,7 +147,7 @@ async def test_set_secret(ds, use_actors_plugin): ) assert post_response.status_code == 302 assert post_response.headers["Location"] == "/-/secrets" - internal_db = ds.get_internal_database() + internal_db = get_internal_database(ds) secrets = await internal_db.execute("select * from datasette_secrets") rows = [dict(r) for r in secrets.rows] assert rows == [ @@ -174,7 +179,12 @@ async def test_set_secret(ds, use_actors_plugin): assert response.status_code == 200 assert "EXAMPLE_SECRET" in response.text assert "new-note" in response.text - assert "ADMIN" in response.text + + if hasattr(ds, "actors_from_ids"): + assert "ADMIN" in response.text + else: + # Pre 1.0, so can't use that mechanism + assert "admin" in response.text # Now let's edit it post_response2 = await ds.client.post( @@ -213,11 +223,11 @@ async def test_set_secret(ds, use_actors_plugin): @pytest.mark.asyncio async def test_get_secret(ds, monkeypatch): # First set it manually - cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})} + cookies = {"ds_actor": actor_cookie(ds, {"id": "admin"})} get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies) csrftoken = get_response.cookies["ds_csrftoken"] cookies["ds_csrftoken"] = csrftoken - db = ds.get_internal_database() + db = get_internal_database(ds) # Reset state await db.execute_write( "update datasette_secrets set last_used_at = null, last_used_by = null" @@ -288,7 +298,7 @@ async def test_secret_index_page(ds, register_multiple_secrets): response = await ds.client.get( "/-/secrets", cookies={ - "ds_actor": ds.client.actor_cookie({"id": "admin"}), + "ds_actor": actor_cookie(ds, {"id": "admin"}), }, ) assert response.status_code == 200