mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
- Test hook decorator with enum, string, and inferred names - Test Plugin base class hook discovery via __init_subclass__ - Test duplicate registration error - Test implemented_hooks() returns sorted canonical names - Test invoke_hook() and get_hook_handler() methods Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
9 KiB
Python
281 lines
9 KiB
Python
# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
# See LICENSE.txt for license information.
|
|
|
|
"""
|
|
Tests for hook registration mechanism.
|
|
|
|
Tests verify:
|
|
- Decorated methods are discovered via __init_subclass__
|
|
- Duplicate registration fails loudly
|
|
- implemented_hooks() emits canonical names
|
|
- Various decorator forms work correctly
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from mattermost_plugin import Plugin, hook, HookName, HookRegistrationError
|
|
|
|
|
|
class TestHookDecorator:
|
|
"""Tests for the @hook decorator."""
|
|
|
|
def test_hook_with_enum(self) -> None:
|
|
"""Test @hook(HookName.X) form."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def my_activate(self) -> None:
|
|
pass
|
|
|
|
assert TestPlugin.has_hook("OnActivate")
|
|
assert "OnActivate" in TestPlugin.implemented_hooks()
|
|
|
|
def test_hook_with_string(self) -> None:
|
|
"""Test @hook("HookName") form."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook("OnDeactivate")
|
|
def handle_deactivate(self) -> None:
|
|
pass
|
|
|
|
assert TestPlugin.has_hook("OnDeactivate")
|
|
assert "OnDeactivate" in TestPlugin.implemented_hooks()
|
|
|
|
def test_hook_inferred_from_name(self) -> None:
|
|
"""Test @hook form that infers name from method."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook
|
|
def OnActivate(self) -> None:
|
|
pass
|
|
|
|
assert TestPlugin.has_hook("OnActivate")
|
|
|
|
def test_hook_preserves_function_metadata(self) -> None:
|
|
"""Test that @hook preserves __name__ and __doc__."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def my_activate(self) -> None:
|
|
"""My activation handler."""
|
|
pass
|
|
|
|
# Get the handler from registry
|
|
handler = TestPlugin._hook_registry.get("OnActivate")
|
|
assert handler is not None
|
|
assert handler.__name__ == "my_activate"
|
|
assert handler.__doc__ == "My activation handler."
|
|
|
|
def test_invalid_hook_name_raises(self) -> None:
|
|
"""Test that invalid hook names raise HookRegistrationError."""
|
|
with pytest.raises(HookRegistrationError, match="Unknown hook name"):
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook("InvalidHookName")
|
|
def bad_hook(self) -> None:
|
|
pass
|
|
|
|
|
|
class TestPluginRegistration:
|
|
"""Tests for Plugin base class hook discovery."""
|
|
|
|
def test_multiple_hooks_registered(self) -> None:
|
|
"""Test that multiple hooks are discovered."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> None:
|
|
pass
|
|
|
|
@hook(HookName.OnDeactivate)
|
|
def deactivate(self) -> None:
|
|
pass
|
|
|
|
@hook(HookName.MessageWillBePosted)
|
|
def filter_message(self, context: object, post: object) -> tuple:
|
|
return post, ""
|
|
|
|
hooks = TestPlugin.implemented_hooks()
|
|
assert len(hooks) == 3
|
|
assert "OnActivate" in hooks
|
|
assert "OnDeactivate" in hooks
|
|
assert "MessageWillBePosted" in hooks
|
|
|
|
def test_duplicate_hook_raises(self) -> None:
|
|
"""Test that registering the same hook twice raises error."""
|
|
with pytest.raises(HookRegistrationError, match="Duplicate hook registration"):
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate1(self) -> None:
|
|
pass
|
|
|
|
@hook(HookName.OnActivate)
|
|
def activate2(self) -> None:
|
|
pass
|
|
|
|
def test_implemented_hooks_is_sorted(self) -> None:
|
|
"""Test that implemented_hooks() returns sorted list."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.UserHasLoggedIn)
|
|
def login(self) -> None:
|
|
pass
|
|
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> None:
|
|
pass
|
|
|
|
@hook(HookName.MessageWillBePosted)
|
|
def message(self, c: object, p: object) -> tuple:
|
|
return p, ""
|
|
|
|
hooks = TestPlugin.implemented_hooks()
|
|
assert hooks == sorted(hooks)
|
|
|
|
def test_subclass_does_not_inherit_parent_hooks(self) -> None:
|
|
"""Test that subclasses have their own hook registry."""
|
|
|
|
class ParentPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def parent_activate(self) -> None:
|
|
pass
|
|
|
|
class ChildPlugin(ParentPlugin):
|
|
@hook(HookName.OnDeactivate)
|
|
def child_deactivate(self) -> None:
|
|
pass
|
|
|
|
# Child should have both inherited and own hooks
|
|
# (because dir() sees inherited methods)
|
|
child_hooks = ChildPlugin.implemented_hooks()
|
|
assert "OnActivate" in child_hooks
|
|
assert "OnDeactivate" in child_hooks
|
|
|
|
# Parent should only have its own hook
|
|
parent_hooks = ParentPlugin.implemented_hooks()
|
|
assert "OnActivate" in parent_hooks
|
|
assert "OnDeactivate" not in parent_hooks
|
|
|
|
def test_has_hook_returns_false_for_missing(self) -> None:
|
|
"""Test has_hook returns False for unimplemented hooks."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> None:
|
|
pass
|
|
|
|
assert TestPlugin.has_hook("OnActivate")
|
|
assert not TestPlugin.has_hook("OnDeactivate")
|
|
assert not TestPlugin.has_hook("NonExistentHook")
|
|
|
|
|
|
class TestPluginInvocation:
|
|
"""Tests for hook invocation via Plugin instance."""
|
|
|
|
def test_invoke_hook_calls_handler(self) -> None:
|
|
"""Test that invoke_hook calls the registered handler."""
|
|
call_count = [0]
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> str:
|
|
call_count[0] += 1
|
|
return "activated"
|
|
|
|
plugin = TestPlugin()
|
|
result = plugin.invoke_hook("OnActivate")
|
|
|
|
assert call_count[0] == 1
|
|
assert result == "activated"
|
|
|
|
def test_invoke_hook_returns_none_for_missing(self) -> None:
|
|
"""Test that invoke_hook returns None for unimplemented hooks."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> None:
|
|
pass
|
|
|
|
plugin = TestPlugin()
|
|
result = plugin.invoke_hook("OnDeactivate")
|
|
assert result is None
|
|
|
|
def test_invoke_hook_passes_arguments(self) -> None:
|
|
"""Test that invoke_hook passes arguments to handler."""
|
|
received_args = []
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.MessageWillBePosted)
|
|
def filter_message(self, context: object, post: object) -> tuple:
|
|
received_args.append((context, post))
|
|
return post, ""
|
|
|
|
plugin = TestPlugin()
|
|
ctx = {"request_id": "123"}
|
|
post = {"message": "hello"}
|
|
|
|
plugin.invoke_hook("MessageWillBePosted", ctx, post)
|
|
|
|
assert len(received_args) == 1
|
|
assert received_args[0] == (ctx, post)
|
|
|
|
def test_get_hook_handler_returns_bound_method(self) -> None:
|
|
"""Test that get_hook_handler returns a callable bound method."""
|
|
|
|
class TestPlugin(Plugin):
|
|
@hook(HookName.OnActivate)
|
|
def activate(self) -> str:
|
|
return "from handler"
|
|
|
|
plugin = TestPlugin()
|
|
handler = plugin.get_hook_handler("OnActivate")
|
|
|
|
assert handler is not None
|
|
assert callable(handler)
|
|
assert handler() == "from handler"
|
|
|
|
def test_get_hook_handler_returns_none_for_missing(self) -> None:
|
|
"""Test that get_hook_handler returns None for missing hooks."""
|
|
|
|
class TestPlugin(Plugin):
|
|
pass
|
|
|
|
plugin = TestPlugin()
|
|
handler = plugin.get_hook_handler("OnActivate")
|
|
assert handler is None
|
|
|
|
|
|
class TestHookNameEnum:
|
|
"""Tests for HookName enum."""
|
|
|
|
def test_enum_values_are_strings(self) -> None:
|
|
"""Test that HookName values are strings."""
|
|
assert HookName.OnActivate == "OnActivate"
|
|
assert HookName.MessageWillBePosted == "MessageWillBePosted"
|
|
|
|
def test_enum_can_be_used_in_comparisons(self) -> None:
|
|
"""Test that HookName can be compared with strings."""
|
|
assert HookName.OnActivate == "OnActivate"
|
|
assert "OnActivate" == HookName.OnActivate
|
|
|
|
def test_all_hooks_from_proto_are_present(self) -> None:
|
|
"""Test that key hooks from hooks.proto are in the enum."""
|
|
expected_hooks = [
|
|
"OnActivate",
|
|
"OnDeactivate",
|
|
"OnConfigurationChange",
|
|
"OnInstall",
|
|
"MessageWillBePosted",
|
|
"MessageWillBeUpdated",
|
|
"MessageHasBeenPosted",
|
|
"UserHasBeenCreated",
|
|
"UserWillLogIn",
|
|
"ChannelHasBeenCreated",
|
|
"ExecuteCommand",
|
|
"OnWebSocketConnect",
|
|
]
|
|
|
|
for hook_name in expected_hooks:
|
|
assert hasattr(HookName, hook_name), f"Missing hook: {hook_name}"
|
|
assert HookName[hook_name].value == hook_name
|