add passreset routes

Introduced `get|post` `/passreset` routes. These routes mimic the
behavior of the existing PHP implementation, with the exception of
HTTP status code returns.

Routes added:
    GET /passreset
    POST /passreset

Routers added:
    aurweb.routers.accounts

* On an unknown user or mismatched resetkey (where resetkey must ==
  user.resetkey), return HTTP status NOT_FOUND (404).
* On another error in the request, return HTTP status BAD_REQUEST (400).

Both `get|post` routes requires that the current user is **not**
authenticated, hence `@auth_required(False, redirect="/")`.

+ Added auth_required decorator to aurweb.auth.
+ Added some more utility to aurweb.models.user.User.
+ Added `partials/error.html` template.
+ Added `passreset.html` template.
+ Added aurweb.db.ConnectionExecutor functor for paramstyle logic.
  Decoupling the executor logic from the database connection logic
  is needed for us to easily use the same logic with a fastapi
  database session, when we need to use aurweb.scripts modules.

At this point, notification configuration is now required to complete
tests involved with notifications properly, like passreset.
`conf/config.dev` has been modified to include [notifications] sendmail,
sender and reply-to overrides. Dockerfile and .gitlab-ci.yml have been
updated to setup /etc/hosts and start postfix before running tests.

* setup.cfg: ignore E741, C901 in aurweb.routers.accounts

These two warnings (shown in the commit) are not dangerous and a bi-product
of maintaining compatibility with our current code flow.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-01-06 21:00:12 -08:00
parent 4423326cec
commit a33d076d8b
15 changed files with 552 additions and 41 deletions

View file

@ -15,6 +15,8 @@ before_script:
python-itsdangerous python-httpx python-jinja python-pytest-cov python-itsdangerous python-httpx python-jinja python-pytest-cov
python-requests python-aiofiles python-python-multipart python-requests python-aiofiles python-python-multipart
python-pytest-asyncio python-coverage python-bcrypt python-pytest-asyncio python-coverage python-bcrypt
- bash -c "echo '127.0.0.1' > /etc/hosts"
- bash -c "echo '::1' >> /etc/hosts"
test: test:
script: script:

View file

@ -1,5 +1,4 @@
import http import http
import os
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
@ -11,7 +10,7 @@ import aurweb.config
from aurweb.auth import BasicAuthBackend from aurweb.auth import BasicAuthBackend
from aurweb.db import get_engine 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() routes = set()
@ -43,6 +42,7 @@ async def app_startup():
app.include_router(sso.router) app.include_router(sso.router)
app.include_router(html.router) app.include_router(html.router)
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(accounts.router)
# Initialize the database engine and ORM. # Initialize the database engine and ORM.
get_engine() get_engine()

View file

@ -145,35 +145,21 @@ def connect():
return get_engine().connect() return get_engine().connect()
class Connection: class ConnectionExecutor:
_conn = None _conn = None
_paramstyle = None _paramstyle = None
def __init__(self): def __init__(self, conn, backend=aurweb.config.get("database", "backend")):
aur_db_backend = aurweb.config.get('database', 'backend') self._conn = conn
if backend == "mysql":
if aur_db_backend == 'mysql':
import mysql.connector 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 self._paramstyle = mysql.connector.paramstyle
elif aur_db_backend == 'sqlite': elif backend == "sqlite":
import sqlite3 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 self._paramstyle = sqlite3.paramstyle
else:
raise ValueError('unsupported database backend') def paramstyle(self):
return self._paramstyle
def execute(self, query, params=()): def execute(self, query, params=()):
if self._paramstyle in ('format', 'pyformat'): if self._paramstyle in ('format', 'pyformat'):
@ -193,3 +179,43 @@ class Connection:
def close(self): def close(self):
self._conn.close() 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()

102
aurweb/routers/accounts.py Normal file
View file

@ -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))

View file

