...
 
Commits (2)
......@@ -5,6 +5,7 @@ namespace Minds\Controllers\Cli;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Cli;
use Minds\Core\Reports\Summons\Summon;
use Minds\Interfaces;
use Minds\Entities;
......@@ -47,9 +48,16 @@ class Moderation extends Cli\Controller implements Interfaces\CliControllerInter
$userId = $this->getOpt('user');
$reportUrn = $this->getOpt('report');
$juryType = $this->getOpt('jury-type') ?? null;
$respond = $this->getOpt('respond') ?? null;
if (!$userId || !$reportUrn) {
$this->out('Usage: cli.php moderation summon --user=<username_or_guid> --report=<report_urn>');
$this->out([
'Usage:',
'- Summoning: cli.php moderation summon --user=<username_or_guid> --report=<report_urn>',
'- Responding: cli.php moderation summon --user=<username_or_guid> --report=<report_urn> --jury-type=<initial_jury|appeal_jury> --respond=<accepted|declined>',
]);
exit(1);
}
......@@ -60,6 +68,54 @@ class Moderation extends Cli\Controller implements Interfaces\CliControllerInter
exit(1);
}
if (!$respond) {
$report = $reportsRepository->get($reportUrn);
if (!$report) {
$this->out('Error: Invalid report');
exit(1);
}
$appeal = new Core\Reports\Appeals\Appeal();
$appeal->setReport($report);
$summonsManager->summon($appeal, [ $user->guid ]);
$this->out("Summoned {$user->guid} to {$reportUrn}");
} else {
$summon = new Summon();
$summon
->setReportUrn($reportUrn)
->setJuryType($juryType)
->setJurorGuid((string) $user->guid)
->setStatus($respond);
$summonsManager->respond($summon);
$this->out("Responded to {$user->guid}'s summon to {$reportUrn} with {$respond}");
}
}
public function dev_only_simulate_summon()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
/** @var Core\Reports\Repository $reportsRepository */
$reportsRepository = Di::_()->get('Reports\Repository');
/** @var Core\Reports\Summons\Manager $summonsManager */
$summonsManager = Di::_()->get('Moderation\Summons\Manager');
$reportUrn = $this->getOpt('report');
if (!$reportUrn) {
$this->out([
'Usage: cli.php moderation dev_only_simulate_summon --report=<report_urn>',
]);
exit(1);
}
$report = $reportsRepository->get($reportUrn);
if (!$report) {
......@@ -68,8 +124,12 @@ class Moderation extends Cli\Controller implements Interfaces\CliControllerInter
}
$appeal = new Core\Reports\Appeals\Appeal();
$appeal->setReport($report);
$appeal
->setReport($report)
->setOwnerGuid($report->getEntityOwnerGuid());
$cohort = $summonsManager->summon($appeal, null);
$summonsManager->summon($appeal, [ $user->guid ]);
var_dump($cohort);
}
}
......@@ -53,6 +53,8 @@ class summons implements Interfaces\Api
->setJuryType($juryType)
->setJurorGuid((string) $userGuid)
->setStatus($status);
$summonsManager->respond($summon);
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
......@@ -60,15 +62,6 @@ class summons implements Interfaces\Api
]);
}
if (!$summonsManager->isSummoned($summon)) {
return Factory::response([
'status' => 'error',
'message' => 'You\'re not summoned',
]);
}
$summonsManager->respond($summon);
$response = [
'summon' => $summon->getStatus(),
'expires_in' => $summon->getTtl(),
......
......@@ -12,6 +12,7 @@ use Minds\Core\Queue\Message;
use Minds\Core\Queue\Client;
use Minds\Core\Queue\Interfaces\QueueClient;
use Minds\Core\Queue\Interfaces\QueueRunner;
use Minds\Core\Reports\Appeals\Appeal;
use Minds\Core\Reports\Summons\Manager;
class ReportsAppealSummon implements QueueRunner
......@@ -31,7 +32,10 @@ class ReportsAppealSummon implements QueueRunner
->receive(function (Message $data) {
$params = $data->getData();
/** @var Appeal $appeal */
$appeal = $params['appeal'] ?? null;
/** @var string[] $cohort */
$cohort = $params['cohort'] ?? null;
if (!$appeal) {
......@@ -39,9 +43,18 @@ class ReportsAppealSummon implements QueueRunner
return;
}
echo "Summoning for {$appeal->getReport()->getUrn()}..." . PHP_EOL;
/** @var Manager $manager */
$manager = Di::_()->get('Moderation\Summons\Manager');
$manager->summon($appeal, $cohort);
$missing = $manager->summon($appeal, $cohort);
if ($missing > 0) {
echo "Missing {$missing} juror(s). Deferring..." . PHP_EOL;
$manager->defer($appeal);
}
echo "Done!" . PHP_EOL;
});
}
}
......@@ -7,149 +7,91 @@
namespace Minds\Core\Reports\Summons;
use Minds\Core\Data\ElasticSearch\Client as ElasticsearchClient;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Helpers\Text;
use Exception;
class Cohort
{
/** @var ElasticsearchClient */
protected $elasticsearch;
/** @var Repository */
protected $repository;
/** @var string */
protected $index;
/** @var Pool */
protected $pool;
/**
* Repository constructor.
* @param ElasticsearchClient $elasticsearch
* @param string $index
* Cohort constructor.
* @param Repository $repository
* @param Pool $pool
*/
public function __construct(
$elasticsearch = null,
$index = null
$repository = null,
$pool = null
)
{
$this->elasticsearch = $elasticsearch ?: Di::_()->get('Database\ElasticSearch');
$this->index = $index ?: 'minds-metrics-*';
$this->repository = $repository ?: new Repository();
$this->pool = $pool ?: new Pool();
}
/**
* @param array $opts
* @return \Generator
* @yields string
* @return string[]
* @throws Exception
*/
public function getList(array $opts = [])
public function pick($opts)
{
$opts = array_merge([
'size' => 0,
'for' => null,
'active_threshold' => 0,
'platform' => null,
'validated' => false,
'limit' => 10,
'offset' => 0,
'except' => [],
'active_threshold' => null,
], $opts);
$now = (int) (microtime(true) * 1000);
$fromTimestamp = $now - ($opts['active_threshold'] * 1000);
$body = [
'_source' => [
'user_guid',
],
'query' => [
'bool' => [
'must' => [
[
'range' => [
'@timestamp' => [
'gte' => $fromTimestamp,
],
],
],
[
'term' => [
'type' => 'action',
],
],
],
],
],
'aggs' => [
'entities' => [
'terms' => [
'field' => 'user_guid.keyword',
'size' => $opts['limit'],
],
],
],
'size' => 0,
];
if ($opts['platform']) {
$body['query']['bool']['must'][] = [
'terms' => [
'platform' => Text::buildArray($opts['platform']),
],
];
}
if ($opts['for']) {
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
}
$cohort = [];
$body['query']['bool']['must_not'][] = [
'term' => [
'user_guid' => (string) $opts['for'],
],
];
$body['query']['bool']['must_not'][] = [
'terms' => [
'user_guid' => [
'index' => 'minds-graph',
'type' => 'subscriptions',
'id' => (string) $opts['for'],
'path' => 'guids',
],
],
];
}
// Uncomment below to scale
// $poolSize = $opts['size'] * 5;
// $max_pages = 20; // NOTE: Normally capped to 20.
if ($opts['validated']) {
$body['query']['bool']['must'][] = [
'exists' => [
'field' => 'user_phone_number_hash',
],
];
$poolSize = 400;
$max_pages = 1; // NOTE: Normally capped to 20.
$page = 0;
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
while (true) {
if ($page > $max_pages) {
// Max = PoolSize * MaxPages
error_log('Cannot gather a cohort');
break;
}
$body['query']['bool']['must_not'][] = [
'term' => [
'user_phone_number_hash' => '',
],
];
}
$query = [
'index' => $this->index,
'type' => 'action',
'body' => $body,
'size' => $opts['limit'],
'from' => $opts['offset'],
];
$prepared = new Search();
$prepared->query($query);
$pool = $this->pool->getList([
'active_threshold' => $opts['active_threshold'],
'platform' => 'browser',
'for' => $opts['for'],
'except' => $opts['except'],
'validated' => false,
'size' => $poolSize,
'page' => $page,
'max_pages' => $max_pages,
]);
$j = 0;
foreach ($pool as $userGuid) {
$j++;
// TODO: Check subs
$cohort[] = $userGuid;
if (count($cohort) >= $opts['size']) {
break;
}
}
$result = $this->elasticsearch->request($prepared);
if ($j === 0 || count($cohort) >= $opts['size']) {
break;
}
foreach ($result['aggregations']['entities']['buckets'] as $bucket) {
yield $bucket['key'];
$page++;
}
return $cohort;
}
}
......@@ -7,6 +7,7 @@
namespace Minds\Core\Reports\Summons\Delegates;
use Exception;
use Minds\Core\Reports\Summons\Summon;
use Minds\Core\Sockets\Events as SocketEvents;
......@@ -28,7 +29,7 @@ class SocketDelegate
/**
* @param Summon $summon
* @throws \Exception
* @throws Exception
*/
public function onSummon(Summon $summon)
{
......
......@@ -7,6 +7,10 @@
namespace Minds\Core\Reports\Summons;
use Exception;
use Minds\Core\Queue\Client as QueueClient;
use Minds\Core\Queue\Client;
use Minds\Core\Queue\Runners\ReportsAppealSummon;
use Minds\Core\Reports\Appeals\Appeal;
use Minds\Core\Reports\Summons\Delegates;
......@@ -15,9 +19,12 @@ class Manager
/** @var Cohort $cohort */
protected $cohort;
/** @vat Repository $repository */
/** @var Repository $repository */
protected $repository;
/** @var QueueClient */
protected $queueClient;
/** @var Delegates\SocketDelegate $socketDelegate */
protected $socketDelegate;
......@@ -25,39 +32,75 @@ class Manager
* Manager constructor.
* @param Cohort $cohort
* @param Repository $repository
* @param QueueClient $queueClient
* @param Delegates\SocketDelegate $socketDelegate
* @throws Exception
*/
public function __construct(
$cohort = null,
$repository = null,
$queueClient = null,
$socketDelegate = null
)
{
$this->cohort = $cohort ?: new Cohort();
$this->repository = $repository ?: new Repository();
$this->queueClient = $queueClient ?: Client::build();
$this->socketDelegate = $socketDelegate ?: new Delegates\SocketDelegate();
}
/**
* @param Appeal $appeal
* @param array $cohort
* @throws \Exception
* @return int
* @throws Exception
*/
public function summon(Appeal $appeal, $cohort = null)
{
$cohort = $cohort ?: $this->cohort->getList([
'active_threshold' => 5 * 60,
'platform' => 'browser',
'for' => $appeal->getOwnerGuid(),
'validated' => true,
'limit' => 12,
]);
$reportUrn = $appeal->getReport()->getUrn();
$juryType = 'appeal_jury';
$missing = 0;
if (!$cohort) {
$jury = iterator_to_array($this->repository->getList([
'report_urn' => $reportUrn,
'jury_type' => $juryType,
]));
// Check how many are missing
$notDeclined = array_filter($jury, function (Summon $summon) {
return $summon->isAccepted() || $summon->isAwaiting();
});
$missing = 12 - count($notDeclined);
// If we have a full jury, don't summon
if ($missing <= 0) {
return 0;
}
// Reduce jury to juror guids and try to pick up to missing size
$juryGuids = array_map(function (Summon $summon) {
return (string) $summon->getJurorGuid();
}, $jury);
$cohort = $this->cohort->pick([
'size' => $missing,
'for' => $appeal->getOwnerGuid(),
'except' => $juryGuids,
'active_threshold' => 5 * 60,
]);
}
foreach ($cohort as $juror) {
$summon = new Summon();
$summon
->setReportUrn($appeal->getReport()->getUrn())
->setJuryType('appeal_jury')
->setReportUrn($reportUrn)
->setJuryType($juryType)
->setJurorGuid($juror)
->setTtl(120)
->setStatus('awaiting');
......@@ -65,6 +108,8 @@ class Manager
$this->repository->add($summon);
$this->socketDelegate->onSummon($summon);
}
return $missing;
}
/**
......@@ -79,9 +124,14 @@ class Manager
/**
* @param Summon $summon
* @return Summon
* @throws Exception
*/
public function respond(Summon $summon)
{
if (!$this->isSummoned($summon)) {
throw new Exception('User is not summoned');
}
$summon
->setTtl(10 * 60);
......@@ -89,4 +139,30 @@ class Manager
return $summon;
}
/**
* @param string $reportUrn
* @param string $juryType
* @return bool
* @throws Exception
*/
public function release($reportUrn, $juryType)
{
return $this->repository->deleteAll([
'report_urn' => $reportUrn,
'jury_type' => $juryType,
]);
}
/**
* @param Appeal $appeal
*/
public function defer(Appeal $appeal)
{
$this->queueClient
->setQueue(ReportsAppealSummon::class)
->send([
'appeal' => $appeal,
], 600);
}
}
<?php
/**
* User Pool for Cohort
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons;
use Generator;
use Minds\Core\Data\ElasticSearch\Client as ElasticsearchClient;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Helpers\Text;
class Pool
{
/** @var ElasticsearchClient */
protected $elasticsearch;
/** @var string */
protected $index;
/**
* Repository constructor.
* @param ElasticsearchClient $elasticsearch
* @param string $index
*/
public function __construct(
$elasticsearch = null,
$index = null
)
{
$this->elasticsearch = $elasticsearch ?: Di::_()->get('Database\ElasticSearch');
$this->index = $index ?: 'minds-metrics-*';
}
/**
* @param array $opts
* @return Generator
* @yields string
*/
public function getList(array $opts = [])
{
$opts = array_merge([
'for' => null,
'active_threshold' => 0,
'platform' => null,
'validated' => false,
'size' => 10,
'page' => 0,
'max_pages' => 20,
], $opts);
$now = (int) (microtime(true) * 1000);
$fromTimestamp = $now - ($opts['active_threshold'] * 1000);
$body = [
'_source' => [
'user_guid',
],
'query' => [
'bool' => [
'must' => [
[
'range' => [
'@timestamp' => [
'gte' => $fromTimestamp,
],
],
],
[
'term' => [
'type' => 'action',
],
],
],
],
],
'aggs' => [
'entities' => [
'terms' => [
'field' => 'user_guid.keyword',
'size' => $opts['size'],
'include' => [
'partition' => $opts['page'],
'num_partitions' => $opts['max_pages'],
],
],
],
],
'size' => 0,
];
if ($opts['platform']) {
$body['query']['bool']['must'][] = [
'terms' => [
'platform' => Text::buildArray($opts['platform']),
],
];
}
if ($opts['for']) {
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
}
$body['query']['bool']['must_not'][] = [
'term' => [
'user_guid' => (string) $opts['for'],
],
];
$body['query']['bool']['must_not'][] = [
'terms' => [
'user_guid' => [
'index' => 'minds-graph',
'type' => 'subscriptions',
'id' => (string) $opts['for'],
'path' => 'guids',
],
],
];
}
if ($opts['except']) {
$body['query']['bool']['must_not'][] = [
'terms' => [
'user_guid' => $opts['except'],
],
];
}
if ($opts['validated']) {
$body['query']['bool']['must'][] = [
'exists' => [
'field' => 'user_phone_number_hash',
],
];
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
}
$body['query']['bool']['must_not'][] = [
'term' => [
'user_phone_number_hash' => '',
],
];
}
$query = [
'index' => $this->index,
'type' => 'action',
'body' => $body,
];
$prepared = new Search();
$prepared->query($query);
$result = $this->elasticsearch->request($prepared);
foreach ($result['aggregations']['entities']['buckets'] as $bucket) {
yield $bucket['key'];
}
}
}
......@@ -8,6 +8,8 @@
namespace Minds\Core\Reports\Summons;
use Cassandra\Bigint;
use Exception;
use Generator;
use Minds\Core\Data\Cassandra\Client as CassandraClient;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
......@@ -30,11 +32,70 @@ class Repository
/**
* @param array $opts
* @throws \NotImplementedException
* @return Generator
* @yields array
* @throws Exception
*/
public function getList(array $opts = [])
{
throw new \NotImplementedException();
$opts = array_merge([
'report_urn' => null,
'jury_type' => null,
'juror_guid' => null,
'limit' => 10000,
'offset' => null,
], $opts);
if (!$opts['report_urn']) {
throw new Exception('Invalid Report URN');
}
if (!$opts['jury_type']) {
throw new Exception('Invalid Jury type');
}
$cql = "SELECT * FROM moderation_summons WHERE report_urn = ? AND jury_type = ?";
$values = [
$opts['report_urn'],
$opts['jury_type'],
];
$cqlOpts = [];
if ($opts['juror_guid']) {
$cql .= " AND juror_guid = ?";
$values[] = new Bigint($opts['juror_guid']);
}
if ($opts['offset']) {
$cqlOpts['paging_state_token'] = base64_decode($opts['offset']);
}
if ($opts['limit']) {
$cqlOpts['page_size'] = (int) $opts['limit'];
}
$prepared = new Custom();
$prepared->query($cql, $values);
$prepared->setOpts($cqlOpts);
try {
$rows = $this->db->request($prepared);
foreach ($rows as $row) {
$summon = new Summon();
$summon
->setReportUrn($row['report_urn'])
->setJuryType($row['jury_type'])
->setJurorGuid($row['juror_guid']->toInt())
->setStatus($row['status'])
->setTtl($row['expires'] - time());
yield $summon;
}
} catch (Exception $e) {
error_log($e);
return [];
}
}
/**
......@@ -60,7 +121,7 @@ class Repository
try {
return (bool) $this->db->request($prepared, true);
} catch (\Exception $e) {
} catch (Exception $e) {
error_log($e);
return false;
}
......@@ -86,7 +147,7 @@ class Repository
$response = $this->db->request($prepared);
return $response[0]['total'] > 0;
} catch (\Exception $e) {
} catch (Exception $e) {
error_log($e);
return false;
}
......@@ -94,10 +155,59 @@ class Repository
/**
* @param Summon $summon
* @throws \NotImplementedException
* @return bool
* @throws Exception
*/
public function delete(Summon $summon)
{
throw new \NotImplementedException();
return $this->deleteAll([
'report_urn' => $summon->getReportUrn(),
'jury_type' => $summon->getJuryType(),
'juror_guid' => $summon->getJurorGuid(),
]);
}
/**
* @param array $opts
* @return bool
* @throws Exception
* @yields array
*/
public function deleteAll(array $opts = [])
{
$opts = array_merge([
'report_urn' => null,
'jury_type' => null,
'juror_guid' => null,
], $opts);
if (!$opts['report_urn']) {
throw new Exception('Invalid Report URN');
}
if (!$opts['jury_type']) {
throw new Exception('Invalid Jury type');
}
$cql = "DELETE FROM moderation_summons WHERE report_urn = ? AND jury_type = ?";
$values = [
$opts['report_urn'],
$opts['jury_type'],
];
if ($opts['juror_guid']) {
$cql .= " AND juror_guid = ?";
$values[] = new Bigint($opts['juror_guid']);
}
$prepared = new Custom();
$prepared->query($cql, $values);
try {
return (bool) $this->db->request($prepared, true);
} catch (Exception $e) {
error_log($e);
return false;
}
}
}
......@@ -7,6 +7,8 @@
namespace Minds\Core\Reports\Summons;
use Exception;
use JsonSerializable;
use Minds\Traits\MagicAttributes;
/**
......@@ -22,7 +24,7 @@ use Minds\Traits\MagicAttributes;
* @method int getTtl()
* @method Summon setTtl(int $ttl)
*/
class Summon implements \JsonSerializable
class Summon implements JsonSerializable
{
use MagicAttributes;
......@@ -44,12 +46,12 @@ class Summon implements \JsonSerializable
/**
* @param string $status
* @return $this
* @throws \Exception
* @throws Exception
*/
public function setStatus($status)
{
if (!in_array($status, ['awaiting', 'accepted', 'declined'])) {
throw new \Exception('Invalid status');
throw new Exception('Invalid status');
}
$this->status = $status;
......
<?php
/**
* ReleaseSummonsesDelegate
*
* @author edgebal
*/
namespace Minds\Core\Reports\Verdict\Delegates;
use Minds\Core\Di\Di;
use Minds\Core\Reports\Summons\Manager;
use Minds\Core\Reports\Verdict\Verdict;
class ReleaseSummonsesDelegate
{
/** @var Manager */
protected $summonsManager;
/**
* ReleaseSummonsesDelegate constructor.
* @param Manager $summonsManager
*/
public function __construct(
$summonsManager = null
)
{
$this->summonsManager = $summonsManager ?: Di::_()->get('Moderation\Summons\Manager');
}
/**
* @param Verdict $verdict
* @throws \Exception
*/
public function onCast(Verdict $verdict)
{
$juryType = $verdict->isAppeal() ? 'appeal_jury' : 'initial_jury';
$this->summonsManager->release(
$verdict->getReport()->getUrn(),
$juryType
);
}
}
......@@ -32,17 +32,22 @@ class Manager
/** @var Delegates\NotificationDelegate $notificationDelegate */
private $notificationDelegate;
/** @var Delegates\ReleaseSummonsesDelegate $releaseSummonsesDelegate */
private $releaseSummonsesDelegate;
public function __construct(
$repository = null,
$actionDelegate = null,
$reverseActionDelegate = null,
$notificationDelegate = null
$notificationDelegate = null,
$releaseSummonsesDelegate = null
)
{
$this->repository = $repository ?: new Repository;
$this->actionDelegate = $actionDelegate ?: new Delegates\ActionDelegate;
$this->reverseActionDelegate = $reverseActionDelegate ?: new Delegates\ReverseActionDelegate;
$this->notificationDelegate = $notificationDelegate ?: new Delegates\NotificationDelegate;
$this->releaseSummonsesDelegate = $releaseSummonsesDelegate ?: new Delegates\ReleaseSummonsesDelegate;
}
/**
......@@ -119,6 +124,7 @@ class Manager
* Cast a verdict
* @param Verdict $verdict
* @return boolean
* @throws \Exception
*/
public function cast(Verdict $verdict)
{
......@@ -133,6 +139,9 @@ class Manager
// Send a notification to the reported user
$this->notificationDelegate->onAction($verdict);
// Release summonses
$this->releaseSummonsesDelegate->onCast($verdict);
// Send rewards to reporters
return $added;
......
......@@ -4,16 +4,16 @@
*/
namespace Minds\Core\Reports\Verdict;
use Minds\Core\Reports\Jury\Decision;
use Minds\Core\Reports\Report;
use Minds\Traits\MagicAttributes;
/**
* @method Report getReport(): Report
* @method Report getDecisions(): array<Decision>
* @method Report isAppeal(): boolean
* @method Report isUphold(): boolean
* @method Report getAction(): string
* @method Report getInitialJuryAction(): string
* @method Report getTimestamp: int
* @method Report getReport()
* @method boolean isUphold()
* @method string getAction()
* @method string getInitialJuryAction()
* @method int getTimestamp()
*/
class Verdict
{
......@@ -36,7 +36,7 @@ class Verdict
/**
* Decisions
* @return array<Decision>
* @return Decision[]
*/
public function getDecisions()
{
......
......@@ -18,19 +18,22 @@ class ManagerSpec extends ObjectBehavior
private $actionDelegate;
private $reverseDelegate;
private $notificationDelegate;
private $releaseSummonsesDelegate;
function let(
Repository $repository,
Delegates\ActionDelegate $actionDelegate,
Delegates\ReverseActionDelegate $reverseDelegate,
Delegates\NotificationDelegate $notificationDelegate
Delegates\NotificationDelegate $notificationDelegate,
Delegates\ReleaseSummonsesDelegate $releaseSummonsesDelegate
)
{
$this->beConstructedWith($repository, $actionDelegate, $reverseDelegate, $notificationDelegate);
$this->beConstructedWith($repository, $actionDelegate, $reverseDelegate, $notificationDelegate, $releaseSummonsesDelegate);
$this->repository = $repository;
$this->actionDelegate = $actionDelegate;
$this->reverseDelegate = $reverseDelegate;
$this->notificationDelegate = $notificationDelegate;
$this->releaseSummonsesDelegate = $releaseSummonsesDelegate;
}
......@@ -60,6 +63,9 @@ class ManagerSpec extends ObjectBehavior
$this->notificationDelegate->onAction($verdict)
->shouldBeCalled();
$this->releaseSummonsesDelegate->onCast($verdict)
->shouldBeCalled();
$this->cast($verdict->getWrappedObject());
}
......