Compare commits

...

4 Commits

6 changed files with 113 additions and 11 deletions

View File

@ -6,22 +6,21 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'sachet-server'
copyright = '2023, dogeystamp'
author = 'dogeystamp'
project = "sachet-server"
copyright = "2023, dogeystamp"
author = "dogeystamp"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx_rtd_theme"]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]

View File

@ -54,6 +54,7 @@ Sachet implements the following endpoints for this API::
GET /users/<username>
PATCH /users/<username>
PUT /users/<username>
DELETE /users/<username>
GET
^^^
@ -117,6 +118,13 @@ For example:
Only :ref:`administrators<permissions_table>` can request this method.
DELETE
^^^^^^
Requesting ``DELETE /users/<username>`` deletes the specified user.
Only :ref:`administrators<permissions_table>` can request this method.
.. _user_list_api:
List API
@ -139,3 +147,15 @@ POST
``POST /users`` creates a new user.
The request body must conform to the :ref:`User schema<user_schema>`.
The server will return a ``201 Created`` code with a similar body to this:
.. code-block:: json
{
"status": "success",
"url": "/users/user"
}
The ``url`` field is the URL to the new user.
It can be used in further requests to manage the user's information, or delete it.

View File

@ -91,7 +91,7 @@ class User(db.Model):
password, current_app.config.get("BCRYPT_LOG_ROUNDS")
).decode()
self.username = username
self.url = url_for("users_blueprint.user_list_api", username=self.username)
self.url = url_for("users_blueprint.user_api", username=self.username)
self.register_date = datetime.datetime.now()
def encode_token(self, jti=None):

View File

@ -109,7 +109,7 @@ users_blueprint.add_url_rule(
class UserAPI(ModelAPI):
"""User information API"""
"""User information API."""
@auth_required
def get(self, username, auth_user=None):
@ -136,11 +136,16 @@ class UserAPI(ModelAPI):
put_user = User.query.filter_by(username=username).first()
return super().put(put_user)
@auth_required(required_permissions=(Permissions.ADMIN,))
def delete(self, username, auth_user=None):
delete_user = User.query.filter_by(username=username).first()
return super().delete(delete_user)
users_blueprint.add_url_rule(
"/users/<username>",
view_func=UserAPI.as_view("user_api"),
methods=["GET", "PATCH", "PUT"],
methods=["GET", "PATCH", "PUT", "DELETE"],
)

View File

@ -125,6 +125,16 @@ def users(client):
Permissions.READ,
),
),
no_admin_user=dict(
password="password",
permissions=Bitmask(
Permissions.CREATE,
Permissions.MODIFY,
Permissions.DELETE,
Permissions.LOCK,
Permissions.READ,
),
),
administrator=dict(password="4321", permissions=Bitmask(Permissions.ADMIN)),
)

68
tests/test_user.py Normal file
View File

@ -0,0 +1,68 @@
import pytest
def test_post(client, users, auth):
"""Test registering a user, then logging in to it."""
# register without adequate permissions
resp = client.post(
"/users",
headers=auth("no_admin_user"),
json={"username": "claire", "permissions": [], "password": "claire123"},
)
assert resp.status_code == 403
# properly register
resp = client.post(
"/users",
headers=auth("administrator"),
json={"username": "claire", "permissions": [], "password": "claire123"},
)
assert resp.status_code == 201
data = resp.get_json()
url = data.get("url")
assert url is not None
assert url == "/users/claire"
# try logging in now
resp = client.post(
"/users/login", json={"username": "claire", "password": "claire123"}
)
assert resp.status_code == 200
data = resp.get_json()
assert data.get("status") == "success"
assert data.get("username") == "claire"
token = data.get("auth_token")
assert token is not None and token != ""
def test_delete(client, users, auth):
"""Test registering a user, then deleting it."""
resp = client.post(
"/users",
headers=auth("administrator"),
json={"username": "claire", "permissions": [], "password": "claire123"},
)
assert resp.status_code == 201
# try logging in now
resp = client.post(
"/users/login", json={"username": "claire", "password": "claire123"}
)
assert resp.status_code == 200
data = resp.get_json()
token = data.get("auth_token")
# test if the token works
resp = client.get("/users/claire", headers={"Authorization": f"bearer {token}"})
assert resp.status_code == 200
# delete without permission
resp = client.delete("/users/claire", headers=auth("no_admin_user"))
assert resp.status_code == 403
# delete properly
resp = client.delete("/users/claire", headers=auth("administrator"))
assert resp.status_code == 200
# test if the token for a non-existent user works
resp = client.get("/users/claire", headers={"Authentication": f"bearer {token}"})
assert resp.status_code == 401