Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate the Fediverse protocol API to use acct identifiers, not Actor URIs #385

Merged
merged 2 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ FEDITEST?=$(VENV)/bin/feditest -v
DOMAIN?=--domain 1234.lan


default : all
default : lint

all : build lint tests
all : lint tests

build : venv
$(VENV)/bin/pip install .
Expand Down Expand Up @@ -55,7 +55,7 @@ tests.smoke : venv
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api.session.json --constellation tests.smoke/mastodon.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api.session.json --constellation tests.smoke/wordpress.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/mastodon_mastodon.ubos.constellation.json $(DOMAIN)
# Currently broken: $(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/wordpress_mastodon.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/wordpress_mastodon.ubos.constellation.json $(DOMAIN)

release :
@which $(PYTHON) || ( echo 'No executable called "python". Append your python to the make command, like "make PYTHON=your-python"' && false )
Expand Down
2 changes: 1 addition & 1 deletion src/feditest/nodedrivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def start_delay(self) -> float:


def __str__(self) -> str:
return f'NodeConfiguration ({ type(self).__name__ }): node driver: "{ self.node_driver }", app: "{ self.app }", hostname: "{ self.hostname }"'
return f'NodeConfiguration: node driver: "{ self.node_driver }", app: "{ self.app }", hostname: "{ self.hostname }"'


class Node(ABC):
Expand Down
228 changes: 51 additions & 177 deletions src/feditest/nodedrivers/fallback/fediverse.py

Large diffs are not rendered by default.

365 changes: 175 additions & 190 deletions src/feditest/nodedrivers/mastodon/__init__.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/feditest/nodedrivers/mastodon/ubos.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from feditest.nodedrivers.mastodon import (
MastodonAccount,
MastodonNode,
MastodonNonExistingAccount,
MastodonUserPasswordAccount,
NodeWithMastodonApiConfiguration,
EMAIL_ACCOUNT_FIELD,
Expand Down Expand Up @@ -49,6 +48,7 @@
TLSCERT_PAR,
TLSKEY_PAR
)
from feditest.protocols.fediverse import FediverseNonExistingAccount
from feditest.registry import registry_singleton
from feditest.reporting import error, trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameterMalformedError
Expand Down Expand Up @@ -199,7 +199,7 @@ def provision_non_existing_account_for_role(self, role: str | None = None) -> No
# We just make it up
userid = self._generate_candidate_userid()

return MastodonNonExistingAccount(role, userid)
return FediverseNonExistingAccount(role, userid)


