Repository: mamba-org/quetz (language: python)

Issue:
Move plugins to their own repos and make them `conda` installable
Discoverability and installability of plugins can be improved by moving them to their own repos & conda packages. I would like to try this out with the `quetz-sql-authenticator` plugin.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index e9eca079..235835d5 100644
--- a/README.md
+++ b/README.md
@@ -226,12 +226,6 @@ npm run watch
 
 This will build the javascript files and place them in `/quetz_frontend/dist/` from where they are automatically picked up by the quetz server.
 
-## License
-
-We use a shared copyright model that enables all contributors to maintain the copyright on their contributions.
-
-This software is licensed under the BSD-3-Clause license. See the [LICENSE](LICENSE) file for details.
-
 ## Using quetz
 
 ### Create a channel
@@ -355,3 +349,28 @@ curl -X PUT localhost:8000/api/channels/mirror-channel/actions \
 ```
 
 Only channel owners or maintainers are allowed to trigger synchronisation, therefore you have to provide a valid API key of a privileged user.
+
+## Plugins
+
+Quetz offers plugins in the plugins folder of this repo as well as via standalone installs. The following plugins are currently available:
+
+| Plugin                                                                          | Description                                                                                                                |
+| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| [quetz_conda_suggest](plugins/quetz_conda_suggest/)                             | Generate `.map` files for [conda-suggest](https://github.com/conda-incubator/conda-suggest)                                |
+| [quetz_content_trust](plugins/quetz_content_trust/)                             | Generate signed repodata files                                                                                             |
+| [quetz_current_repodata](plugins/quetz_current_repodata/)                       | Trim the repodata to only include latest package versions                                                                  |
+| [quetz_dictauthenticator](plugins/quetz_dictauthenticator/)                     | Demo for creating new authenticators                                                                                       |
+| [quetz_harvester](plugins/quetz_harvester/)                                     | Extract additional metadata from packages using the [libcflib](https://github.com/regro/libcflib) harvester                |
+| [quetz_mamba_solve](plugins/quetz_mamba_solve/)                                 | Export a specific set of package versions for reproducibility                                                              |
+| [quetz_repodata_patching](plugins/quetz_repodata_patching/)                     | [Repodata patching](https://docs.conda.io/projects/conda-build/en/latest/concepts/generating-index.html#repodata-patching) |
+| [quetz_repodata_zchunk](plugins/quetz_repodata_zchunk/)                         | Serve repodata using [zchunk](https://github.com/zchunk/zchunk)                                                            |
+| [quetz_runexports](plugins/quetz_runexports/)                                   | Extract and expose `run_exports` from package files                                                                        |
+| [quetz_sql_authenticator](https://github.com/mamba-org/quetz-sql-authenticator) | An authenticator that stores credentials in the Quetz SQL database using passlib.                                          |
+| [quetz_tos](plugins/quetz_tos/)                                                 | Enforce signing the terms of service for Quetz users                                                                       |
+| [quetz_transmutation](plugins/quetz_transmutation/)                             | Convert packages to .conda format                                                                                          |
+
+## License
+
+We use a shared copyright model that enables all contributors to maintain the copyright on their contributions.
+
+This software is licensed under the BSD-3-Clause license. See the [LICENSE](LICENSE) file for details.
diff --git a/plugins/quetz_sql_authenticator/README.md b/plugins/quetz_sql_authenticator/README.md
deleted file mode 100644
index 897144e7..00000000
--- a/plugins/quetz_sql_authenticator/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-## SQL Authenticator
-
-An authenticator that stores credentials in the Quetz SQL database using passlib. It ships with REST routets for CRUD operations on the credentials table.
-
-### Installation
-
-```
-quetz plugin install plugins/quetz_sql_authenticator
-```
-
-### Usage
-
-The authenticator should be active now. You can login by navigating to `<HOST>/auth/sql/login`.
-
-### CRUD operations
-
-The authenticator provides REST routes to create, update, and delete credentials and to reset the entire table.
-
-`GET /api/sqlauth/credentials/`: List all users.
-
-`GET /api/sqlauth/credentials/{username}`: Verify that a user exists.
-
-`POST /api/sqlauth/credentials/{username}?password={password}`: Create a new user.
-
-`PUT /api/sqlauth/credentials/{username}?password={password}`: Update a user's password.
-
-`DELETE /api/sqlauth/credentials/{username}`: Delete a user.
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/__init__.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/api.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/api.py
deleted file mode 100644
index 4247739e..00000000
--- a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/api.py
+++ /dev/null
@@ -1,134 +0,0 @@
-from typing import List
-
-from fastapi import APIRouter, Depends, HTTPException, status
-from passlib.hash import pbkdf2_sha256
-from sqlalchemy.orm.session import Session
-
-from quetz import authorization
-from quetz.authorization import SERVER_MAINTAINER, SERVER_OWNER
-from quetz.deps import get_db, get_rules
-
-from .db_models import Credentials
-
-router = APIRouter()
-
-
-def _calculate_hash(value: str) -> str:
-    """Calculate hash from value."""
-    return pbkdf2_sha256.hash(value)
-
-
-@router.get(
-    "/api/sqlauth/credentials/{username}",
-    tags=["sqlauth"],
-)
-def _get(
-    username: str,
-    auth: authorization.Rules = Depends(get_rules),
-    db: Session = Depends(get_db),
-) -> str:
-    """Verify that a specific user exists."""
-    auth.assert_server_roles([SERVER_OWNER, SERVER_MAINTAINER])
-
-    # Get user from db
-    db_credentials = (
-        db.query(Credentials).filter(Credentials.username == username).one_or_none()
-    )
-    if db_credentials is None:
-        raise HTTPException(
-            status_code=status.HTTP_404_NOT_FOUND,
-            detail=f"User {username} not found",
-        )
-
-    return username
-
-
-@router.get(
-    "/api/sqlauth/credentials",
-    tags=["sqlauth"],
-)
-def _get_all(
-    auth: authorization.Rules = Depends(get_rules),
-    db: Session = Depends(get_db),
-) -> List[str]:
-    """List all users."""
-    auth.assert_server_roles([SERVER_OWNER, SERVER_MAINTAINER])
-
-    # Get users from db
-    db_credentials = db.query(Credentials)
-
-    return [c.username for c in db_credentials]
-
-
-@router.post("/api/sqlauth/credentials/{username}", tags=["sqlauth"])
-def _create(
-    username: str,
-    password: str,
-    auth: authorization.Rules = Depends(get_rules),
-    db: Session = Depends(get_db),
-) -> str:
-    """Create a new user."""
-    auth.assert_server_roles([SERVER_OWNER, SERVER_MAINTAINER])
-
-    # Check if user already exists
-    db_credentials = (
-        db.query(Credentials).filter(Credentials.username == username).one_or_none()
-    )
-    if db_credentials is not None:
-        raise HTTPException(
-            status_code=status.HTTP_409_CONFLICT,
-            detail=f"User {username} already exists",
-        )
-
-    credentials = Credentials(
-        username=username, password_hash=_calculate_hash(password)
-    )
-
-    db.add(credentials)
-    db.commit()
-    return username
-
-
-@router.put("/api/sqlauth/credentials/{username}", tags=["sqlauth"])
-def _update(
-    username: str,
-    password: str,
-    auth: authorization.Rules = Depends(get_rules),
-    db: Session = Depends(get_db),
-) -> str:
-    """Update a user's password."""
-    auth.assert_server_roles([SERVER_OWNER, SERVER_MAINTAINER])
-
-    credentials = (
-        db.query(Credentials).filter(Credentials.username == username).one_or_none()
-    )
-    if credentials is None:
-        raise HTTPException(
-            status_code=status.HTTP_404_NOT_FOUND,
-            detail=f"User {username} not found",
-        )
-    credentials.password_hash = _calculate_hash(password)
-    db.commit()
-    return username
-
-
-@router.delete("/api/sqlauth/credentials/{username}", tags=["sqlauth"])
-def _delete(
-    username: str,
-    auth: authorization.Rules = Depends(get_rules),
-    db: Session = Depends(get_db),
-) -> str:
-    """Delete a user."""
-    auth.assert_server_roles([SERVER_OWNER, SERVER_MAINTAINER])
-
-    credentials = (
-        db.query(Credentials).filter(Credentials.username == username).one_or_none()
-    )
-    if credentials is None:
-        raise HTTPException(
-            status_code=status.HTTP_404_NOT_FOUND,
-            detail=f"User {username} not found",
-        )
-    db.delete(credentials)
-    db.commit()
-    return username
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/db_models.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/db_models.py
deleted file mode 100644
index 58dea895..00000000
--- a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/db_models.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from sqlalchemy import Column, String
-
-from quetz.db_models import Base
-
-
-class Credentials(Base):
-    __tablename__ = "credentials"
-
-    username = Column("username", String, primary_key=True)
-    password_hash = Column("password_hash", String, nullable=False)
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/main.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/main.py
deleted file mode 100644
index 26626d39..00000000
--- a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/main.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from passlib.hash import pbkdf2_sha256
-
-import quetz
-from quetz.authentication.base import SimpleAuthenticator
-from quetz.database import get_db_manager
-
-from .api import router
-from .db_models import Credentials
-
-
-def _verify_hash(value: str, hash: str) -> bool:
-    """Verify value against hash."""
-    return pbkdf2_sha256.verify(value, hash)
-
-
-@quetz.hookimpl
-def register_router():
-    return router
-
-
-class UsernameNotFound(RuntimeError):
-    """Error that is thrown when the username is not found."""
-
-
-def _get_password_hash(username: str) -> str:
-    with get_db_manager() as db:
-        credentials = (
-            db.query(Credentials).filter(Credentials.username == username).one_or_none()
-        )
-        if credentials is None:
-            raise UsernameNotFound(f"Username '{username}' not found.")
-        return credentials.password_hash
-
-
-class SQLAuthenticator(SimpleAuthenticator):
-    """An authenticator that uses a SQLAlchemy backend."""
-
-    provider = "sql"
-
-    async def authenticate(self, request, data, **kwargs):
-        """Authenticate."""
-        try:
-            password_hash = _get_password_hash(data["username"])
-        except UsernameNotFound:
-            return
-        if _verify_hash(data["password"], password_hash):
-            return data["username"]
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/__init__.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/versions/__init__.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/versions/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/versions/db1fe7199ca1_.py b/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/versions/db1fe7199ca1_.py
deleted file mode 100644
index 7f3b8e76..00000000
--- a/plugins/quetz_sql_authenticator/quetz_sql_authenticator/migrations/versions/db1fe7199ca1_.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""empty message
-
-Revision ID: db1fe7199ca1
-Revises:
-Create Date: 2022-10-25 14:37:24.496612
-
-"""
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = 'db1fe7199ca1'
-down_revision = None
-branch_labels = ('quetz-sql-authenticator',)
-depends_on = 'quetz'
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table(
-        'credentials',
-        sa.Column('username', sa.String(), nullable=False),
-        sa.Column('password_hash', sa.String(), nullable=False),
-        sa.PrimaryKeyConstraint('username'),
-    )
-    with op.batch_alter_table('channels', schema=None) as batch_op:
-        batch_op.alter_column(
-            'size',
-            existing_type=sa.INTEGER(),
-            type_=sa.BigInteger(),
-            existing_nullable=True,
-        )
-        batch_op.alter_column(
-            'size_limit',
-            existing_type=sa.INTEGER(),
-            type_=sa.BigInteger(),
-            existing_nullable=True,
-        )
-
-    with op.batch_alter_table('package_versions', schema=None) as batch_op:
-        batch_op.alter_column(
-            'size',
-            existing_type=sa.INTEGER(),
-            type_=sa.BigInteger(),
-            existing_nullable=True,
-        )
-
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('package_versions', schema=None) as batch_op:
-        batch_op.alter_column(
-            'size',
-            existing_type=sa.BigInteger(),
-            type_=sa.INTEGER(),
-            existing_nullable=True,
-        )
-
-    with op.batch_alter_table('channels', schema=None) as batch_op:
-        batch_op.alter_column(
-            'size_limit',
-            existing_type=sa.BigInteger(),
-            type_=sa.INTEGER(),
-            existing_nullable=True,
-        )
-        batch_op.alter_column(
-            'size',
-            existing_type=sa.BigInteger(),
-            type_=sa.INTEGER(),
-            existing_nullable=True,
-        )
-
-    op.drop_table('credentials')
-    # ### end Alembic commands ###
diff --git a/plugins/quetz_sql_authenticator/setup.py b/plugins/quetz_sql_authenticator/setup.py
deleted file mode 100644
index e1f6fd31..00000000
--- a/plugins/quetz_sql_authenticator/setup.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# type: ignore
-
-from setuptools import setup
-
-setup(
-    name="quetz-sql-authenticator",
-    install_requires=["passlib"],
-    entry_points={
-        "quetz": ["quetz-sql-authenticator = quetz_sql_authenticator.main"],
-        "quetz.authenticator": [
-            "sql-authenticator = quetz_sql_authenticator.main:SQLAuthenticator"
-        ],
-        "console_scripts": [
-            "quetz-sql-authenticator = quetz_sql_authenticator.cli:_cli"
-        ],
-        "quetz.migrations": [
-            "quetz-sql-authenticator = quetz_sql_authenticator.migrations"
-        ],
-        "quetz.models": ["quetz-sql-authenticator = quetz_sql_authenticator.db_models"],
-    },
-    packages=[
-        "quetz_sql_authenticator",
-        "quetz_sql_authenticator.migrations",
-        "quetz_sql_authenticator.migrations.versions",
-    ],
-)


Test patch:
diff --git a/plugins/quetz_sql_authenticator/tests/conftest.py b/plugins/quetz_sql_authenticator/tests/conftest.py
deleted file mode 100644
index c4ff9aa8..00000000
--- a/plugins/quetz_sql_authenticator/tests/conftest.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import uuid
-
-import pytest
-from sqlalchemy import event
-
-from quetz.authorization import SERVER_MEMBER, SERVER_OWNER
-from quetz.db_models import Profile, User
-
-pytest_plugins = "quetz.testing.fixtures"
-
-
-@pytest.fixture
-def plugins():
-    return ["quetz-sql-authenticator"]
-
-
-@pytest.fixture
-def testuser():
-    return "testuser"
-
-
-@pytest.fixture
-def testpassword():
-    return "testpassword"
-
-
-# Fixture taken from the pytest_harvester plugin
-@pytest.fixture
-def owner_role():
-    return SERVER_OWNER
-
-
-@pytest.fixture
-def member_role():
-    return SERVER_MEMBER
-
-
-@pytest.fixture
-def owner_user(db, owner_role):
-    yield _create_user(db, owner_role, "test_owner")
-    _delete_user(db, "test_owner")
-
-
-@pytest.fixture
-def member_user(db, member_role):
-    yield _create_user(db, member_role, "test_member")
-    _delete_user(db, "test_member")
-
-
-@pytest.fixture
-def owner_client(client, owner_user):
-    return _create_auth_client(client, owner_user)
-
-
-@pytest.fixture
-def member_client(client, member_user):
-    return _create_auth_client(client, member_user)
-
-
-def _create_auth_client(client, user):
-    """authenticated client"""
-    response = client.get(f"/api/dummylogin/{user.username}")
-    assert response.status_code == 200
-    return client
-
-
-def _create_user(db, user_role, username):
-    new_user = User(id=uuid.uuid4().bytes, username=username, role=user_role)
-    profile = Profile(name=username, avatar_url="http:///avatar", user=new_user)
-    db.add(profile)
-    db.add(new_user)
-    db.commit()
-
-    return new_user
-
-
-def _delete_user(db, username):
-    user = db.query(User).filter(User.username == username).one()
-    db.delete(user)
-    db.commit()
-
-
-@pytest.fixture
-def db(session_maker, expires_on_commit, auto_rollback, request):
-    session = session_maker()
-
-    # We overwrite this fixture to support rollbacks within
-    # the test by wrapping the test in a nested transaction.
-    # We start a new nested transaction when a transaction
-    # ends using the `after_transaction_end` event.
-    # See https://docs.sqlalchemy.org/en/13/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites # noqa
-    # for why this is necessary and how this works.
-
-    if auto_rollback:
-        session.begin_nested()
-
-        # each time the nested transaction ends, reopen it
-        @event.listens_for(session, "after_transaction_end")
-        def restart_savepoint(session, transaction):
-            if transaction.nested and not transaction._parent.nested:
-                session.expire_all()
-                session.begin_nested()
-
-    session.expire_on_commit = expires_on_commit
-    yield session
-    session.close()
diff --git a/plugins/quetz_sql_authenticator/tests/test_quetz_sql_authenticator.py b/plugins/quetz_sql_authenticator/tests/test_quetz_sql_authenticator.py
deleted file mode 100644
index e822d8e3..00000000
--- a/plugins/quetz_sql_authenticator/tests/test_quetz_sql_authenticator.py
+++ /dev/null
@@ -1,245 +0,0 @@
-from passlib.hash import pbkdf2_sha256
-from quetz_sql_authenticator.api import _calculate_hash
-from quetz_sql_authenticator.db_models import Credentials
-
-
-def test_invalid_login(client, testuser, testpassword):
-    response = client.post(
-        "/auth/sql/authorize",
-        data={"username": testuser, "password": testpassword},
-    )
-    # Unauthorized
-    assert response.status_code == 200
-    assert "login failed" in response.text
-
-
-def test_valid_login(client, db, testuser, testpassword):
-    # Insert user
-    credentials = Credentials(
-        username=testuser, password_hash=_calculate_hash(testpassword)
-    )
-    db.add(credentials)
-
-    response = client.post(
-        "/auth/sql/authorize",
-        data={"username": testuser, "password": testpassword},
-        allow_redirects=False,
-    )
-    # Assert that we get a redirect to the main page
-    assert response.status_code == 303
-
-
-def test_create_user(owner_client, db, testuser, testpassword):
-    # Check that user is not in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-    response = owner_client.post(
-        f"/api/sqlauth/credentials/{testuser}?password={testpassword}",
-    )
-
-    # Check that user is in table
-    assert response.status_code == 200
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-
-
-def test_update_non_existing_user(owner_client, db, testuser, testpassword):
-    # Check that user is not in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-    response = owner_client.put(
-        f"/api/sqlauth/credentials/{testuser}?password={testpassword}",
-    )
-
-    # Check that user is not in table
-    assert response.status_code == 404
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-
-def test_update_user(owner_client, db, testuser, testpassword):
-    # Insert user
-    credentials = Credentials(
-        username=testuser, password_hash=_calculate_hash(testpassword)
-    )
-    db.add(credentials)
-
-    # Assert user is in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-
-    newpassword = "newpassword"
-    response = owner_client.put(
-        f"/api/sqlauth/credentials/{testuser}?password={newpassword}",
-    )
-
-    # Check that user is in table
-    assert response.status_code == 200
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-    password_hash = (
-        db.query(Credentials)
-        .filter(Credentials.username == testuser)
-        .one_or_none()
-        .password_hash
-    )
-
-    assert pbkdf2_sha256.verify(newpassword, password_hash)
-
-
-def test_delete_user(owner_client, db, testuser, testpassword):
-    # Insert user
-    credentials = Credentials(
-        username=testuser, password_hash=_calculate_hash(testpassword)
-    )
-    db.add(credentials)
-
-    # Assert user is in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-
-    response = owner_client.delete(
-        f"/api/sqlauth/credentials/{testuser}",
-    )
-
-    # Check that user is in table
-    assert response.status_code == 200
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-
-def test_delete_non_existing_user(owner_client, db, testuser, testpassword):
-    # Check that user is not in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-    response = owner_client.delete(
-        f"/api/sqlauth/credentials/{testuser}",
-    )
-
-    # Check that user is not in table
-    assert response.status_code == 404
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is None
-    )
-
-
-def test_get_all_users(owner_client, db, testuser, testpassword):
-    # Insert user
-    credentials = Credentials(
-        username=testuser, password_hash=_calculate_hash(testpassword)
-    )
-    db.add(credentials)
-
-    # Assert user is in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-
-    response = owner_client.get(
-        "/api/sqlauth/credentials",
-    )
-
-    # Check that user is in table
-    assert response.status_code == 200
-    assert testuser in response.text
-
-
-def test_get_all_users_unauthorized(member_client):
-    response = member_client.get(
-        "/api/sqlauth/credentials",
-    )
-
-    assert response.status_code == 403
-    assert "this operation requires owner or maintainer roles" in response.text
-
-
-def test_double_create(owner_client, testuser, testpassword):
-    response = owner_client.post(
-        f"/api/sqlauth/credentials/{testuser}?password={testpassword}",
-    )
-    assert response.status_code == 200
-
-    response = owner_client.post(
-        f"/api/sqlauth/credentials/{testuser}?password={testpassword}",
-    )
-    assert response.status_code == 409
-
-
-def test_changing_password(owner_client, client, db, testuser, testpassword):
-    # Create user
-    response = owner_client.post(
-        f"/api/sqlauth/credentials/{testuser}?password={testpassword}",
-    )
-    assert response.status_code == 200
-
-    # Assert user is in table
-    assert (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-        is not None
-    )
-
-    # Test login
-    response = client.post(
-        "/auth/sql/authorize",
-        data={"username": testuser, "password": testpassword},
-        allow_redirects=False,
-    )
-    # Assert that we get a redirect to the main page
-    assert response.status_code == 303
-
-    # Login in as owner again
-    response = client.get("/api/dummylogin/test_owner")
-    assert response.status_code == 200
-
-    # Change password
-    newpassword = "newpassword"
-    response = owner_client.put(
-        f"/api/sqlauth/credentials/{testuser}?password={newpassword}",
-    )
-    assert response.status_code == 200
-
-    # Check password in table
-    credentials = (
-        db.query(Credentials).filter(Credentials.username == testuser).one_or_none()
-    )
-    assert pbkdf2_sha256.verify(newpassword, credentials.password_hash)
-
-    # Test that old password does not work
-    response = client.post(
-        "/auth/sql/authorize",
-        data={"username": testuser, "password": testpassword},
-    )
-    assert response.status_code == 200
-    assert "login failed" in response.text
-
-    # Test that new password works
-    response = client.post(
-        "/auth/sql/authorize",
-        data={"username": testuser, "password": newpassword},
-        allow_redirects=False,
-    )
-    # Assert that we get a redirect to the main page
-    assert response.status_code == 303