diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4ad97393..db7dec9b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,6 +15,8 @@ before_script:
python-itsdangerous python-httpx python-jinja python-pytest-cov
python-requests python-aiofiles python-python-multipart
python-pytest-asyncio python-coverage python-bcrypt
+ - bash -c "echo '127.0.0.1' > /etc/hosts"
+ - bash -c "echo '::1' >> /etc/hosts"
test:
script:
diff --git a/aurweb/asgi.py b/aurweb/asgi.py
index b15e5874..1a61b1f4 100644
--- a/aurweb/asgi.py
+++ b/aurweb/asgi.py
@@ -1,5 +1,4 @@
import http
-import os
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
@@ -11,7 +10,7 @@ import aurweb.config
from aurweb.auth import BasicAuthBackend
from aurweb.db import get_engine
-from aurweb.routers import auth, html, sso, errors
+from aurweb.routers import accounts, auth, errors, html, sso
routes = set()
@@ -43,6 +42,7 @@ async def app_startup():
app.include_router(sso.router)
app.include_router(html.router)
app.include_router(auth.router)
+ app.include_router(accounts.router)
# Initialize the database engine and ORM.
get_engine()
diff --git a/aurweb/db.py b/aurweb/db.py
index 3f5731a9..7dab6c4a 100644
--- a/aurweb/db.py
+++ b/aurweb/db.py
@@ -145,35 +145,21 @@ def connect():
return get_engine().connect()
-class Connection:
+class ConnectionExecutor:
_conn = None
_paramstyle = None
- def __init__(self):
- aur_db_backend = aurweb.config.get('database', 'backend')
-
- if aur_db_backend == 'mysql':
+ def __init__(self, conn, backend=aurweb.config.get("database", "backend")):
+ self._conn = conn
+ if backend == "mysql":
import mysql.connector
- aur_db_host = aurweb.config.get('database', 'host')
- aur_db_name = aurweb.config.get('database', 'name')
- aur_db_user = aurweb.config.get('database', 'user')
- aur_db_pass = aurweb.config.get('database', 'password')
- aur_db_socket = aurweb.config.get('database', 'socket')
- self._conn = mysql.connector.connect(host=aur_db_host,
- user=aur_db_user,
- passwd=aur_db_pass,
- db=aur_db_name,
- unix_socket=aur_db_socket,
- buffered=True)
self._paramstyle = mysql.connector.paramstyle
- elif aur_db_backend == 'sqlite':
+ elif backend == "sqlite":
import sqlite3
- aur_db_name = aurweb.config.get('database', 'name')
- self._conn = sqlite3.connect(aur_db_name)
- self._conn.create_function("POWER", 2, math.pow)
self._paramstyle = sqlite3.paramstyle
- else:
- raise ValueError('unsupported database backend')
+
+ def paramstyle(self):
+ return self._paramstyle
def execute(self, query, params=()):
if self._paramstyle in ('format', 'pyformat'):
@@ -193,3 +179,43 @@ class Connection:
def close(self):
self._conn.close()
+
+
+class Connection:
+ _executor = None
+ _conn = None
+
+ def __init__(self):
+ aur_db_backend = aurweb.config.get('database', 'backend')
+
+ if aur_db_backend == 'mysql':
+ import mysql.connector
+ aur_db_host = aurweb.config.get('database', 'host')
+ aur_db_name = aurweb.config.get('database', 'name')
+ aur_db_user = aurweb.config.get('database', 'user')
+ aur_db_pass = aurweb.config.get('database', 'password')
+ aur_db_socket = aurweb.config.get('database', 'socket')
+ self._conn = mysql.connector.connect(host=aur_db_host,
+ user=aur_db_user,
+ passwd=aur_db_pass,
+ db=aur_db_name,
+ unix_socket=aur_db_socket,
+ buffered=True)
+ elif aur_db_backend == 'sqlite':
+ import sqlite3
+ aur_db_name = aurweb.config.get('database', 'name')
+ self._conn = sqlite3.connect(aur_db_name)
+ self._conn.create_function("POWER", 2, math.pow)
+ else:
+ raise ValueError('unsupported database backend')
+
+ self._conn = ConnectionExecutor(self._conn)
+
+ def execute(self, query, params=()):
+ return self._conn.execute(query, params)
+
+ def commit(self):
+ self._conn.commit()
+
+ def close(self):
+ self._conn.close()
diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py
new file mode 100644
index 00000000..0839f64e
--- /dev/null
+++ b/aurweb/routers/accounts.py
@@ -0,0 +1,102 @@
+from http import HTTPStatus
+
+from fastapi import APIRouter, Form, Request
+from fastapi.responses import HTMLResponse, RedirectResponse
+from sqlalchemy import or_
+
+from aurweb import db
+from aurweb.auth import auth_required
+from aurweb.l10n import get_translator_for_request
+from aurweb.models.user import User
+from aurweb.scripts.notify import ResetKeyNotification
+from aurweb.templates import make_context, render_template
+
+router = APIRouter()
+
+
+@router.get("/passreset", response_class=HTMLResponse)
+@auth_required(False)
+async def passreset(request: Request):
+ context = make_context(request, "Password Reset")
+
+ for k, v in request.query_params.items():
+ context[k] = v
+
+ return render_template(request, "passreset.html", context)
+
+
+@router.post("/passreset", response_class=HTMLResponse)
+@auth_required(False)
+async def passreset_post(request: Request,
+ user: str = Form(...),
+ resetkey: str = Form(default=None),
+ password: str = Form(default=None),
+ confirm: str = Form(default=None)):
+ from aurweb.db import session
+
+ context = make_context(request, "Password Reset")
+
+ for k, v in dict(await request.form()).items():
+ context[k] = v
+
+ # The user parameter being required, we can match against
+ user = db.query(User, or_(User.Username == user,
+ User.Email == user)).first()
+ if not user:
+ context["errors"] = ["Invalid e-mail."]
+ return render_template(request, "passreset.html", context,
+ status_code=int(HTTPStatus.NOT_FOUND))
+
+ if resetkey:
+ context["resetkey"] = resetkey
+
+ if not user.ResetKey or resetkey != user.ResetKey:
+ context["errors"] = ["Invalid e-mail."]
+ return render_template(request, "passreset.html", context,
+ status_code=int(HTTPStatus.NOT_FOUND))
+
+ if not user or not password:
+ context["errors"] = ["Missing a required field."]
+ return render_template(request, "passreset.html", context,
+ status_code=int(HTTPStatus.BAD_REQUEST))
+
+ if password != confirm:
+ # If the provided password does not match the provided confirm.
+ context["errors"] = ["Password fields do not match."]
+ return render_template(request, "passreset.html", context,
+ status_code=int(HTTPStatus.BAD_REQUEST))
+
+ if len(password) < User.minimum_passwd_length():
+ # Translate the error here, which simplifies error output
+ # in the jinja2 template.
+ _ = get_translator_for_request(request)
+ context["errors"] = [_(
+ "Your password must be at least %s characters.") % (
+ str(User.minimum_passwd_length()))]
+ return render_template(request, "passreset.html", context,
+ status_code=int(HTTPStatus.BAD_REQUEST))
+
+ # We got to this point; everything matched up. Update the password
+ # and remove the ResetKey.
+ user.ResetKey = str()
+ user.update_password(password)
+
+ if user.session:
+ session.delete(user.session)
+ session.commit()
+
+ # Render ?step=complete.
+ return RedirectResponse(url="/passreset?step=complete",
+ status_code=int(HTTPStatus.SEE_OTHER))
+
+ # If we got here, we continue with issuing a resetkey for the user.
+ resetkey = db.make_random_value(User, User.ResetKey)
+ user.ResetKey = resetkey
+ session.commit()
+
+ executor = db.ConnectionExecutor(db.get_engine().raw_connection())
+ ResetKeyNotification(executor, user.ID).send()
+
+ # Render ?step=confirm.
+ return RedirectResponse(url="/passreset?step=confirm",
+ status_code=int(HTTPStatus.SEE_OTHER))
diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py
index 3a1c7192..e4864424 100644
--- a/aurweb/routers/auth.py
+++ b/aurweb/routers/auth.py
@@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
import aurweb.config
+from aurweb.auth import auth_required
from aurweb.models.user import User
from aurweb.templates import make_context, render_template
@@ -21,12 +22,13 @@ def login_template(request: Request, next: str, errors: list = None):
@router.get("/login", response_class=HTMLResponse)
+@auth_required(False)
async def login_get(request: Request, next: str = "/"):
- """ Homepage route. """
return login_template(request, next)
@router.post("/login", response_class=HTMLResponse)
+@auth_required(False)
async def login_post(request: Request,
next: str = Form(...),
user: str = Form(default=str()),
@@ -45,8 +47,8 @@ async def login_post(request: Request,
cookie_timeout = aurweb.config.getint(
"options", "persistent_cookie_timeout")
- _, sid = user.login(request, passwd, cookie_timeout)
- if not _:
+ sid = user.login(request, passwd, cookie_timeout)
+ if not sid:
return login_template(request, next,
errors=["Bad username or password."])
@@ -62,6 +64,7 @@ async def login_post(request: Request,
@router.get("/logout")
+@auth_required()
async def logout(request: Request, next: str = "/"):
""" A GET and POST route for logging out.
@@ -81,5 +84,6 @@ async def logout(request: Request, next: str = "/"):
@router.post("/logout")
+@auth_required()
async def logout_post(request: Request, next: str = "/"):
return await logout(request=request, next=next)
diff --git a/conf/config.dev b/conf/config.dev
index 194a3bf8..94775a92 100644
--- a/conf/config.dev
+++ b/conf/config.dev
@@ -25,6 +25,12 @@ disable_http_login = 0
enable-maintenance = 0
localedir = YOUR_AUR_ROOT/web/locale
+[notifications]
+; For development/testing, use /usr/bin/sendmail
+sendmail = YOUR_AUR_ROOT/util/sendmail
+sender = notify@localhost
+reply-to = noreply@localhost
+
; Single sign-on; see doc/sso.txt.
[sso]
openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration
diff --git a/setup.cfg b/setup.cfg
index b868c096..98261651 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,6 +2,19 @@
max-line-length = 127
max-complexity = 10
+# Ignore some unavoidable flake8 warnings; we know this is against
+# pycodestyle, but some of the existing codebase uses `I` variables,
+# so specifically silence warnings about it in pre-defined files.
+# In E741, the 'I', 'O', 'l' are ambiguous variable names.
+# Our current implementation uses these variables through HTTP
+# and the FastAPI form specification wants them named as such.
+# In C901's case, our process_account_form function is way too
+# complex for PEP (too many if statements). However, we need to
+# process these anyways, and making it any more complex would
+# just add confusion to the implementation.
+per-file-ignores =
+ aurweb/routers/accounts.py:E741,C901
+
[isort]
line_length = 127
lines_between_types = 1
diff --git a/templates/partials/error.html b/templates/partials/error.html
new file mode 100644
index 00000000..6043dfd1
--- /dev/null
+++ b/templates/partials/error.html
@@ -0,0 +1,15 @@
+{% if errors %}
+
+ {% for error in errors %}
+ {% if error is string %}
+ - {{ error | tr | safe }}
+ {% elif error is iterable %}
+
+ {% for e in error %}
+ - {{ e | tr | safe }}
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+
+{% endif %}
diff --git a/templates/passreset.html b/templates/passreset.html
new file mode 100644
index 00000000..d2c3c2ee
--- /dev/null
+++ b/templates/passreset.html
@@ -0,0 +1,76 @@
+{% extends "partials/layout.html" %}
+
+{% block pageContent %}
+
+
{% trans %}Password Reset{% endtrans %}
+
+
+ {% if step == "confirm" %}
+ {% trans %}Check your e-mail for the confirmation link.{% endtrans %}
+ {% elif step == "complete" %}
+ {% trans %}Your password has been reset successfully.{% endtrans %}
+ {% elif resetkey %}
+
+ {% include "partials/error.html" %}
+
+
+ {% else %}
+
+ {% set url = "https://mailman.archlinux.org/mailman/listinfo/aur-general" %}
+ {{ "If you have forgotten the user name and the primary e-mail "
+ "address you used to register, please send a message to the "
+ "%saur-general%s mailing list."
+ | tr
+ | format(
+ '
' | format(url),
+ "")
+ | safe
+ }}
+
+
+ {% include "partials/error.html" %}
+
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/test/README.md b/test/README.md
index 3261899b..872d980b 100644
--- a/test/README.md
+++ b/test/README.md
@@ -27,6 +27,7 @@ For all the test to run, the following Arch packages should be installed:
- python-pytest
- python-pytest-cov
- python-pytest-asyncio
+- postfix
Running tests
-------------
@@ -37,6 +38,10 @@ First, setup the test configuration:
$ sed -r 's;YOUR_AUR_ROOT;$(pwd);g' conf/config.dev > conf/config
+You'll need to make sure that emails can be sent out by aurweb.scripts.notify.
+If you don't have anything setup, just install postfix and start it before
+running tests.
+
With those installed, one can run Python tests manually with any AUR config
specified by `AUR_CONFIG`:
diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py
new file mode 100644
index 00000000..0f548805
--- /dev/null
+++ b/test/test_accounts_routes.py
@@ -0,0 +1,218 @@
+from http import HTTPStatus
+
+import pytest
+
+from fastapi.testclient import TestClient
+
+from aurweb.asgi import app
+from aurweb.db import query
+from aurweb.models.account_type import AccountType
+from aurweb.models.session import Session
+from aurweb.models.user import User
+from aurweb.testing import setup_test_db
+from aurweb.testing.models import make_user
+from aurweb.testing.requests import Request
+
+# Some test global constants.
+TEST_USERNAME = "test"
+TEST_EMAIL = "test@example.org"
+
+# Global mutables.
+client = TestClient(app)
+user = None
+
+
+@pytest.fixture(autouse=True)
+def setup():
+ global user
+
+ setup_test_db("Users", "Sessions", "Bans")
+
+ account_type = query(AccountType,
+ AccountType.AccountType == "User").first()
+ user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL,
+ RealName="Test User", Passwd="testPassword",
+ AccountType=account_type)
+
+
+def test_get_passreset_authed_redirects():
+ sid = user.login(Request(), "testPassword")
+ assert sid is not None
+
+ with client as request:
+ response = request.get("/passreset", cookies={"AURSID": sid},
+ allow_redirects=False)
+
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/"
+
+
+def test_get_passreset():
+ with client as request:
+ response = request.get("/passreset")
+ assert response.status_code == int(HTTPStatus.OK)
+
+
+def test_get_passreset_translation():
+ # Test that translation works.
+ with client as request:
+ response = request.get("/passreset", cookies={"AURLANG": "de"})
+
+ # The header title should be translated.
+ assert "Passwort zurücksetzen".encode("utf-8") in response.content
+
+ # The form input label should be translated.
+ assert "Benutzername oder primäre E-Mail-Adresse eingeben:".encode(
+ "utf-8") in response.content
+
+ # And the button.
+ assert "Weiter".encode("utf-8") in response.content
+
+
+def test_get_passreset_with_resetkey():
+ with client as request:
+ response = request.get("/passreset", data={"resetkey": "abcd"})
+ assert response.status_code == int(HTTPStatus.OK)
+
+
+def test_post_passreset_authed_redirects():
+ sid = user.login(Request(), "testPassword")
+ assert sid is not None
+
+ with client as request:
+ response = request.post("/passreset",
+ cookies={"AURSID": sid},
+ data={"user": "blah"},
+ allow_redirects=False)
+
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/"
+
+
+def test_post_passreset_user():
+ # With username.
+ with client as request:
+ response = request.post("/passreset", data={"user": TEST_USERNAME})
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/passreset?step=confirm"
+
+ # With e-mail.
+ with client as request:
+ response = request.post("/passreset", data={"user": TEST_EMAIL})
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/passreset?step=confirm"
+
+
+def test_post_passreset_resetkey():
+ from aurweb.db import session
+
+ user.session = Session(UsersID=user.ID, SessionID="blah",
+ LastUpdateTS=datetime.utcnow().timestamp())
+ session.commit()
+
+ # Prepare a password reset.
+ with client as request:
+ response = request.post("/passreset", data={"user": TEST_USERNAME})
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/passreset?step=confirm"
+
+ # Now that we've prepared the password reset, prepare a POST
+ # request with the user's ResetKey.
+ resetkey = user.ResetKey
+ post_data = {
+ "user": TEST_USERNAME,
+ "resetkey": resetkey,
+ "password": "abcd1234",
+ "confirm": "abcd1234"
+ }
+
+ with client as request:
+ response = request.post("/passreset", data=post_data)
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/passreset?step=complete"
+
+
+def test_post_passreset_error_invalid_email():
+ # First, test with a user that doesn't even exist.
+ with client as request:
+ response = request.post("/passreset", data={"user": "invalid"})
+ assert response.status_code == int(HTTPStatus.NOT_FOUND)
+
+ error = "Invalid e-mail."
+ assert error in response.content.decode("utf-8")
+
+ # Then, test with an invalid resetkey for a real user.
+ _ = make_resetkey()
+ post_data = make_passreset_data("fake")
+ post_data["password"] = "abcd1234"
+ post_data["confirm"] = "abcd1234"
+
+ with client as request:
+ response = request.post("/passreset", data=post_data)
+ assert response.status_code == int(HTTPStatus.NOT_FOUND)
+ assert error in response.content.decode("utf-8")
+
+
+def make_resetkey():
+ with client as request:
+ response = request.post("/passreset", data={"user": TEST_USERNAME})
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+ assert response.headers.get("location") == "/passreset?step=confirm"
+ return user.ResetKey
+
+
+def make_passreset_data(resetkey):
+ return {
+ "user": user.Username,
+ "resetkey": resetkey
+ }
+
+
+def test_post_passreset_error_missing_field():
+ # Now that we've prepared the password reset, prepare a POST
+ # request with the user's ResetKey.
+ resetkey = make_resetkey()
+ post_data = make_passreset_data(resetkey)
+
+ with client as request:
+ response = request.post("/passreset", data=post_data)
+
+ assert response.status_code == int(HTTPStatus.BAD_REQUEST)
+
+ error = "Missing a required field."
+ assert error in response.content.decode("utf-8")
+
+
+def test_post_passreset_error_password_mismatch():
+ resetkey = make_resetkey()
+ post_data = make_passreset_data(resetkey)
+
+ post_data["password"] = "abcd1234"
+ post_data["confirm"] = "mismatched"
+
+ with client as request:
+ response = request.post("/passreset", data=post_data)
+
+ assert response.status_code == int(HTTPStatus.BAD_REQUEST)
+
+ error = "Password fields do not match."
+ assert error in response.content.decode("utf-8")
+
+
+def test_post_passreset_error_password_requirements():
+ resetkey = make_resetkey()
+ post_data = make_passreset_data(resetkey)
+
+ passwd_min_len = User.minimum_passwd_length()
+ assert passwd_min_len >= 4
+
+ post_data["password"] = "x"
+ post_data["confirm"] = "x"
+
+ with client as request:
+ response = request.post("/passreset", data=post_data)
+
+ assert response.status_code == int(HTTPStatus.BAD_REQUEST)
+
+ error = f"Your password must be at least {passwd_min_len} characters."
+ assert error in response.content.decode("utf-8")
diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py
index adf75329..ff8a08e9 100644
--- a/test/test_auth_routes.py
+++ b/test/test_auth_routes.py
@@ -14,8 +14,12 @@ from aurweb.models.session import Session
from aurweb.testing import setup_test_db
from aurweb.testing.models import make_user
-client = TestClient(app)
+# Some test global constants.
+TEST_USERNAME = "test"
+TEST_EMAIL = "test@example.org"
+# Global mutables.
+client = TestClient(app)
user = None
@@ -27,7 +31,8 @@ def setup():
account_type = query(AccountType,
AccountType.AccountType == "User").first()
- user = make_user(Username="test", Email="test@example.org",
+
+ user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL,
RealName="Test User", Passwd="testPassword",
AccountType=account_type)
@@ -40,16 +45,16 @@ def test_login_logout():
}
with client as request:
- response = client.get("/login")
+ # First, let's test get /login.
+ response = request.get("/login")
assert response.status_code == int(HTTPStatus.OK)
response = request.post("/login", data=post_data,
allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
- response = request.get(response.headers.get("location"), cookies={
- "AURSID": response.cookies.get("AURSID")
- })
+ # Simulate following the redirect location from above's response.
+ response = request.get(response.headers.get("location"))
assert response.status_code == int(HTTPStatus.OK)
response = request.post("/logout", data=post_data,
@@ -60,9 +65,37 @@ def test_login_logout():
"AURSID": response.cookies.get("AURSID")
}, allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
+
assert "AURSID" not in response.cookies
+def test_authenticated_login_forbidden():
+ post_data = {
+ "user": "test",
+ "passwd": "testPassword",
+ "next": "/"
+ }
+
+ with client as request:
+ # Login.
+ response = request.post("/login", data=post_data,
+ allow_redirects=False)
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+
+ # Now, let's verify that we receive 403 Forbidden when we
+ # try to get /login as an authenticated user.
+ response = request.get("/login", allow_redirects=False)
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+
+
+def test_unauthenticated_logout_unauthorized():
+ with client as request:
+ # Alright, let's verify that attempting to /logout when not
+ # authenticated returns 401 Unauthorized.
+ response = request.get("/logout", allow_redirects=False)
+ assert response.status_code == int(HTTPStatus.SEE_OTHER)
+
+
def test_login_missing_username():
post_data = {
"passwd": "testPassword",
@@ -75,8 +108,6 @@ def test_login_missing_username():
def test_login_remember_me():
- from aurweb.db import session
-
post_data = {
"user": "test",
"passwd": "testPassword",
@@ -94,8 +125,8 @@ def test_login_remember_me():
"options", "persistent_cookie_timeout")
expected_ts = datetime.utcnow().timestamp() + cookie_timeout
- _session = session.query(Session).filter(
- Session.UsersID == user.ID).first()
+ _session = query(Session,
+ Session.UsersID == user.ID).first()
# Expect that LastUpdateTS was within 5 seconds of the expected_ts,
# which is equal to the current timestamp + persistent_cookie_timeout.
diff --git a/test/test_db.py b/test/test_db.py
index 1eb0dc28..e0946ed5 100644
--- a/test/test_db.py
+++ b/test/test_db.py
@@ -3,7 +3,6 @@ import re
import sqlite3
import tempfile
-from datetime import datetime
from unittest import mock
import mysql.connector
@@ -185,3 +184,15 @@ def test_create_delete():
db.delete(AccountType, AccountType.AccountType == "test")
record = db.query(AccountType, AccountType.AccountType == "test").first()
assert record is None
+
+
+@mock.patch("mysql.connector.paramstyle", "qmark")
+def test_connection_executor_mysql_paramstyle():
+ executor = db.ConnectionExecutor(None, backend="mysql")
+ assert executor.paramstyle() == "qmark"
+
+
+@mock.patch("sqlite3.paramstyle", "pyformat")
+def test_connection_executor_sqlite_paramstyle():
+ executor = db.ConnectionExecutor(None, backend="sqlite")
+ assert executor.paramstyle() == "pyformat"
diff --git a/test/test_user.py b/test/test_user.py
index b8d4248a..4f144819 100644
--- a/test/test_user.py
+++ b/test/test_user.py
@@ -28,8 +28,8 @@ def setup():
setup_test_db("Users", "Sessions", "Bans")
- account_type = session.query(AccountType).filter(
- AccountType.AccountType == "User").first()
+ account_type = query(AccountType,
+ AccountType.AccountType == "User").first()
user = make_user(Username="test", Email="test@example.org",
RealName="Test User", Passwd="testPassword",
@@ -67,7 +67,7 @@ def test_user_login_logout():
assert user.session.User == user
# Search for the user via query API.
- result = session.query(User).filter(User.ID == user.ID).first()
+ result = query(User, User.ID == user.ID).first()
# Compare the result and our original user.
assert result == user
diff --git a/util/sendmail b/util/sendmail
new file mode 100755
index 00000000..06bd9865
--- /dev/null
+++ b/util/sendmail
@@ -0,0 +1,2 @@
+#!/bin/bash
+exit 0