def add_cert_to_trust_store(self, root_cert: str) -> None:
Expand Down Expand Up @@ -266,7 +266,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down
103 changes: 27 additions & 76 deletions src/feditest/nodedrivers/wordpress/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
"""

import re
import time
from typing import cast

from feditest.nodedrivers import (
Expand All @@ -18,11 +18,17 @@
from feditest.nodedrivers.mastodon import (
AccountOnNodeWithMastodonAPI,
Mastodon, # Re-import from there to avoid duplicating the package import hackery
MastodonOAuthApp,
NodeWithMastodonAPI,
NodeWithMastodonApiConfiguration
)
from feditest.protocols.fediverse import FediverseNode
from feditest.protocols.fediverse import (
ROLE_ACCOUNT_FIELD,
ROLE_NON_EXISTING_ACCOUNT_FIELD,
USERID_ACCOUNT_FIELD,
USERID_NON_EXISTING_ACCOUNT_FIELD,
FediverseNode,
FediverseNonExistingAccount
)
from feditest.reporting import is_trace_active, trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameter
from feditest.utils import boolean_parse_validate, hostname_validate, prompt_user
Expand All @@ -43,41 +49,11 @@ def _oauth_token_validate(candidate: str) -> str | None:
return candidate if len(candidate)>10 else None


def _userid_validate(candidate: str) -> str | None:
"""
Validate a WordPress user name. Avoids user input errors.
FIXME this is a wild guess and can be better.
"""
candidate = candidate.strip()
return candidate if re.match(r'[a-zA-Z0-9_]', candidate) else None


USERID_ACCOUNT_FIELD = TestPlanNodeAccountField(
'userid',
"""Mastodon userid for a user (e.g. "joe") (required).""",
_userid_validate
)
OAUTH_TOKEN_ACCOUNT_FIELD = TestPlanNodeAccountField(
'oauth_token',
"""OAuth token of a user so the "Enable Mastodon apps" API can be invoked.""",
_oauth_token_validate
)
ROLE_ACCOUNT_FIELD = TestPlanNodeAccountField(
'role',
"""A symbolic name for the Account as used by tests (optional).""",
lambda x: len(x)
)

USERID_NON_EXISTING_ACCOUNT_FIELD = TestPlanNodeNonExistingAccountField(
'userid',
"""Mastodon userid for a non-existing user (e.g. "joe") (required).""",
_userid_validate
)
ROLE_NON_EXISTING_ACCOUNT_FIELD = TestPlanNodeNonExistingAccountField(
'role',
"""A symbolic name for the non-existing Account as used by tests (optional).""",
lambda x: len(x)
)


class WordPressAccount(AccountOnNodeWithMastodonAPI):
Expand Down Expand Up @@ -105,14 +81,10 @@ def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str


@property
def actor_uri(self):
return f'https://{ self.node.hostname }/author/{ self.userid }/'


def mastodon_user_client(self) -> Mastodon:
if self._mastodon_user_client is None:
node = cast(NodeWithMastodonAPI, self._node)
oauth_app = cast(MastodonOAuthApp, node._mastodon_oauth_app)
oauth_app = node._obtain_mastodon_oauth_app()
self._ensure_oauth_token(oauth_app.client_id)
trace(f'Logging into WordPress at "{ oauth_app.api_base_url }" with userid "{ self.userid }" with OAuth token "{ self._oauth_token }".')
client = Mastodon(
Expand All @@ -138,54 +110,33 @@ def _ensure_oauth_token(self, oauth_client_id: str) -> None:
self._oauth_token = real_node._provision_oauth_token_for(self, oauth_client_id)


class WordPressNonExistingAccount(NonExistingAccount):
def __init__(self, role: str | None, userid: str):
super().__init__(role)
self.userid = userid


@staticmethod
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "non_existing_account" dict of TestPlanConstellationNode
"""
userid = USERID_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, context_msg)
role = ROLE_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, context_msg)
return WordPressNonExistingAccount(role, userid)


@property
def webfinger_uri(self):
return f'acct:{ self.userid }@{ self.node.hostname }'


@property
def actor_uri(self):
return f'https://{ self.node.hostname }/users/{ self.userid }'


class WordPressPlusPluginsNode(NodeWithMastodonAPI):
"""
A Node running WordPress with the ActivityPub plugin.
"""
# Python 3.12 @override -- implement WordPress scheme
def _actor_uri_to_userid(self, actor_uri: str) -> str:
if m:= re.match('^https://([^/]+)/author/([^/]+)/?$', actor_uri):
if m.group(1) == self._config.hostname:
return m.group(2)
raise ValueError( f'Cannot find actor at this node: { actor_uri }' )


def _provision_oauth_token_for(self, account: WordPressAccount, oauth_client_id: str) -> str:
ret = cast(str, prompt_user(f'Enter the OAuth token for the Mastodon API for user "{ account.userid }"'
+ f' on constellation role "{ self.rolename }", OAuth client id "{ oauth_client_id }" (user field "{ OAUTH_TOKEN_ACCOUNT_FIELD }"): ',
parse_validate=_oauth_token_validate))
return ret


# Python 3.12 @override
def _run_poor_mans_cron(self) -> None:
# Seems we need two HTTP GETs
url = f'https://{ self.hostname }/wp-cron.php?doing_wp_cron'
session = self._obtain_requests_session()

# There must be a better way. But this seems to do it. 15 might be enough. 10 might not.
for _ in range(20):
time.sleep(1)
trace('Triggering wp-cron at { url }')
session.get(url)


