Skip to content

Commit

Permalink
Add exception if UsersService does not have a valid database configur…
Browse files Browse the repository at this point in the history
…ation

Add `create_user` method with added check for input passwords being hashed with unit tests
  • Loading branch information
NeonDaniel committed Oct 25, 2024
1 parent 193bade commit 39ff65b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 0 deletions.
6 changes: 6 additions & 0 deletions neon_users_service/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ class UserExistsError(Exception):
class UserNotFoundError(Exception):
"""
Raised when trying to look up a user that does not exist.
"""


class ConfigurationError(KeyError):
"""
Raised when service configuration is not valid.
"""
40 changes: 40 additions & 0 deletions neon_users_service/service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import hashlib
import re
from copy import copy
from typing import Optional
from ovos_config import Configuration
from neon_users_service.databases import UserDatabase
from neon_users_service.exceptions import ConfigurationError
from neon_users_service.models import User


class NeonUsersService:
def __init__(self, config: Optional[dict] = None):
self.config = config or Configuration().get("neon_users_service", {})
self.database = self.init_database()
if not self.database:
raise ConfigurationError(f"`{self.config.get('module')}` is not a "
f"valid database module.")

def init_database(self) -> UserDatabase:
module = self.config.get("module")
Expand All @@ -16,3 +24,35 @@ def init_database(self) -> UserDatabase:
return SQLiteUserDatabase(**module_config)
# Other supported databases may be added here

@staticmethod
def _ensure_hashed(password: str) -> str:
"""
Generate the sha-256 hash for an input password to be stored in the
database. If the password is already a valid hash string, it will be
returned with no changes.
@param password: Input password string to be hashed
@retruns: A hexadecimal string representation of the sha-256 hash
"""
if re.compile(r"^[a-f0-9]{64}$").match(password):
password_hash = password
else:
password_hash = hashlib.sha256(password.encode("utf-8")).hexdigest()
return password_hash

def create_user(self, user: User) -> User:
"""
Helper to create a new user. Includes a check that the input password
hash is valid, replacing string passwords with hashes as necessary.
@param user: The user to be created
@returns: The user as added to the database
"""
# Create a copy to prevent modifying the input object
user = copy(user)
user.password_hash = self._ensure_hashed(user.password_hash)
return self.database.create_user(user)

def shutdown(self):
"""
Shutdown the service
"""
self.database.shutdown()
50 changes: 50 additions & 0 deletions tests/test_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import hashlib
import os
from unittest import TestCase
from os.path import join, dirname, isfile

from neon_users_service.databases import UserDatabase
from neon_users_service.databases.sqlite import SQLiteUserDatabase
from neon_users_service.exceptions import ConfigurationError
from neon_users_service.models import User
from neon_users_service.service import NeonUsersService


class TestUsersService(TestCase):
test_db_path = join(dirname(__file__), 'test_db.sqlite')
test_config = {"module": "sqlite",
"sqlite": {"db_path": test_db_path}}

def setUp(self):
if isfile(self.test_db_path):
os.remove(self.test_db_path)

def test_create_service(self):
# Create with default config
service = NeonUsersService()
self.assertIsNotNone(service.config)
self.assertIsInstance(service.database, UserDatabase)
service.shutdown()

# Create with valid passed configuration
service = NeonUsersService(self.test_config)
self.assertIsInstance(service.database, SQLiteUserDatabase)
self.assertTrue(isfile(self.test_db_path))
service.shutdown()

# Create with invalid configuration
with self.assertRaises(ConfigurationError):
NeonUsersService({"module": None})

def test_create_user(self):
service = NeonUsersService(self.test_config)
string_password = "super secret password"
hashed_password = hashlib.sha256(string_password.encode()).hexdigest()
user_1 = service.create_user(User(username="user_1",
password_hash=hashed_password))
input_user_2 = User(username="user_2", password_hash=string_password)
user_2 = service.create_user(input_user_2)
self.assertEqual(user_1.password_hash, hashed_password)
self.assertEqual(user_2.password_hash, hashed_password)
# The input object should not be modified
self.assertNotEqual(user_2, input_user_2)

0 comments on commit 39ff65b

Please sign in to comment.