Compare commits
	
		
			3 commits
		
	
	
		
			988749d80e
			...
			5b7117078c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b7117078c | |||
| ad6a3377ab | |||
| a721e63275 | 
					 10 changed files with 358 additions and 6 deletions
				
			
		
							
								
								
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -5,6 +5,16 @@ All notable changes to WuttJamaican will be documented in this file.
 | 
			
		|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 | 
			
		||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 | 
			
		||||
 | 
			
		||||
## v0.22.0 (2025-08-09)
 | 
			
		||||
 | 
			
		||||
### Feat
 | 
			
		||||
 | 
			
		||||
- add WuttaConfigProfile base class
 | 
			
		||||
- add user API tokens; handler methods to manage/authenticate
 | 
			
		||||
- allow arbitrary kwargs for `config.get()` and `app.get_setting()`
 | 
			
		||||
 | 
			
		||||
## v0.21.1 (2025-06-29)
 | 
			
		||||
 | 
			
		||||
## v0.21.0 (2025-06-29)
 | 
			
		||||
 | 
			
		||||
### Feat
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ build-backend = "hatchling.build"
 | 
			
		|||
 | 
			
		||||
[project]
 | 
			
		||||
name = "WuttJamaican"
 | 
			
		||||
version = "0.21.1"
 | 
			
		||||
version = "0.22.0"
 | 
			
		||||
description = "Base package for Wutta Framework"
 | 
			
		||||
readme = "README.md"
 | 
			
		||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2023-2024 Lance Edgar
 | 
			
		||||
#  Copyright © 2023-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -26,8 +26,11 @@ Auth Handler
 | 
			
		|||
This defines the default :term:`auth handler`.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import secrets
 | 
			
		||||