@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
import aurweb.config import aurweb.config
from aurweb.auth import auth_required
from aurweb.models.user import User from aurweb.models.user import User
from aurweb.templates import make_context, render_template 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) @router.get("/login", response_class=HTMLResponse)
@auth_required(False)
async def login_get(request: Request, next: str = "/"): async def login_get(request: Request, next: str = "/"):
""" Homepage route. """
return login_template(request, next) return login_template(request, next)
@router.post("/login", response_class=HTMLResponse) @router.post("/login", response_class=HTMLResponse)
@auth_required(False)
async def login_post(request: Request, async def login_post(request: Request,
next: str = Form(...), next: str = Form(...),
user: str = Form(default=str()), user: str = Form(default=str()),
@ -45,8 +47,8 @@ async def login_post(request: Request,
cookie_timeout = aurweb.config.getint( cookie_timeout = aurweb.config.getint(
"options", "persistent_cookie_timeout") "options", "persistent_cookie_timeout")
_, sid = user.login(request, passwd, cookie_timeout) sid = user.login(request, passwd, cookie_timeout)
if not _: if not sid:
return login_template(request, next, return login_template(request, next,
errors=["Bad username or password."]) errors=["Bad username or password."])
@ -62,6 +64,7 @@ async def login_post(request: Request,
@router.get("/logout") @router.get("/logout")
@auth_required()
async def logout(request: Request, next: str = "/"): async def logout(request: Request, next: str = "/"):
""" A GET and POST route for logging out. """ A GET and POST route for logging out.
@ -81,5 +84,6 @@ async def logout(request: Request, next: str = "/"):
@router.post("/logout") @router.post("/logout")
@auth_required()
async def logout_post(request: Request, next: str = "/"): async def logout_post(request: Request, next: str = "/"):
return await logout(request=request, next=next) return await logout(request=request, next=next)

View file

@ -25,6 +25,12 @@ disable_http_login = 0
enable-maintenance = 0 enable-maintenance = 0
localedir = YOUR_AUR_ROOT/web/locale 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. ; Single sign-on; see doc/sso.txt.
[sso] [sso]
openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration

View file

@ -2,6 +2,19 @@
max-line-length = 127 max-line-length = 127
max-complexity = 10 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] [isort]
line_length = 127 line_length = 127
lines_between_types = 1 lines_between_types = 1

View file

@ -0,0 +1,15 @@
{% if errors %}
<ul class="errorlist">
{% for error in errors %}
{% if error is string %}
<li>{{ error | tr | safe }}</li>
{% elif error is iterable %}
<ul>
{% for e in error %}
<li>{{ e | tr | safe }}</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
</ul>
{% endif %}

76
templates/passreset.html Normal file
View file

@ -0,0 +1,76 @@
{% extends "partials/layout.html" %}
{% block pageContent %}
<div class="box">
<h2>{% trans %}Password Reset{% endtrans %}</h2>
<p>
{% 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 %}
<!-- Provided with a resetkey. -->
{% include "partials/error.html" %}
<form method="post">
<table>
<tbody>
<tr>
<td>{% trans %}Confirm your user name or primary e-mail address:{% endtrans %}</td>
<td>
<input type="text" name="user" size="30" maxlength="64"
value="{{ user or '' }}">
</td>
</tr>
<tr>
<td>{% trans %}Enter your new password:{% endtrans %}</td>
<td>
<input type="password" name="password" size="30"
value="{{ password or '' }}">
</td>
</tr>
<tr>
<td>{% trans %}Confirm your new password:{% endtrans %}</td>
<td>
<input type="password" name="confirm" size="30"
value="{{ confirm or '' }}">
</td>
</tr>
</tbody>
</table>
<br>
<input type="hidden" name="resetkey"
value="{{ resetkey }}">
<input class="button" type="submit"
value="{% trans %}Continue{% endtrans %}">
</form>
{% else %}
<!-- Default page with prompt for user name/e-mail. -->
{% 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(
'<a href="%s">' | format(url),
"</a>")
| safe
}}
</p>
{% include "partials/error.html" %}
<form method="post">
<p>
{% trans %}Enter your user name or your primary e-mail address:{% endtrans %}
<input type="text" name="user" size="30" maxlength="64"
value="{{ user or '' }}">
</p>
<input class="button" type="submit"
value="{% trans %}Continue{% endtrans %}">
</form>
{% endif %}
</p>
</div>
{% endblock %}

View file

@ -27,6 +27,7 @@ For all the test to run, the following Arch packages should be installed:
- python-pytest - python-pytest
- python-pytest-cov - python-pytest-cov
- python-pytest-asyncio - python-pytest-asyncio
- postfix
Running tests Running tests
------------- -------------
@ -37,6 +38,10 @@ First, setup the test configuration:
$ sed -r 's;YOUR_AUR_ROOT;$(pwd);g' conf/config.dev > conf/config $ 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 With those installed, one can run Python tests manually with any AUR config
specified by `AUR_CONFIG`: specified by `AUR_CONFIG`:

View file

@ -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")

View file

@ -14,8 +14,12 @@ from aurweb.models.session import Session
from aurweb.testing import setup_test_db from aurweb.testing import setup_test_db
from aurweb.testing.models import make_user 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 user = None
@ -27,7 +31,8 @@ def setup():
account_type = query(AccountType, account_type = query(AccountType,
AccountType.AccountType == "User").first() 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", RealName="Test User", Passwd="testPassword",
AccountType=account_type) AccountType=account_type)
@ -40,16 +45,16 @@ def test_login_logout():
} }
with client as request: 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) assert response.status_code == int(HTTPStatus.OK)
response = request.post("/login", data=post_data, response = request.post("/login", data=post_data,
allow_redirects=False) allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.status_code == int(HTTPStatus.SEE_OTHER)
response = request.get(response.headers.get("location"), cookies={ # Simulate following the redirect location from above's response.
"AURSID": response.cookies.get("AURSID") response = request.get(response.headers.get("location"))
})
assert response.status_code == int(HTTPStatus.OK) assert response.status_code == int(HTTPStatus.OK)
response = request.post("/logout", data=post_data, response = request.post("/logout", data=post_data,
@ -60,9 +65,37 @@ def test_login_logout():
"AURSID": response.cookies.get("AURSID") "AURSID": response.cookies.get("AURSID")
}, allow_redirects=False) }, allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.status_code == int(HTTPStatus.SEE_OTHER)
assert "AURSID" not in response.cookies 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(): def test_login_missing_username():
post_data = { post_data = {
"passwd": "testPassword", "passwd": "testPassword",
@ -75,8 +108,6 @@ def test_login_missing_username():
def test_login_remember_me(): def test_login_remember_me():
from aurweb.db import session
post_data = { post_data = {
"user": "test", "user": "test",
"passwd": "testPassword", "passwd": "testPassword",
@ -94,7 +125,7 @@ def test_login_remember_me():
"options", "persistent_cookie_timeout") "options", "persistent_cookie_timeout")
expected_ts = datetime.utcnow().timestamp() + cookie_timeout expected_ts = datetime.utcnow().timestamp() + cookie_timeout
_session = session.query(Session).filter( _session = query(Session,
Session.UsersID == user.ID).first() Session.UsersID == user.ID).first()
# Expect that LastUpdateTS was within 5 seconds of the expected_ts, # Expect that LastUpdateTS was within 5 seconds of the expected_ts,

View file

@ -3,7 +3,6 @@ import re
import sqlite3 import sqlite3
import tempfile import tempfile
from datetime import datetime
from unittest import mock from unittest import mock
import mysql.connector import mysql.connector
@ -185,3 +184,15 @@ def test_create_delete():
db.delete(AccountType, AccountType.AccountType == "test") db.delete(AccountType, AccountType.AccountType == "test")
record = db.query(AccountType, AccountType.AccountType == "test").first() record = db.query(AccountType, AccountType.AccountType == "test").first()
assert record is None 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"

View file

@ -28,7 +28,7 @@ def setup():
setup_test_db("Users", "Sessions", "Bans") setup_test_db("Users", "Sessions", "Bans")
account_type = session.query(AccountType).filter( account_type = query(AccountType,
AccountType.AccountType == "User").first() AccountType.AccountType == "User").first()
user = make_user(Username="test", Email="test@example.org", user = make_user(Username="test", Email="test@example.org",
@ -67,7 +67,7 @@ def test_user_login_logout():
assert user.session.User == user assert user.session.User == user
# Search for the user via query API. # 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. # Compare the result and our original user.
assert result == user assert result == user

2
util/sendmail Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
exit 0