class WordPressPlusPluginsSaasNodeDriver(NodeDriver):
"""
Create a WordPress + ActivityPubPlugin Node that already runs as Saas
Create a WordPress+plugins Node that already runs as Saas
"""
# Python 3.12 @override
@staticmethod
Expand All @@ -207,13 +158,13 @@ def test_plan_node_non_existing_account_fields() -> list[TestPlanNodeNonExisting

# Python 3.12 @override
def create_configuration_account_manager(self, rolename: str, test_plan_node: TestPlanConstellationNode) -> tuple[NodeConfiguration, AccountManager | None]:
app = test_plan_node.parameter_or_raise(APP_PAR, { APP_PAR.name: 'WordPress + ActivityPub plugin' }) # Let user give a more descriptive name if they want to
app = test_plan_node.parameter_or_raise(APP_PAR, { APP_PAR.name: 'WordPress+plugins' }) # Let user give a more descriptive name if they want to
app_version = test_plan_node.parameter(APP_VERSION_PAR)
hostname = test_plan_node.parameter_or_raise(HOSTNAME_PAR)
verify_tls_certificate = test_plan_node.parameter_or_raise(VERIFY_API_TLS_CERTIFICATE_PAR, { VERIFY_API_TLS_CERTIFICATE_PAR.name: 'true' })

if not hostname:
hostname = prompt_user(f'Enter the hostname for the WordPress + ActivityPub plugin Node of constellation role "{ rolename }"'
hostname = prompt_user(f'Enter the hostname for the WordPress+plugins Node of constellation role "{ rolename }"'
+ f' (node parameter "{ HOSTNAME_PAR }"): ',
parse_validate=hostname_validate)

Expand All @@ -227,7 +178,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(WordPressNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down
8 changes: 4 additions & 4 deletions src/feditest/nodedrivers/wordpress/ubos.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
USERID_ACCOUNT_FIELD,
USERID_NON_EXISTING_ACCOUNT_FIELD,
WordPressAccount,
WordPressNonExistingAccount,
WordPressPlusPluginsNode
)
from feditest.protocols.fediverse import FediverseNonExistingAccount
from feditest.reporting import trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField

Expand All @@ -49,7 +49,7 @@ def set_node(self, node: Node) -> None:

class WordPressPlusPluginsUbosNode(WordPressPlusPluginsNode):
"""
A WordPress + plugins Node running on UBOS. This means we know how to interact with it exactly.
A WordPress+plugins Node running on UBOS. This means we know how to interact with it exactly.
"""
# Python 3.12 @override
def provision_account_for_role(self, role: str | None = None) -> Account | None:
Expand Down Expand Up @@ -135,7 +135,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(WordPressNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down Expand Up @@ -163,7 +163,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
}
},
defaults = {
'app' : 'WordPress + plugins'
'app' : 'WordPress+plugins'
}),
WordPressUbosAccountManager(accounts, non_existing_accounts)
)
Expand Down
18 changes: 9 additions & 9 deletions src/feditest/protocols/activitypub/diag.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,37 +144,37 @@ class ActivityPubDiagNode(WebDiagClient, WebDiagServer,ActivityPubNode):

# Work in progress

# def fetch_remote_actor_document(remote_actor_uri: str) -> Actor:
# def fetch_remote_actor_document(remote_actor_acct_uri: str) -> Actor:
# pass


# def set_inbox_uri_to(actor_uri: str, inbox_uri: str | None):
# def set_inbox_uri_to(actor_acct_uri: str, inbox_uri: str | None):
# pass


# def set_outbox_uri_to(actor_uri: str, outbox_uri: str | None):
# def set_outbox_uri_to(actor_acct_uri: str, outbox_uri: str | None):
# pass


# def add_to_followers_collection(actor_uri: str, to_be_added_actor_uri: str):
# def add_to_followers_collection(actor_acct_uri: str, to_be_added_actor_acct_uri: str):
# pass


# def add_to_following_collection(actor_uri: str, to_be_added_actor_uri: str):
# def add_to_following_collection(actor_acct_uri: str, to_be_added_actor_acct_uri: str):
# pass


# def add_to_outbox(actor_uri: str, to_be_added_activity: Activity):
# def add_to_outbox(actor_acct_uri: str, to_be_added_activity: Activity):
# pass


# def add_to_inbox(actor_uri: str, to_be_added_activity: Activity):
# def add_to_inbox(actor_acct_uri: str, to_be_added_activity: Activity):
# pass


# def read_inbox_of(actor_uri: str, inbox_collection: Collection):
# def read_inbox_of(actor_acct_uri: str, inbox_collection: Collection):
# pass


# def read_outbox_of(actor_uri: str, outbox_collection: Collection):
# def read_outbox_of(actor_acct_uri: str, outbox_collection: Collection):
# pass
Loading
Loading