import uuid as _uuid
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.app import GenericHandler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +76,8 @@ class AuthHandler(GenericHandler):
 | 
			
		|||
        default logic assumes a "username" but in practice it may be
 | 
			
		||||
        an email address etc. - whatever the user entered.
 | 
			
		||||
 | 
			
		||||
        See also :meth:`authenticate_user_token()`.
 | 
			
		||||
 | 
			
		||||
        :param session: Open :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param username: Usually a string, but also may be a
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +96,33 @@ class AuthHandler(GenericHandler):
 | 
			
		|||
            if self.check_user_password(user, password):
 | 
			
		||||
                return user
 | 
			
		||||
 | 
			
		||||
    def authenticate_user_token(self, session, token):
 | 
			
		||||
        """
 | 
			
		||||
        Authenticate the given user API token string, and if valid,
 | 
			
		||||
        return the corresponding user.
 | 
			
		||||
 | 
			
		||||
        See also :meth:`authenticate_user()`.
 | 
			
		||||
 | 
			
		||||
        :param session: Open :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param token: Raw token string for the user.
 | 
			
		||||
 | 
			
		||||
        :returns: :class:`~wuttjamaican.db.model.auth.User` instance,
 | 
			
		||||
           or ``None``.
 | 
			
		||||
        """
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            token = session.query(model.UserAPIToken)\
 | 
			
		||||
                           .filter(model.UserAPIToken.token_string == token)\
 | 
			
		||||
                           .one()
 | 
			
		||||
        except orm.exc.NoResultFound:
 | 
			
		||||
            pass
 | 
			
		||||
        else:
 | 
			
		||||
            user = token.user
 | 
			
		||||
            if user.active:
 | 
			
		||||
                return user
 | 
			
		||||
 | 
			
		||||
    def check_user_password(self, user, password, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Check a user's password.
 | 
			
		||||
| 
						 | 
				
			
			@ -545,6 +577,63 @@ class AuthHandler(GenericHandler):
 | 
			
		|||
        if permission in role.permissions:
 | 
			
		||||
            role.permissions.remove(permission)
 | 
			
		||||
 | 
			
		||||
    ##############################
 | 
			
		||||
    # API token methods
 | 
			
		||||
    ##############################
 | 
			
		||||
 | 
			
		||||
    def add_api_token(self, user, description):
 | 
			
		||||
        """
 | 
			
		||||
        Add and return a new API token for the user.
 | 
			
		||||
 | 
			
		||||
        This calls :meth:`generate_api_token_string()` to obtain the
 | 
			
		||||
        actual token string.
 | 
			
		||||
 | 
			
		||||
        See also :meth:`delete_api_token()`.
 | 
			
		||||
 | 
			
		||||
        :param user: :class:`~wuttjamaican.db.model.auth.User`
 | 
			
		||||
           instance for which to add the token.
 | 
			
		||||
 | 
			
		||||
        :param description: String description for the token.
 | 
			
		||||
 | 
			
		||||
        :rtype: :class:`~wuttjamaican.db.model.auth.UserAPIToken`
 | 
			
		||||
        """
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        session = self.app.get_session(user)
 | 
			
		||||
 | 
			
		||||
        # generate raw token
 | 
			
		||||
        token_string = self.generate_api_token_string()
 | 
			
		||||
 | 
			
		||||
        # persist token in DB
 | 
			
		||||
        token = model.UserAPIToken(
 | 
			
		||||
            description=description,
 | 
			
		||||
            token_string=token_string)
 | 
			
		||||
        user.api_tokens.append(token)
 | 
			
		||||
        session.add(token)
 | 
			
		||||
 | 
			
		||||
        return token
 | 
			
		||||
 | 
			
		||||
    def generate_api_token_string(self):
 | 
			
		||||
        """
 | 
			
		||||
        Generate a new *raw* API token string.
 | 
			
		||||
 | 
			
		||||
        This is called by :meth:`add_api_token()`.
 | 
			
		||||
 | 
			
		||||
        :returns: Raw API token string.
 | 
			
		||||
        """
 | 
			
		||||
        return secrets.token_urlsafe()
 | 
			
		||||
 | 
			
		||||
    def delete_api_token(self, token):
 | 
			
		||||
        """
 | 
			
		||||
        Delete the given API token.
 | 
			
		||||
 | 
			
		||||
        See also :meth:`add_api_token()`.
 | 
			
		||||
 | 
			
		||||
        :param token:
 | 
			
		||||
           :class:`~wuttjamaican.db.model.auth.UserAPIToken` instance.
 | 
			
		||||
        """
 | 
			
		||||
        session = self.app.get_session(token)
 | 
			
		||||
        session.delete(token)
 | 
			
		||||
 | 
			
		||||
    ##############################
 | 
			
		||||
    # internal methods
 | 
			
		||||
    ##############################
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1013,3 +1013,89 @@ def make_config(
 | 
			
		|||
            extension.startup(config)
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WuttaConfigProfile:
 | 
			
		||||
    """
 | 
			
		||||
    Base class to represent a configured "profile" in the context of
 | 
			
		||||
    some service etc.
 | 
			
		||||
 | 
			
		||||
    :param config: App :term:`config object`.
 | 
			
		||||
 | 
			
		||||
    :param key: Config key for the profile.
 | 
			
		||||
 | 
			
		||||
    Generally each subclass will represent a certain type of config
 | 
			
		||||
    profile, and each instance will represent a single profile
 | 
			
		||||
    (identified by the ``key``).
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, config, key):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.app = self.config.get_app()
 | 
			
		||||
        self.key = key
 | 
			
		||||
        self.load()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def section(self):
 | 
			
		||||
        """
 | 
			
		||||
        The primary config section under which profiles may be
 | 
			
		||||
        defined.
 | 
			
		||||
 | 
			
		||||
        There is no default; each subclass must declare it.
 | 
			
		||||
 | 
			
		||||
        This corresponds to the typical INI file section, for instance
 | 
			
		||||
        a section of ``wutta.telemetry`` assumes file contents like:
 | 
			
		||||
 | 
			
		||||
        .. code-block:: ini
 | 
			
		||||
 | 
			
		||||
           [wutta.telemetry]
 | 
			
		||||
           default.submit_url = /nodes/telemetry
 | 
			
		||||
           special.submit_url = /nodes/telemetry-special
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def load(self):
 | 
			
		||||
        """
 | 
			
		||||
        Read all relevant settings from config, and assign attributes
 | 
			
		||||
        on the profile instance accordingly.
 | 
			
		||||
 | 
			
		||||
        There is no default logic but subclass will generally override.
 | 
			
		||||
 | 
			
		||||
        While a caller can use :meth:`get_str()` to obtain arbitrary
 | 
			
		||||
        config values dynamically, it is often useful for the profile
 | 
			
		||||
        to pre-load some config values.  This allows "smarter"
 | 
			
		||||
        interpretation of config values in some cases, and at least
 | 
			
		||||
        ensures common/shared logic.
 | 
			
		||||
 | 
			
		||||
        There is no constraint or other guidance in terms of which
 | 
			
		||||
        profile attributes might be set by this method.  Subclass
 | 
			
		||||
        should document if necessary.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    def get_str(self, option, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Get a string value for the profile, from config.
 | 
			
		||||
 | 
			
		||||
        :param option: Name of config option for which to return value.
 | 
			
		||||
 | 
			
		||||
        This just calls :meth:`~WuttaConfig.get()` on the config
 | 
			
		||||
        object, but for a particular setting name which it composes
 | 
			
		||||
        dynamically.
 | 
			
		||||
 | 
			
		||||
        Assuming a config file like:
 | 
			
		||||
 | 
			
		||||
        .. code-block:: ini
 | 
			
		||||
 | 
			
		||||
           [wutta.telemetry]
 | 
			
		||||
           default.submit_url = /nodes/telemetry
 | 
			
		||||
 | 
			
		||||
        Then a ``default`` profile under the ``wutta.telemetry``
 | 
			
		||||
        section would effectively have a ``submit_url`` option::
 | 
			
		||||
 | 
			
		||||
           class TelemetryProfile(WuttaConfigProfile):
 | 
			
		||||
               section = "wutta.telemetry"
 | 
			
		||||
 | 
			
		||||
           profile = TelemetryProfile("default")
 | 
			
		||||
           url = profile.get_str("submit_url")
 | 
			
		||||
        """
 | 
			
		||||
        return self.config.get(f'{self.section}.{self.key}.{option}', **kwargs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
"""add user_api_token
 | 
			
		||||
 | 
			
		||||
Revision ID: efdcb2c75034
 | 
			
		||||
Revises: 6bf900765500
 | 
			
		||||
Create Date: 2025-08-08 08:58:19.376105
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
import wuttjamaican.db.util
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = 'efdcb2c75034'
 | 
			
		||||
down_revision: Union[str, None] = '6bf900765500'
 | 
			
		||||
branch_labels: Union[str, Sequence[str], None] = None
 | 
			
		||||
depends_on: Union[str, Sequence[str], None] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade() -> None:
 | 
			
		||||
 | 
			
		||||
    # user_api_token
 | 
			
		||||
    op.create_table('user_api_token',
 | 
			
		||||
                    sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
			
		||||
                    sa.Column('user_uuid', wuttjamaican.db.util.UUID(), nullable=False),
 | 
			
		||||
                    sa.Column('description', sa.String(length=255), nullable=False),
 | 
			
		||||
                    sa.Column('token_string', sa.String(length=255), nullable=False),
 | 
			
		||||
                    sa.Column('created', sa.DateTime(timezone=True), nullable=False),
 | 
			
		||||
                    sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], name=op.f('fk_user_api_token_user_uuid_user')),
 | 
			
		||||
                    sa.PrimaryKeyConstraint('uuid', name=op.f('pk_user_api_token'))
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
 | 
			
		||||
    # user_api_token
 | 
			
		||||
    op.drop_table('user_api_token')
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2023-2024 Lance Edgar
 | 
			
		||||
#  Copyright © 2023-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +40,7 @@ And the :term:`data models <data model>`:
 | 
			
		|||
* :class:`~wuttjamaican.db.model.auth.Permission`
 | 
			
		||||
* :class:`~wuttjamaican.db.model.auth.User`
 | 
			
		||||
* :class:`~wuttjamaican.db.model.auth.UserRole`
 | 
			
		||||
* :class:`~wuttjamaican.db.model.auth.UserAPIToken`
 | 
			
		||||
* :class:`~wuttjamaican.db.model.upgrades.Upgrade`
 | 
			
		||||
 | 
			
		||||
And the :term:`batch` model base/mixin classes:
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +52,6 @@ And the :term:`batch` model base/mixin classes:
 | 
			
		|||
from wuttjamaican.db.util import uuid_column, uuid_fk_column, UUID
 | 
			
		||||
 | 
			
		||||
from .base import Base, Setting, Person
 | 
			
		||||
from .auth import Role, Permission, User, UserRole
 | 
			
		||||
from .auth import Role, Permission, User, UserRole, UserAPIToken
 | 
			
		||||
from .upgrades import Upgrade
 | 
			
		||||
from .batch import BatchMixin, BatchRowMixin
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2023-2024 Lance Edgar
 | 
			
		||||
#  Copyright © 2023-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +39,8 @@ So a user's permissions are "inherited" from the role(s) to which they
 | 
			
		|||
belong.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
from sqlalchemy.ext.associationproxy import association_proxy
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +213,16 @@ class User(Base):
 | 
			
		|||
        # getset_factory=getset_factory,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    api_tokens = orm.relationship(
 | 
			
		||||
        'UserAPIToken',
 | 
			
		||||
        back_populates='user',
 | 
			
		||||
        order_by='UserAPIToken.created',
 | 
			
		||||
        cascade='all, delete-orphan',
 | 
			
		||||
        cascade_backrefs=False,
 | 
			
		||||
        doc="""
 | 
			
		||||
        List of :class:`UserAPIToken` instances belonging to the user.
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if self.person:
 | 
			
		||||
            name = str(self.person)
 | 
			
		||||
| 
						 | 
				
			
			@ -246,3 +258,36 @@ class UserRole(Base):
 | 
			
		|||
        doc="""
 | 
			
		||||
        Reference to the :class:`Role` involved.
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserAPIToken(Base):
 | 
			
		||||
    """
 | 
			
		||||
    User authentication token for use with HTTP API
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_api_token'
 | 
			
		||||
 | 
			
		||||
    uuid = uuid_column()
 | 
			
		||||
 | 
			
		||||
    user_uuid = uuid_fk_column('user.uuid', nullable=False)
 | 
			
		||||
    user = orm.relationship(
 | 
			
		||||
        User,
 | 
			
		||||
        back_populates='api_tokens',
 | 
			
		||||
        cascade_backrefs=False,
 | 
			
		||||
        doc="""
 | 
			
		||||
        Reference to the :class:`User` whose token this is.
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
    description = sa.Column(sa.String(length=255), nullable=False, doc="""
 | 
			
		||||
    Description of the token.
 | 
			
		||||
    """)
 | 
			
		||||
 | 
			
		||||
    token_string = sa.Column(sa.String(length=255), nullable=False, doc="""
 | 
			
		||||
    Raw token string, to be used by API clients.
 | 
			
		||||
    """)
 | 
			
		||||
 | 
			
		||||
    created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
 | 
			
		||||
    Date/time when the token was created.
 | 
			
		||||
    """)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.description or ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,3 +43,12 @@ else:
 | 
			
		|||
            person = Person(full_name="Barney Rubble")
 | 
			
		||||
            user.person = person
 | 
			
		||||
            self.assertEqual(str(user), "Barney Rubble")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    class TestUserAPIToken(TestCase):
 | 
			
		||||
 | 
			
		||||
        def test_str(self):
 | 
			
		||||
            token = model.UserAPIToken()
 | 
			
		||||
            self.assertEqual(str(token), "")
 | 
			
		||||
            token.description = "test token"
 | 
			
		||||
            self.assertEqual(str(token), "test token")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,6 +63,32 @@ else:
 | 
			
		|||
            user = self.handler.authenticate_user(self.session, 'barney', 'goodpass')
 | 
			
		||||
            self.assertIsNone(user)
 | 
			
		||||
 | 
			
		||||
        def test_authenticate_user_token(self):
 | 
			
		||||
            model = self.app.model
 | 
			
		||||
            barney = model.User(username='barney')
 | 
			
		||||
            self.session.add(barney)
 | 
			
		||||
            token = self.handler.add_api_token(barney, "test token")
 | 
			
		||||
            self.session.commit()
 | 
			
		||||
 | 
			
		||||
            user = self.handler.authenticate_user_token(self.session, None)
 | 
			
		||||
            self.assertIsNone(user)
 | 
			
		||||
 | 
			
		||||
            user = self.handler.authenticate_user_token(self.session, token.token_string)
 | 
			
		||||
            self.assertIs(user, barney)
 | 
			
		||||
 | 
			
		||||
            barney.active = False
 | 
			
		||||
            self.session.flush()
 | 
			
		||||
            user = self.handler.authenticate_user_token(self.session, token.token_string)
 | 
			
		||||
            self.assertIsNone(user)
 | 
			
		||||
 | 
			
		||||
            barney.active = True
 | 
			
		||||
            self.session.flush()
 | 
			
		||||
            user = self.handler.authenticate_user_token(self.session, token.token_string)
 | 
			
		||||
            self.assertIs(user, barney)
 | 
			
		||||
 | 
			
		||||
            user = self.handler.authenticate_user_token(self.session, 'bad-token')
 | 
			
		||||
            self.assertIsNone(user)
 | 
			
		||||
 | 
			
		||||
        def test_check_user_password(self):
 | 
			
		||||
            model = self.app.model
 | 
			
		||||
            barney = model.User(username='barney')
 | 
			
		||||
| 
						 | 
				
			
			@ -416,3 +442,32 @@ else:
 | 
			
		|||
            self.handler.revoke_permission(myrole, 'foo')
 | 
			
		||||
            self.session.commit()
 | 
			
		||||
            self.assertEqual(self.session.query(model.Permission).count(), 0)
 | 
			
		||||
 | 
			
		||||
        def test_generate_api_token_string(self):
 | 
			
		||||
            token = self.handler.generate_api_token_string()
 | 
			
		||||
            # TODO: not sure how to propertly test this yet...
 | 
			
		||||
            self.assertEqual(len(token), 43)
 | 
			
		||||
 | 
			
		||||
        def test_add_api_token(self):
 | 
			
		||||
            model = self.app.model
 | 
			
		||||
            barney = model.User(username='barney')
 | 
			
		||||
            self.session.add(barney)
 | 
			
		||||
 | 
			
		||||
            token = self.handler.add_api_token(barney, "test token")
 | 
			
		||||
            self.assertIs(token.user, barney)
 | 
			
		||||
            self.assertEqual(token.description, "test token")
 | 
			
		||||
            # TODO: not sure how to propertly test this yet...
 | 
			
		||||
            self.assertEqual(len(token.token_string), 43)
 | 
			
		||||
 | 
			
		||||
        def test_delete_api_token(self):
 | 
			
		||||
            model = self.app.model
 | 
			
		||||
            barney = model.User(username='barney')
 | 
			
		||||
            self.session.add(barney)
 | 
			
		||||
            token = self.handler.add_api_token(barney, "test token")
 | 
			
		||||
            self.session.commit()
 | 
			
		||||
 | 
			
		||||
            self.session.refresh(barney)
 | 
			
		||||
            self.assertEqual(len(barney.api_tokens), 1)
 | 
			
		||||
            self.handler.delete_api_token(token)
 | 
			
		||||
            self.session.refresh(barney)
 | 
			
		||||
            self.assertEqual(len(barney.api_tokens), 0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ from wuttjamaican import conf as mod
 | 
			
		|||
from wuttjamaican import conf
 | 
			
		||||
from wuttjamaican.exc import ConfigurationError
 | 
			
		||||
from wuttjamaican.app import AppHandler
 | 
			
		||||
from wuttjamaican.testing import FileTestCase
 | 
			
		||||
from wuttjamaican.testing import FileTestCase, ConfigTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestWuttaConfig(FileTestCase):
 | 
			
		||||
| 
						 | 
				
			
			@ -867,3 +867,21 @@ class TestMakeConfig(FileTestCase):
 | 
			
		|||
                foo_cls.assert_called_once_with()
 | 
			
		||||
                foo_obj.configure.assert_called_once_with(testconfig)
 | 
			
		||||
                foo_obj.startup.assert_called_once_with(testconfig)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestWuttaConfigProfile(ConfigTestCase):
 | 
			
		||||
 | 
			
		||||
    def make_profile(self, key):
 | 
			
		||||
        return mod.WuttaConfigProfile(self.config, key)
 | 
			
		||||
 | 
			
		||||
    def test_section(self):
 | 
			
		||||
        profile = self.make_profile('default')
 | 
			
		||||
        self.assertRaises(NotImplementedError, getattr, profile, 'section')
 | 
			
		||||
 | 
			
		||||
    def test_get_str(self):
 | 
			
		||||
        self.config.setdefault('wutta.telemetry.default.submit_url', '/nodes/telemetry')
 | 
			
		||||
        with patch.object(mod.WuttaConfigProfile, 'section', new='wutta.telemetry'):
 | 
			
		||||
            profile = self.make_profile('default')
 | 
			
		||||
            self.assertEqual(profile.section, 'wutta.telemetry')
 | 
			
		||||
            url = profile.get_str('submit_url')
 | 
			
		||||
            self.assertEqual(url, '/nodes/telemetry')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue