activitypub-1.0.x-dev/tests/src/Functional/ActivityPubTestBase.php
tests/src/Functional/ActivityPubTestBase.php
<?php
namespace Drupal\Tests\activitypub\Functional;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
abstract class ActivityPubTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'activitypub',
'activitypub_test',
'webfinger',
'nodeinfo',
'page_cache',
'dblog',
'block',
'node',
'user',
'path',
'path_alias',
];
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The name for the first account.
*
* @var string
*/
protected $accountNameOne = 'NameOne';
/**
* The name for the second account.
*
* @var string
*/
protected $accountNameTwo = 'NameTwo';
/**
* The default theme to use.
*
* @var string
*/
protected $defaultTheme = 'starterkit_theme';
/**
* A user with all permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An authenticated user with less permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $authenticatedUserOne;
/**
* An authenticated user with less permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $authenticatedUserTwo;
/**
* An authenticated user with less permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $authenticatedUserThree;
/**
* The authenticated user permissions.
*
* @var array
*/
protected $authenticatedUserPermissions = [
'allow users to enable activitypub',
'administer nodes',
'bypass node access',
'access user profiles',
'publish to site-wide actor',
'administer url aliases',
];
/**
* Protected bundles.
*
* @var array
*/
protected $bundles = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->bundles = node_type_get_names();
/** @var \Drupal\user\RoleInterface $role */
$role = Role::load(RoleInterface::ANONYMOUS_ID);
$this->grantPermissions($role, ['access user profiles']);
$this->adminUser = $this->createUser([], 'administrator', TRUE);
$this->authenticatedUserOne = $this->createUser($this->authenticatedUserPermissions, 'fediverseOne');
$this->authenticatedUserTwo = $this->createUser($this->authenticatedUserPermissions, 'fediverseTwo');
$this->authenticatedUserThree = $this->createUser($this->authenticatedUserPermissions, 'nonFediverse');
// Set site-wide actor
$this->setSitewideActor();
$this->httpClient = $this->container->get('http_client_factory')
->fromOptions(['base_uri' => $this->baseUrl]);
}
/**
* Setup language.
*
* @param $language
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function setupLanguage($language) {
ConfigurableLanguage::createFromLangcode($language)->save();
\Drupal::configFactory()->getEditable('system.site')->set('default_langcode', $language)->save();
$this->rebuildContainer();
}
/**
* Helper method to activate ActivityPub for authenticated user One and Two.
*
* @param $assert_session
* @param bool $enable_second_account
*/
protected function enableActivityPub($assert_session, bool $enable_second_account = FALSE) {
$this->drupalLogin($this->authenticatedUserOne);
$edit = [
'activitypub_enable' => TRUE,
];
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub');
$assert_session->responseContains('ActivityPub is not enabled for your account.');
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/settings');
$this->submitForm($edit, 'Save');
$assert_session->responseContains('Please enter your ActivityPub username.');
$edit = [
'activitypub_enable' => TRUE,
'activitypub_name' => $this->accountNameOne,
];
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/settings');
$this->submitForm($edit, 'Save');
if ($enable_second_account) {
$this->drupalLogin($this->authenticatedUserTwo);
$edit = [
'activitypub_enable' => TRUE,
'activitypub_name' => $this->accountNameTwo,
];
$this->drupalGet('user/' . $this->authenticatedUserTwo->id() . '/activitypub/settings');
$this->submitForm($edit, 'Save');
}
}
/**
* Create a type config entity.
*
* @param string $activity
* @param string $bundle
* @param string $object
* @param array $mapping
* @param string $id
* @param string $plugin_id
* @param string $target_entity_type_id
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createType(string $activity, string $bundle, string $object, array $mapping = [], string $id = 'map', string $plugin_id = 'activitypub_dynamic_types', string $target_entity_type_id = 'node') {
if (!isset($this->bundles[$bundle])) {
$this->bundles[$bundle] = TRUE;
$this->drupalCreateContentType(['type' => $bundle]);
}
/** @var \Drupal\activitypub\Entity\ActivityPubTypeInterface $entity */
$values = [
'status' => TRUE,
'label' => $bundle . ' - ' . $object,
'id' => $id,
'plugin' => [
'id' => $plugin_id,
'configuration' => [
'activity' => $activity,
'object' => $object,
'target_bundle' => $bundle,
'target_entity_type_id' => $target_entity_type_id,
'field_mapping' => [
[
'field_name' => 'created',
'property' => 'published',
],
[
'field_name' => 'body',
'property' => 'content',
],
],
],
],
];
if (!empty($mapping)) {
foreach ($mapping as $field_name => $property) {
$values['plugin']['configuration']['field_mapping'][] = [
'field_name' => $field_name,
'property' => $property,
];
}
}
$entity = \Drupal::entityTypeManager()->getStorage('activitypub_type')->create($values);
$entity->save();
}
/**
* Enables an ActivityPub type.
*
* @param $type
* @param bool $status
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function setTypeStatus($type, bool $status = TRUE) {
/** @var \Drupal\activitypub\Entity\ActivityPubTypeInterface $entity */
$entity = \Drupal::entityTypeManager()->getStorage('activitypub_type')->load($type);
$entity->setStatus($status)->save();
}
/**
* Generate dummy Activity for a user id.
*
* @param $uid
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createActivity($uid) {
$values = [
'status' => TRUE,
'uid' => $uid,
'collection' => ActivityPubActivityInterface::INBOX,
'type' => 'Type',
'actor' => 'actor',
'object' => 'object',
];
$entity = \Drupal::entityTypeManager()->getStorage('activitypub_activity')->create($values);
$entity->save();
}
/**
* Get resource url.
*
* @param $account
* @param $addAcctUriScheme
*
* @return string.
*/
protected function getResourceUrl($account, $addAcctUriScheme = TRUE) {
$base_url = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
return ($addAcctUriScheme ? 'acct:' : '') . $account . '@' . str_replace(['http://', 'https://'], '', $base_url);
}
/**
* Get a payload.
*
* @param string $actor
* @param int $id
* @param string $content
*
* @return array
*/
protected function getPayload(string $actor = ACTIVITYPUB_TEST_USER, int $id = 1, string $content = 'My first message') {
return [
'type' => 'Create',
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.com/first-post/' . $id,
'actor' => $actor,
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'object' => [
'type' => 'Note',
'id' => 'https://example.com/first-post/' . $id,
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'attributedTo' => $actor,
'published' => '2020-03-12T09:27:03Z',
'content' => $content,
],
];
}
/**
* Follow a user.
*
* @param $uid
* @param $actor
* @param $object
* @param string $collection
* @param string $type
* @param null $entity_type_id
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function followUserProgrammatically($uid, $actor, $object, string $collection = ActivityPubActivityInterface::OUTBOX, string $type = 'Follow', $entity_type_id = NULL) {
/** @var \Drupal\activitypub\Entity\Storage\ActivityPubActorStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('activitypub_activity');
$follow_values = [
'status' => 1,
'uid' => $uid,
'actor' => $actor,
'object' => $object,
'type' => $type,
'collection' => $collection,
];
if ($entity_type_id) {
$follow_values['entity_id'] = $uid;
$follow_values['entity_type_id'] = $entity_type_id;
}
$f = $storage->create($follow_values);
$f->save();
}
/**
* Add link field.
*/
protected function drupalAddLinkField($type) {
$this->drupalLogin($this->adminUser);
$edit = ['new_storage_type' => 'link'];
$this->drupalGet('admin/structure/types/manage/' . $type . '/fields/add-field');
$this->submitForm($edit, 'Continue');
$edit = ['label' => 'Link field', 'field_name' => 'link'];
$this->submitForm($edit, 'Continue');
$this->submitForm([], 'Save settings');
$this->drupalLogout();
}
/**
* Send inbox request.
*
* @param $url
* @param $payload
*
* @return \Psr\Http\Message\ResponseInterface|null
*/
protected function sendInboxRequest($url, $payload) {
$headers = [
'accept' => 'application/activity+json',
];
try {
$response = $this->httpClient->post($url, ['json' => $payload, 'headers' => $headers]);
}
catch (RequestException | GuzzleException $e) {
$response = $e->getResponse();
}
return $response;
}
/**
* Runs the inbox queue.
*/
protected function runInboxQueue() {
if (\Drupal::config('activitypub.settings')->get('process_inbox_handler') == 'drush') {
\Drupal::service('activitypub.process.client')->handleInboxQueue();
}
}
/**
* Runs the complete outbox queue.
*/
protected function runOutboxQueue() {
if (\Drupal::config('activitypub.settings')->get('process_outbox_handler') == 'drush') {
\Drupal::service('activitypub.process.client')->prepareOutboxQueue();
\Drupal::service('activitypub.process.client')->handleOutboxQueue();
}
}
/**
* Check outbox.
*
* @param $outbox
* @param $page
* @param $assert_session
* @param $count
*/
protected function checkOutbox($outbox, $page, $assert_session, $count) {
$this->drupalGet($outbox);
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($count, $content->totalItems);
$this->drupalGet($content->first);
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($count, count($content->orderedItems));
}
/**
* Clear queue.
*
* @param $queue_name
*/
protected function clearQueue($queue_name) {
\Drupal::database()->delete('queue')->condition('name', $queue_name)->execute();
}
/**
* Get queued items.
*
* @param bool $dump
*
* @return array
*/
protected function getQueuedItems(bool $dump = FALSE) {
$items = [];
$records = \Drupal::database()->select('queue', 'q')->fields('q')->orderBy('name', 'DESC');
foreach ($records->execute() as $r) {
$items[] = $r;
if ($dump) {
activitypub_test_dump_debug(print_r($r, 1), 'queued-items');
}
}
return $items;
}
/**
* Dump watchdog helper.
*/
protected function dumpWatchdog() {
$messages = \Drupal::database()->select('watchdog', 'w')
->fields('w')
->orderBy('wid', 'DESC')
->execute()
->fetchAll();
foreach ($messages as $message) {
activitypub_test_dump_debug(print_r($message, 1), 'watchdog');
}
}
/**
* Dump activities helper.
*
* @param bool $tiny
* @param string $suffix
*/
protected function dumpActivities(bool $tiny = FALSE, string $suffix = '') {
try {
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity */
$dump = [];
$tiny_dump = [];
foreach (\Drupal::entityTypeManager()->getStorage('activitypub_activity')->loadMultiple() as $activity) {
if ($tiny) {
$properties = [];
$properties[] = 'aid:' . $activity->id();
$properties[] = 'uid: ' . $activity->getOwnerId();
$properties[] = $activity->getType();
$properties[] = $activity->getCollection();
$properties[] = $activity->getActor();
$properties[] = $activity->getObject();
$tiny_dump[] = implode("\n", $properties);
}
else {
$dump[] = print_r($activity->toArray(), 1) . "\n";
}
}
activitypub_test_dump_debug('---------------------------------------', 'activities' . $suffix);
activitypub_test_dump_debug(($tiny ? implode("\n", $tiny_dump) : implode("\n", $dump)), 'activities' . $suffix);
}
catch (\Exception $ignored) {}
}
/**
* Flush cache bins.
*
* I don't understand why the cache is not cleared in the test because it
* works fine manually.
*/
protected function flushBins() {
$module_handler = \Drupal::moduleHandler();
$module_handler->invokeAll('cache_flush');
foreach (Cache::getBins() as $cache_backend) {
$cache_backend->deleteAll();
}
}
/**
* Follow a user.
*
* @param $actor
* @param $object
* @param int $actor_nr
* @param bool $run_queue
*/
protected function followUser($actor, $object, $actor_nr = 1, $run_queue = FALSE) {
if ($actor_nr == 2) {
$this->drupalLogin($this->authenticatedUserTwo);
}
else {
$this->drupalLogin($this->authenticatedUserOne);
}
$edit = [
'config_id' => 'follow',
'actor' => $actor,
'object' => $object,
'type' => 'Follow',
'collection' => 'outbox',
'status' => 0,
];
$this->drupalGet('activitypub/add');
$this->submitForm($edit, 'Save');
$this->drupalLogout();
if ($run_queue) {
$this->runOutboxQueue();
}
}
/**
* View activities of user 1 and 2.
*/
protected function viewActivityOverview() {
$this->drupalLogin($this->authenticatedUserOne);
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub');
$this->drupalLogout();
$this->drupalLogin($this->authenticatedUserTwo);
$this->drupalGet('user/' . $this->authenticatedUserTwo->id() . '/activitypub');
$this->drupalLogout();
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/' . $this->accountNameOne . '/outbox', ['query' => ['page' => 0]]);
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/' . $this->accountNameOne . '/following', ['query' => ['page' => 0]]);
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/' . $this->accountNameOne . '/followers', ['query' => ['page' => 0]]);
$this->drupalGet('user/' . $this->authenticatedUserTwo->id() . '/activitypub/' . $this->accountNameTwo . '/following', ['query' => ['page' => 0]]);
$this->drupalGet('user/' . $this->authenticatedUserTwo->id() . '/activitypub/' . $this->accountNameTwo . '/followers', ['query' => ['page' => 0]]);
}
/**
* Set the outbox handler.
*/
protected function setOutboxHandler() {
$this->drupalLogin($this->adminUser);
$edit = [
'process_outbox_handler' => 'drush',
];
$this->drupalGet('admin/config/services/activitypub');
$this->submitForm($edit, 'Save configuration');
$this->drupalLogout();
}
/**
* Set the inbox handler.
*/
protected function setInboxHandler() {
$this->drupalLogin($this->adminUser);
$edit = [
'process_inbox_handler' => 'drush',
];
$this->drupalGet('admin/config/services/activitypub');
$this->submitForm($edit, 'Save configuration');
$this->drupalLogout();
}
/**
* Set the user id of the site-wide actor.
*/
protected function setSitewideActor()
{
$this->drupalLogin($this->adminUser);
$edit = [
'site_wide_uid' => $this->authenticatedUserOne->id(),
];
$this->drupalGet('admin/config/services/activitypub');
$this->submitForm($edit, 'Save configuration');
$this->drupalLogout();
}
/**
* Assert the number of items.
*
* @param $total
* @param $assert_session
* @param $page
*/
protected function assertOutboxItems($total, $assert_session, $page) {
$this->drupalGet('user/' . $this->authenticatedUserOne->id() . '/activitypub/' . $this->accountNameOne . '/outbox', ['query' => ['page' => 0]]);
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($total, $content->totalItems);
self::assertEquals($total, count($content->orderedItems));
}
/**
* Assert followers and following.
*
* @param $id
* @param $name
* @param $followers
* @param $following
* @param $followers_urls
* @param $following_urls
* @param $assert_session
* @param $page
*/
protected function assertFollowersAndFollowing($id, $name, $followers, $following, $followers_urls, $following_urls, $assert_session, $page) {
$this->drupalGet('user/' . $id . '/activitypub/' . $name . '/followers');
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($followers, $content->totalItems);
$this->drupalGet('user/' . $id . '/activitypub/' . $name . '/followers', ['query' => ['page' => 0]]);
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($followers, $content->totalItems);
self::assertEquals($followers, count($content->orderedItems));
if (is_array($followers_urls)) {
foreach ($followers_urls as $u) {
self::assertTrue(in_array($u, $content->orderedItems));
}
}
elseif (!empty($followers_urls)) {
self::assertTrue(in_array($followers_urls, $content->orderedItems));
}
$this->drupalGet('user/' . $id . '/activitypub/' . $name . '/following');
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($following, $content->totalItems);
$this->drupalGet('user/' . $id . '/activitypub/' . $name . '/following', ['query' => ['page' => 0]]);
$assert_session->statusCodeEquals(200);
$content = json_decode($page->getContent());
self::assertEquals($following, $content->totalItems);
self::assertEquals($following, count($content->orderedItems));
if (is_array($following_urls)) {
foreach ($following_urls as $u) {
self::assertTrue(in_array($u, $content->orderedItems));
}
}
elseif (!empty($following_urls)) {
self::assertTrue(in_array($following_urls, $content->orderedItems));
}
}
}
