Added owlbot.testing package with pytest plugin, event factories, and recording Owncast stubs for module tests.
Audit / Dependencies (push) Successful in 8s
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 19s
CI / Tests (Python 3.13) (push) Successful in 19s
CI / Tests (Python 3.14) (push) Successful in 16s
CI / Type Checking (push) Successful in 11s
CI / Spelling (push) Successful in 9s
Audit / Dependencies (push) Successful in 8s
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 19s
CI / Tests (Python 3.13) (push) Successful in 19s
CI / Tests (Python 3.14) (push) Successful in 16s
CI / Type Checking (push) Successful in 11s
CI / Spelling (push) Successful in 9s
- owlbot.testing.plugin: fixtures that mirror ModuleLoader's wiring (module_context, module_lifecycle, command/event/route dispatchers, in-memory storage, real Config on a tmp YAML, HttpClient with aioresponses interception, and an aiohttp route_client). - owlbot.testing.helpers: make_user, make_chat_event, and factories for every Owncast event type, with sensible defaults. - owlbot.testing.stubs: RecordingOwncastClient and RecordingOwncastAdminClient that log each call to a .calls list in place of hitting the API. - pyproject.toml: added a 'testing' extra for downstream module authors (pytest, pytest-asyncio, pytest-aiohttp, aioresponses). - tests: migrated in-tree tests to the new helpers, moved module tests under tests/builtin_modules/, and swapped freezegun for time-machine.
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Factory functions for building test event and user objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from owlbot.api.event_types import (
|
||||
ChatEvent,
|
||||
NameChangedEvent,
|
||||
StreamStartedEvent,
|
||||
StreamStatus,
|
||||
StreamStoppedEvent,
|
||||
StreamTitleUpdatedEvent,
|
||||
User,
|
||||
UserJoinedEvent,
|
||||
UserPartedEvent,
|
||||
VisibilityUpdateEvent,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def make_user(
|
||||
*,
|
||||
id: str = "test-user-id", # noqa: A002
|
||||
display_name: str = "TestUser",
|
||||
display_color: int = 0,
|
||||
created_at: datetime | None = None,
|
||||
previous_names: list[str] | None = None,
|
||||
name_changed_at: datetime | None = None,
|
||||
is_bot: bool = False,
|
||||
is_authenticated: bool = False,
|
||||
is_moderator: bool = False,
|
||||
scopes: frozenset[str] | None = None,
|
||||
) -> User:
|
||||
"""Build a User with sensible test defaults.
|
||||
|
||||
:param id: User identifier.
|
||||
:param display_name: Display name shown in chat.
|
||||
:param display_color: Color index for the user's name.
|
||||
:param created_at: Account creation timestamp.
|
||||
:param previous_names: List of previous display names.
|
||||
:param name_changed_at: Timestamp of last name change.
|
||||
:param is_bot: Whether the user is a bot.
|
||||
:param is_authenticated: Whether the user is authenticated.
|
||||
:param is_moderator: When True, adds ``"MODERATOR"`` to scopes.
|
||||
:param scopes: Scope set for the user; defaults to empty.
|
||||
:return: A populated User instance.
|
||||
"""
|
||||
resolved_scopes = scopes if scopes is not None else frozenset()
|
||||
if is_moderator:
|
||||
resolved_scopes = resolved_scopes | frozenset({"MODERATOR"})
|
||||
|
||||
return User(
|
||||
id=id,
|
||||
display_name=display_name,
|
||||
display_color=display_color,
|
||||
created_at=created_at,
|
||||
previous_names=previous_names if previous_names is not None else [],
|
||||
name_changed_at=name_changed_at,
|
||||
is_bot=is_bot,
|
||||
is_authenticated=is_authenticated,
|
||||
scopes=resolved_scopes,
|
||||
)
|
||||
|
||||
|
||||
def make_chat_event(
|
||||
*,
|
||||
user: User | None = None,
|
||||
raw_body: str = "test message",
|
||||
body: str | None = None,
|
||||
client_id: int = 0,
|
||||
message_id: str = "test-msg-id",
|
||||
is_visible: bool = True,
|
||||
timestamp: datetime | None = None,
|
||||
) -> ChatEvent:
|
||||
"""Build a ChatEvent with sensible test defaults.
|
||||
|
||||
``raw_body`` is the primary field because it is what the command
|
||||
dispatcher parses; tests dispatching commands should set it to the
|
||||
command text. ``body`` defaults to a literal copy of ``raw_body``. It
|
||||
is **not** Owncast-rendered HTML, so tests that need the rendered form
|
||||
must pass ``body=`` explicitly.
|
||||
|
||||
:param user: The chat user; defaults to ``make_user()``.
|
||||
:param raw_body: Original user input; also used as the body default.
|
||||
:param body: Rendered message body; defaults to *raw_body*.
|
||||
:param client_id: Client connection identifier.
|
||||
:param message_id: Unique message identifier.
|
||||
:param is_visible: Whether the message is visible. Defaults to True;
|
||||
chat messages are normally shown.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated ChatEvent instance.
|
||||
"""
|
||||
return ChatEvent(
|
||||
user=user if user is not None else make_user(),
|
||||
body=body if body is not None else raw_body,
|
||||
raw_body=raw_body,
|
||||
client_id=client_id,
|
||||
message_id=message_id,
|
||||
is_visible=is_visible,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_user_joined_event(
|
||||
*,
|
||||
user: User | None = None,
|
||||
client_id: int = 0,
|
||||
event_id: str = "test-join-id",
|
||||
timestamp: datetime | None = None,
|
||||
) -> UserJoinedEvent:
|
||||
"""Build a UserJoinedEvent with sensible test defaults.
|
||||
|
||||
:param user: The joining user; defaults to ``make_user()``.
|
||||
:param client_id: Client connection identifier.
|
||||
:param event_id: Unique event identifier.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated UserJoinedEvent instance.
|
||||
"""
|
||||
return UserJoinedEvent(
|
||||
user=user if user is not None else make_user(),
|
||||
client_id=client_id,
|
||||
event_id=event_id,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_user_parted_event(
|
||||
*,
|
||||
user: User | None = None,
|
||||
client_id: int = 0,
|
||||
event_id: str = "test-part-id",
|
||||
timestamp: datetime | None = None,
|
||||
) -> UserPartedEvent:
|
||||
"""Build a UserPartedEvent with sensible test defaults.
|
||||
|
||||
:param user: The departing user; defaults to ``make_user()``.
|
||||
:param client_id: Client connection identifier.
|
||||
:param event_id: Unique event identifier.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated UserPartedEvent instance.
|
||||
"""
|
||||
return UserPartedEvent(
|
||||
user=user if user is not None else make_user(),
|
||||
client_id=client_id,
|
||||
event_id=event_id,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_name_changed_event(
|
||||
*,
|
||||
user: User | None = None,
|
||||
client_id: int = 0,
|
||||
new_name: str = "NewName",
|
||||
event_id: str = "test-name-change-id",
|
||||
timestamp: datetime | None = None,
|
||||
) -> NameChangedEvent:
|
||||
"""Build a NameChangedEvent with sensible test defaults.
|
||||
|
||||
:param user: The user changing names; defaults to ``make_user()``.
|
||||
:param client_id: Client connection identifier.
|
||||
:param new_name: The new display name.
|
||||
:param event_id: Unique event identifier.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated NameChangedEvent instance.
|
||||
"""
|
||||
return NameChangedEvent(
|
||||
user=user if user is not None else make_user(),
|
||||
client_id=client_id,
|
||||
new_name=new_name,
|
||||
event_id=event_id,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_stream_started_event(
|
||||
*,
|
||||
server_id: str = "test-server-id",
|
||||
server_name: str = "Test Server",
|
||||
stream_title: str = "Test Stream",
|
||||
summary: str = "",
|
||||
timestamp: datetime | None = None,
|
||||
) -> StreamStartedEvent:
|
||||
"""Build a StreamStartedEvent with sensible test defaults.
|
||||
|
||||
:param server_id: Server identifier.
|
||||
:param server_name: Server display name.
|
||||
:param stream_title: Current stream title.
|
||||
:param summary: Stream summary text.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated StreamStartedEvent instance.
|
||||
"""
|
||||
return StreamStartedEvent(
|
||||
server_id=server_id,
|
||||
server_name=server_name,
|
||||
stream_title=stream_title,
|
||||
summary=summary,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_stream_stopped_event(
|
||||
*,
|
||||
server_id: str = "test-server-id",
|
||||
server_name: str = "Test Server",
|
||||
stream_title: str = "Test Stream",
|
||||
summary: str = "",
|
||||
timestamp: datetime | None = None,
|
||||
) -> StreamStoppedEvent:
|
||||
"""Build a StreamStoppedEvent with sensible test defaults.
|
||||
|
||||
:param server_id: Server identifier.
|
||||
:param server_name: Server display name.
|
||||
:param stream_title: Current stream title.
|
||||
:param summary: Stream summary text.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated StreamStoppedEvent instance.
|
||||
"""
|
||||
return StreamStoppedEvent(
|
||||
server_id=server_id,
|
||||
server_name=server_name,
|
||||
stream_title=stream_title,
|
||||
summary=summary,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_stream_status(
|
||||
*,
|
||||
last_connect_time: datetime | None = None,
|
||||
last_disconnect_time: datetime | None = None,
|
||||
version_number: str = "",
|
||||
stream_title: str = "Test Stream",
|
||||
viewer_count: int = 0,
|
||||
overall_max_viewer_count: int = 0,
|
||||
session_max_viewer_count: int = 0,
|
||||
is_online: bool = False,
|
||||
) -> StreamStatus:
|
||||
"""Build a StreamStatus with sensible test defaults.
|
||||
|
||||
:param last_connect_time: Timestamp of the last stream connect.
|
||||
:param last_disconnect_time: Timestamp of the last stream disconnect.
|
||||
:param version_number: Owncast server version string.
|
||||
:param stream_title: Current stream title.
|
||||
:param viewer_count: Current viewer count.
|
||||
:param overall_max_viewer_count: All-time peak viewer count.
|
||||
:param session_max_viewer_count: Current-session peak viewer count.
|
||||
:param is_online: Whether the stream is currently live.
|
||||
:return: A populated StreamStatus instance.
|
||||
"""
|
||||
return StreamStatus(
|
||||
last_connect_time=last_connect_time,
|
||||
last_disconnect_time=last_disconnect_time,
|
||||
version_number=version_number,
|
||||
stream_title=stream_title,
|
||||
viewer_count=viewer_count,
|
||||
overall_max_viewer_count=overall_max_viewer_count,
|
||||
session_max_viewer_count=session_max_viewer_count,
|
||||
is_online=is_online,
|
||||
)
|
||||
|
||||
|
||||
def make_stream_title_updated_event(
|
||||
*,
|
||||
server_id: str = "test-server-id",
|
||||
server_name: str = "Test Server",
|
||||
stream_title: str = "Test Stream",
|
||||
summary: str = "",
|
||||
status: StreamStatus | None = None,
|
||||
timestamp: datetime | None = None,
|
||||
) -> StreamTitleUpdatedEvent:
|
||||
"""Build a StreamTitleUpdatedEvent with sensible test defaults.
|
||||
|
||||
:param server_id: Server identifier.
|
||||
:param server_name: Server display name.
|
||||
:param stream_title: Current stream title.
|
||||
:param summary: Stream summary text.
|
||||
:param status: Optional stream status snapshot.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated StreamTitleUpdatedEvent instance.
|
||||
"""
|
||||
return StreamTitleUpdatedEvent(
|
||||
server_id=server_id,
|
||||
server_name=server_name,
|
||||
stream_title=stream_title,
|
||||
summary=summary,
|
||||
status=status,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def make_visibility_update_event(
|
||||
*,
|
||||
event_id: str = "test-vis-id",
|
||||
message_ids: list[str] | None = None,
|
||||
is_visible: bool = False,
|
||||
timestamp: datetime | None = None,
|
||||
) -> VisibilityUpdateEvent:
|
||||
"""Build a VisibilityUpdateEvent with sensible test defaults.
|
||||
|
||||
:param event_id: Unique event identifier.
|
||||
:param message_ids: List of affected message IDs.
|
||||
:param is_visible: Whether the messages are now visible. Defaults to
|
||||
False; visibility updates are primarily used to hide messages.
|
||||
:param timestamp: Event timestamp.
|
||||
:return: A populated VisibilityUpdateEvent instance.
|
||||
"""
|
||||
return VisibilityUpdateEvent(
|
||||
event_id=event_id,
|
||||
message_ids=message_ids if message_ids is not None else [],
|
||||
is_visible=is_visible,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
Reference in New Issue
Block a user