Compare commits
No commits in common. "5b7117078cb255fbafc69d3659e93747d7b01cee" and "988749d80eedcd8010f0a2e45ba445cc0bb57dc1" have entirely different histories.
5b7117078c
...
988749d80e
10 changed files with 6 additions and 358 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,16 +5,6 @@ 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/)
|
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).
|
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)
|
## v0.21.0 (2025-06-29)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttJamaican"
|
name = "WuttJamaican"
|
||||||
version = "0.22.0"
|
version = "0.21.1"
|
||||||
description = "Base package for Wutta Framework"
|
description = "Base package for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttJamaican -- Base package for Wutta Framework
|
# WuttJamaican -- Base package for Wutta Framework
|
||||||
# Copyright © 2023-2025 Lance Edgar
|
# Copyright © 2023-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -26,11 +26,8 @@ Auth Handler
|
||||||
This defines the default :term:`auth handler`.
|
This defines the default :term:`auth handler`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import secrets
|
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
|
|
||||||
from sqlalchemy import orm
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,8 +73,6 @@ class AuthHandler(GenericHandler):
|
||||||
default logic assumes a "username" but in practice it may be
|
default logic assumes a "username" but in practice it may be
|
||||||
an email address etc. - whatever the user entered.
|
an email address etc. - whatever the user entered.
|
||||||
|
|
||||||
See also :meth:`authenticate_user_token()`.
|
|
||||||
|
|
||||||
:param session: Open :term:`db session`.
|
:param session: Open :term:`db session`.
|
||||||
|
|
||||||
:param username: Usually a string, but also may be a
|
:param username: Usually a string, but also may be a
|
||||||
|
@ -96,33 +91,6 @@ class AuthHandler(GenericHandler):
|
||||||
if self.check_user_password(user, password):
|
if self.check_user_password(user, password):
|
||||||
return user
|
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):
|
def check_user_password(self, user, password, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check a user's password.
|
Check a user's password.
|
||||||
|
@ -577,63 +545,6 @@ class AuthHandler(GenericHandler):
|
||||||
if permission in role.permissions:
|
if permission in role.permissions:
|
||||||
role.permissions.remove(permission)
|
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
|
# internal methods
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -1013,89 +1013,3 @@ def make_config(
|
||||||
extension.startup(config)
|
extension.startup(config)
|
||||||
|
|
||||||
return 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)
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
"""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
|
# WuttJamaican -- Base package for Wutta Framework
|
||||||
# Copyright © 2023-2025 Lance Edgar
|
# Copyright © 2023-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -40,7 +40,6 @@ And the :term:`data models <data model>`:
|
||||||
* :class:`~wuttjamaican.db.model.auth.Permission`
|
* :class:`~wuttjamaican.db.model.auth.Permission`
|
||||||
* :class:`~wuttjamaican.db.model.auth.User`
|
* :class:`~wuttjamaican.db.model.auth.User`
|
||||||
* :class:`~wuttjamaican.db.model.auth.UserRole`
|
* :class:`~wuttjamaican.db.model.auth.UserRole`
|
||||||
* :class:`~wuttjamaican.db.model.auth.UserAPIToken`
|
|
||||||
* :class:`~wuttjamaican.db.model.upgrades.Upgrade`
|
* :class:`~wuttjamaican.db.model.upgrades.Upgrade`
|
||||||
|
|
||||||
And the :term:`batch` model base/mixin classes:
|
And the :term:`batch` model base/mixin classes:
|
||||||
|
@ -52,6 +51,6 @@ And the :term:`batch` model base/mixin classes:
|
||||||
from wuttjamaican.db.util import uuid_column, uuid_fk_column, UUID
|
from wuttjamaican.db.util import uuid_column, uuid_fk_column, UUID
|
||||||
|
|
||||||
from .base import Base, Setting, Person
|
from .base import Base, Setting, Person
|
||||||
from .auth import Role, Permission, User, UserRole, UserAPIToken
|
from .auth import Role, Permission, User, UserRole
|
||||||
from .upgrades import Upgrade
|
from .upgrades import Upgrade
|
||||||
from .batch import BatchMixin, BatchRowMixin
|
from .batch import BatchMixin, BatchRowMixin
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttJamaican -- Base package for Wutta Framework
|
# WuttJamaican -- Base package for Wutta Framework
|
||||||
# Copyright © 2023-2025 Lance Edgar
|
# Copyright © 2023-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -39,8 +39,6 @@ So a user's permissions are "inherited" from the role(s) to which they
|
||||||
belong.
|
belong.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
@ -213,16 +211,6 @@ class User(Base):
|
||||||
# getset_factory=getset_factory,
|
# 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):
|
def __str__(self):
|
||||||
if self.person:
|
if self.person:
|
||||||
name = str(self.person)
|
name = str(self.person)
|
||||||
|
@ -258,36 +246,3 @@ class UserRole(Base):
|
||||||
doc="""
|
doc="""
|
||||||
Reference to the :class:`Role` involved.
|
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,12 +43,3 @@ else:
|
||||||
person = Person(full_name="Barney Rubble")
|
person = Person(full_name="Barney Rubble")
|
||||||
user.person = person
|
user.person = person
|
||||||
self.assertEqual(str(user), "Barney Rubble")
|
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,32 +63,6 @@ else:
|
||||||
user = self.handler.authenticate_user(self.session, 'barney', 'goodpass')
|
user = self.handler.authenticate_user(self.session, 'barney', 'goodpass')
|
||||||
self.assertIsNone(user)
|
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):
|
def test_check_user_password(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
barney = model.User(username='barney')
|
barney = model.User(username='barney')
|
||||||
|
@ -442,32 +416,3 @@ else:
|
||||||
self.handler.revoke_permission(myrole, 'foo')
|
self.handler.revoke_permission(myrole, 'foo')
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.assertEqual(self.session.query(model.Permission).count(), 0)
|
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 import conf
|
||||||
from wuttjamaican.exc import ConfigurationError
|
from wuttjamaican.exc import ConfigurationError
|
||||||
from wuttjamaican.app import AppHandler
|
from wuttjamaican.app import AppHandler
|
||||||
from wuttjamaican.testing import FileTestCase, ConfigTestCase
|
from wuttjamaican.testing import FileTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaConfig(FileTestCase):
|
class TestWuttaConfig(FileTestCase):
|
||||||
|
@ -867,21 +867,3 @@ class TestMakeConfig(FileTestCase):
|
||||||
foo_cls.assert_called_once_with()
|
foo_cls.assert_called_once_with()
|
||||||
foo_obj.configure.assert_called_once_with(testconfig)
|
foo_obj.configure.assert_called_once_with(testconfig)
|
||||||
foo_obj.startup.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