diff --git a/appinfo/routes.php b/appinfo/routes.php index 9ffa71655..63d98159f 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -90,6 +90,8 @@ return [ ['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'], ['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'], + ['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST', 'requirements' => ['apiVersion' => '1.1']], + ['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'], ['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'], diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index ac1ebaab2..2d0cd0678 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -23,40 +23,28 @@ namespace OCA\Deck\Command; -use JsonSchema\Constraints\Constraint; -use JsonSchema\Validator; -use OCA\Deck\Command\ImportHelper\AImport; -use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\BoardImportCommandService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; class BoardImport extends Command { - /** @var string */ - private $system; - private $allowedSystems; - /** @var TrelloHelper */ - private $trelloHelper; + /** @var boardImportCommandService */ + private $boardImportCommandService; public function __construct( - TrelloHelper $trelloHelper + BoardImportCommandService $boardImportCommandService ) { + $this->boardImportCommandService = $boardImportCommandService; parent::__construct(); - $this->trelloHelper = $trelloHelper; } /** * @return void */ protected function configure() { - $allowedSystems = glob(__DIR__ . '/ImportHelper/*Helper.php'); - $this->allowedSystems = array_map(function ($name) { - preg_match('/\/(?\w+)Helper\.php$/', $name, $matches); - return lcfirst($matches['system']); - }, $allowedSystems); + $allowedSystems = $this->boardImportCommandService->getAllowedImportSystems(); $this ->setName('deck:import') ->setDescription('Import data') @@ -64,7 +52,7 @@ class BoardImport extends Command { 'system', null, InputOption::VALUE_REQUIRED, - 'Source system for import. Available options: ' . implode(', ', $this->allowedSystems) . '.', + 'Source system for import. Available options: ' . implode(', ', $allowedSystems) . '.', 'trello' ) ->addOption( @@ -90,89 +78,10 @@ class BoardImport extends Command { * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { - $this->validateSystem($input, $output); - $this->validateConfig($input, $output); - $this->getSystemHelper() - ->validate($input, $output); - } - - protected function validateConfig(InputInterface $input, OutputInterface $output): void { - $configFile = $input->getOption('config'); - if (!is_file($configFile)) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid config json file: ', - 'config.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'config file not found' - ); - } - return $answer; - }); - $configFile = $helper->ask($input, $output, $question); - $input->setOption('config', $configFile); - } - - $config = json_decode(file_get_contents($configFile)); - $schemaPath = __DIR__ . '/ImportHelper/fixtures/config-' . $this->getSystem() . '-schema.json'; - $validator = new Validator(); - $validator->validate( - $config, - (object)['$ref' => 'file://' . realpath($schemaPath)], - Constraint::CHECK_MODE_APPLY_DEFAULTS - ); - if (!$validator->isValid()) { - $output->writeln('Invalid config file'); - $output->writeln(array_map(function ($v) { - return $v['message']; - }, $validator->getErrors())); - $output->writeln('Valid schema:'); - $output->writeln(print_r(file_get_contents($schemaPath), true)); - $input->setOption('config', null); - $this->validateConfig($input, $output); - } - $this->getSystemHelper()->setConfigInstance($config); - } - - private function setSystem(string $system): void { - $this->system = $system; - } - - public function getSystem() { - return $this->system; - } - - /** - * @return AImport - */ - private function getSystemHelper() { - $helper = $this->{$this->system . 'Helper'}; - $helper->setCommand($this); - return $helper; - } - - /** - * @return void - */ - private function validateSystem(InputInterface $input, OutputInterface $output) { - $system = $input->getOption('system'); - if (in_array($system, $this->allowedSystems)) { - $this->setSystem($system); - return; - } - $helper = $this->getHelper('question'); - $question = new ChoiceQuestion( - 'Please inform a source system', - $this->allowedSystems, - 0 - ); - $question->setErrorMessage('System %s is invalid.'); - $system = $helper->ask($input, $output, $question); - $input->setOption('system', $system); - $this->setSystem($system); + $this->boardImportCommandService + ->setInput($input) + ->setOutput($output) + ->validate(); } /** @@ -182,8 +91,11 @@ class BoardImport extends Command { * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->getSystemHelper() - ->import($input, $output); + $this + ->boardImportCommandService + ->setInput($input) + ->setOutput($output) + ->import(); $output->writeln('Done!'); return 0; } diff --git a/lib/Command/ImportHelper/AImport.php b/lib/Command/ImportHelper/AImport.php deleted file mode 100644 index c7ba56cce..000000000 --- a/lib/Command/ImportHelper/AImport.php +++ /dev/null @@ -1,75 +0,0 @@ -trelloImportService = $trelloImportService; - } - - abstract public function validate(InputInterface $input, OutputInterface $output): void; - - abstract public function import(InputInterface $input, OutputInterface $output): void; - - /** - * Define Command instance - * - * @param Command $command - * @return void - */ - public function setCommand(Command $command): void { - $this->command = $command; - } - - /** - * @return BoardImport - */ - public function getCommand() { - return $this->command; - } - - public function setConfigInstance(\stdClass $config) { - $this->trelloImportService->setConfigInstance($config); - } - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void { - $this->trelloImportService->setConfig($configName, $value); - } - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig(string $configName = null) { - return $this->trelloImportService->getConfig($configName); - } -} diff --git a/lib/Command/ImportHelper/ImportInterface.php b/lib/Command/ImportHelper/ImportInterface.php deleted file mode 100644 index f5d68794c..000000000 --- a/lib/Command/ImportHelper/ImportInterface.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Command\ImportHelper; - -use OCA\Deck\Command\BoardImport; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -interface ImportInterface { - /** - * Validate data before run execute method - * - * @param InputInterface $input - * @param OutputInterface $output - * @return void - */ - public function validate(InputInterface $input, OutputInterface $output): void; - - /** - * Run import - * - * @param InputInterface $input - * @param OutputInterface $output - * @return void - */ - public function import(InputInterface $input, OutputInterface $output): void; - - /** - * Define Command instance - * - * @param Command $command - * @return void - */ - public function setCommand(Command $command): void; - - /** - * @return BoardImport - */ - public function getCommand(); - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void; - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig($configName); -} diff --git a/lib/Command/ImportHelper/TrelloHelper.php b/lib/Command/ImportHelper/TrelloHelper.php deleted file mode 100644 index 448f8e629..000000000 --- a/lib/Command/ImportHelper/TrelloHelper.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Command\ImportHelper; - -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; - -class TrelloHelper extends AImport { - public function validate(InputInterface $input, OutputInterface $output): void { - $this->validateData($input, $output); - $this->trelloImportService->validateOwner(); - $this->trelloImportService->validateUsers(); - } - - public function import(InputInterface $input, OutputInterface $output): void { - $this->trelloImportService->setUserId(); - $output->writeln('Importing board...'); - $this->trelloImportService->importBoard(); - $output->writeln('Assign users to board...'); - $this->trelloImportService->assignUsersToBoard(); - $output->writeln('Importing labels...'); - $this->trelloImportService->importLabels(); - $output->writeln('Importing stacks...'); - $this->trelloImportService->importStacks(); - $output->writeln('Importing cards...'); - $this->trelloImportService->importCards(); - } - - private function validateData(InputInterface $input, OutputInterface $output): void { - $filename = $input->getOption('data'); - if (!is_file($filename)) { - $helper = $this->getCommand()->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($input, $output, $question); - $input->setOption('data', $data); - } - $this->trelloImportService->setData(json_decode(file_get_contents($filename))); - if (!$this->trelloImportService->getData()) { - $output->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($input, $output); - } - } -} diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php new file mode 100644 index 000000000..3545a34ab --- /dev/null +++ b/lib/Controller/BoardImportApiController.php @@ -0,0 +1,50 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Controller; + +use OCA\Deck\Service\BoardImportService; +use OCA\Files\Controller\ApiController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; + +class BoardImportApiController extends ApiController { + /** @var BoardImportService */ + private $boardImportService; + + public function __construct( + BoardImportService $boardImportService + ) { + $this->boardImportService = $boardImportService; + } + + /** + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + */ + public function import($system, $config, $data) { + $board = $this->boardImportService->import($system, $config, $data); + return new DataResponse($board, Http::STATUS_OK); + } +} diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php new file mode 100644 index 000000000..6bdd4b1f3 --- /dev/null +++ b/lib/Service/ABoardImportService.php @@ -0,0 +1,53 @@ +boardImportService = $service; + return $this; + } + + public function getImportService(): BoardImportService { + return $this->boardImportService; + } +} diff --git a/lib/Service/AImportService.php b/lib/Service/AImportService.php deleted file mode 100644 index 3ca20226b..000000000 --- a/lib/Service/AImportService.php +++ /dev/null @@ -1,49 +0,0 @@ -config = $config; - } - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void { - if (!$this->config) { - $this->setConfigInstance(new \stdClass); - } - $this->config->$configName = $value; - } - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig(string $configName = null) { - if (!is_object($this->config)) { - return; - } - if (!$configName) { - return $this->config; - } - if (!property_exists($this->config, $configName)) { - return; - } - return $this->config->$configName; - } -} diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php new file mode 100644 index 000000000..b210e9b5e --- /dev/null +++ b/lib/Service/BoardImportCommandService.php @@ -0,0 +1,180 @@ +command = $command; + } + + /** + * @return BoardImport + */ + public function getCommand() { + return $this->command; + } + + public function setInput($input): self { + $this->input = $input; + return $this; + } + + public function getInput(): InputInterface { + return $this->input; + } + + public function setOutput($output): self { + $this->output = $output; + return $this; + } + + public function getOutput(): OutputInterface { + return $this->output; + } + + public function validate(): self { + $this->validateSystem(); + $this->validateConfig(); + $this->validateData(); + return $this; + } + + private function validateConfig(): void { + $configFile = $this->getInput()->getOption('config'); + if (!is_file($configFile)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid config json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'config file not found' + ); + } + return $answer; + }); + $configFile = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('config', $configFile); + } + + $config = json_decode(file_get_contents($configFile)); + $system = $this->getSystem(); + $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; + $validator = new Validator(); + $validator->validate( + $config, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + $this->getOutput()->writeln('Invalid config file'); + $this->getOutput()->writeln(array_map(function ($v) { + return $v['message']; + }, $validator->getErrors())); + $this->getOutput()->writeln('Valid schema:'); + $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); + $this->getInput()->setOption('config', null); + $this->validateConfig($this->getInput(), $this->getOutput()); + } + $this->setConfigInstance($config); + $this->validateOwner(); + } + + /** + * @return void + */ + private function validateSystem(): self { + $system = $this->getInput()->getOption('system'); + if (in_array($system, $this->getAllowedImportSystems())) { + return $this->setSystem($system); + } + $helper = $this->getCommand()->getHelper('question'); + $question = new ChoiceQuestion( + 'Please inform a source system', + $this->allowedSystems, + 0 + ); + $question->setErrorMessage('System %s is invalid.'); + $system = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('system', $system); + return $this->setSystem($system); + } + + private function validateData(): self { + $filename = $this->getInput()->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('data', $data); + } + $this->setData(json_decode(file_get_contents($filename))); + if (!$this->getData()) { + $this->getOutput()->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($this->getInput(), $this->getOutput()); + } + $this->validateUsers(); + return $this; + } + + public function import(): void { + $this->getOutput()->writeln('Importing board...'); + $this->importBoard(); + $this->getOutput()->writeln('Assign users to board...'); + $this->importAcl(); + $this->getOutput()->writeln('Importing labels...'); + $this->importLabels(); + $this->getOutput()->writeln('Importing stacks...'); + $this->importStacks(); + $this->getOutput()->writeln('Importing cards...'); + $this->importCards(); + $this->getOutput()->writeln('Assign cards to labels...'); + $this->assignCardsToLabels(); + $this->getOutput()->writeln('Iporting comments...'); + $this->importComments(); + $this->getOutput()->writeln('Iporting participants...'); + $this->importParticipants(); + } +} diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php new file mode 100644 index 000000000..38bae3155 --- /dev/null +++ b/lib/Service/BoardImportService.php @@ -0,0 +1,363 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Service; + +use JsonSchema\Constraints\Constraint; +use JsonSchema\Validator; +use OCA\Deck\AppInfo\Application; +use OCA\Deck\BadRequestException; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Label; +use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\NotFoundException; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\MessageTooLongException; +use OCP\Comments\NotFoundException as CommentNotFoundException; +use OCP\IDBConnection; +use OCP\IUserManager; + +class BoardImportService { + /** @var IDBConnection */ + protected $dbConn; + /** @var IUserManager */ + private $userManager; + /** @var BoardMapper */ + private $boardMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var LabelMapper */ + private $labelMapper; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var ICommentsManager */ + private $commentsManager; + /** @var string */ + private $system; + /** @var ABoardImportService */ + private $systemInstance; + /** @var string[] */ + private $allowedSystems; + /** + * Data object created from config JSON + * + * @var \stdClass + */ + public $config; + /** + * Data object created from JSON of origin system + * + * @var \stdClass + */ + private $data; + /** @var Board */ + private $board; + + public function __construct( + IDBConnection $dbConn, + IUserManager $userManager, + BoardMapper $boardMapper, + AclMapper $aclMapper, + LabelMapper $labelMapper, + StackMapper $stackMapper, + CardMapper $cardMapper, + ICommentsManager $commentsManager + ) { + $this->dbConn = $dbConn; + $this->userManager = $userManager; + $this->boardMapper = $boardMapper; + $this->aclMapper = $aclMapper; + $this->labelMapper = $labelMapper; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + $this->commentsManager = $commentsManager; + } + + public function import(): void { + $this->validate(); + $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; + $validator = new Validator(); + $validator->validate( + $config, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + throw new BadRequestException('invalid config'); + } + + if (empty($data)) { + throw new BadRequestException('data must be provided'); + } + $this->getImportService()->setData($data); + $this->getImportService()->import(); + // return $newBoard; + } + + public function validate(): self { + if (is_string($system) === false) { + throw new BadRequestException('system must be provided'); + } + + if (!in_array($system, $this->getAllowedImportSystems())) { + throw new BadRequestException('not allowed system'); + } + + if (empty($config)) { + throw new BadRequestException('config must be provided'); + } + return $this; + } + + public function setSystem(string $system): self { + $this->system = $system; + return $this; + } + + public function getSystem() { + return $this->system; + } + + public function getAllowedImportSystems(): array { + if (!$this->allowedSystems) { + $allowedSystems = glob(__DIR__ . '/BoardImport*Service.php'); + $allowedSystems = array_filter($allowedSystems, function($name) { + $name = basename($name); + switch($name) { + case 'ABoardImportService.php': + case 'BoardImportService.php': + case 'BoardImportCommandService.php': + return false; + } + return true; + }); + $this->allowedSystems = array_map(function ($name) { + preg_match('/\/BoardImport(?\w+)Service\.php$/', $name, $matches); + return lcfirst($matches['system']); + }, $allowedSystems); + } + return $this->allowedSystems; + } + + public function getImportSystem(): ABoardImportService { + $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; + if (!is_object($this->systemInstance)) { + $this->systemInstance = \OC::$server->get($systemClass); + $this->systemInstance->setImportService($this); + } + + return $this->systemInstance; + } + + public function importBoard() { + $board = $this->getImportSystem()->getBoard(); + if ($board) { + $this->boardMapper->insert($board); + $this->board = $board; + } + return $this; + } + + public function getBoard(): Board { + return $this->board; + } + + public function importAcl(): self { + $aclList = $this->getImportSystem()->getAclList(); + foreach ($aclList as $acl) { + $this->aclMapper->insert($acl); + } + return $this; + } + + public function importLabels(): self { + $this->getImportSystem()->importLabels(); + return $this; + } + + public function createLabel($title, $color, $boardId): Label { + $label = new Label(); + $label->setTitle($title); + $label->setColor($color); + $label->setBoardId($boardId); + return $this->labelMapper->insert($label); + } + + public function importStacks(): self { + $stack = $this->getImportSystem()->getStacks(); + foreach ($stack as $code => $stack) { + $this->stackMapper->insert($stack); + $this->getImportSystem()->updateStack($code, $stack); + } + return $this; + } + + public function importCards(): self { + $cards = $this->getImportSystem()->getCards(); + foreach ($cards as $code => $card) { + $this->cardMapper->insert($card); + $this->getImportSystem()->updateCard($code, $card); + } + return $this; + } + + public function assignCardToLabel($cardId, $labelId): self { + $this->cardMapper->assignLabel( + $cardId, + $labelId + ); + return $this; + } + + public function assignCardsToLabels(): self { + $this->getImportSystem()->assignCardsToLabels(); + return $this; + } + + public function importComments(): self { + $this->getImportSystem()->importComments(); + return $this; + } + + public function insertComment($cardId, IComment $comment): IComment { + $comment->setObject('deckCard', (string) $cardId); + $comment->setVerb('comment'); + // Check if parent is a comment on the same card + if ($comment->getParentId() !== '0') { + try { + $comment = $this->commentsManager->get($comment->getParentId()); + if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) { + throw new CommentNotFoundException(); + } + } catch (CommentNotFoundException $e) { + throw new BadRequestException('Invalid parent id: The parent comment was not found or belongs to a different card'); + } + } + + try { + $qb = $this->dbConn->getQueryBuilder(); + + $values = [ + 'parent_id' => $qb->createNamedParameter($comment->getParentId()), + 'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()), + 'children_count' => $qb->createNamedParameter($comment->getChildrenCount()), + 'actor_type' => $qb->createNamedParameter($comment->getActorType()), + 'actor_id' => $qb->createNamedParameter($comment->getActorId()), + 'message' => $qb->createNamedParameter($comment->getMessage()), + 'verb' => $qb->createNamedParameter($comment->getVerb()), + 'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'), + 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'), + 'object_type' => $qb->createNamedParameter($comment->getObjectType()), + 'object_id' => $qb->createNamedParameter($comment->getObjectId()), + 'reference_id' => $qb->createNamedParameter($comment->getReferenceId()) + ]; + + $affectedRows = $qb->insert('comments') + ->values($values) + ->execute(); + + if ($affectedRows > 0) { + $comment->setId((string)$qb->getLastInsertId()); + } + return $comment; + } catch (\InvalidArgumentException $e) { + throw new BadRequestException('Invalid input values'); + } catch (CommentNotFoundException $e) { + throw new NotFoundException('Could not create comment.'); + } + } + + public function importParticipants() { + $this->getImportSystem()->importParticipants(); + } + + public function setData(\stdClass $data): self { + $this->data = $data; + return $this; + } + + public function getData() { + return $this->data; + } + + /** + * Define a config + * + * @param string $configName + * @param mixed $value + * @return self + */ + public function setConfig(string $configName, $value): self { + if (!$this->config) { + $this->setConfigInstance(new \stdClass); + } + $this->config->$configName = $value; + return $this; + } + + /** + * Get a config + * + * @param string $configName config name + * @return mixed + */ + public function getConfig(string $configName = null) { + if (!is_object($this->config)) { + return; + } + if (!$configName) { + return $this->config; + } + if (!property_exists($this->config, $configName)) { + return; + } + return $this->config->$configName; + } + + public function setConfigInstance(\stdClass $config): self { + $this->config = $config; + return $this; + } + + public function validateOwner(): self { + $owner = $this->userManager->get($this->getConfig('owner')); + if (!$owner) { + throw new \LogicException('Owner "' . $this->getConfigboardImportService->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); + } + $this->setConfig('owner', $owner); + return $this; + } + + public function validateUsers(): self { + $this->getImportSystem()->validateUsers(); + return $this; + } +} diff --git a/lib/Service/TrelloImportService.php b/lib/Service/BoardImportTrelloService.php similarity index 55% rename from lib/Service/TrelloImportService.php rename to lib/Service/BoardImportTrelloService.php index c09251579..09b96531d 100644 --- a/lib/Service/TrelloImportService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -23,6 +23,7 @@ namespace OCA\Deck\Service; +use OC\Comments\Comment; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\Assignment; @@ -38,9 +39,7 @@ use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -class TrelloImportService extends AImportService { - /** @var BoardService */ - private $boardService; +class BoardImportTrelloService extends ABoardImportService { /** @var LabelService */ private $labelService; /** @var StackMapper */ @@ -57,8 +56,6 @@ class TrelloImportService extends AImportService { private $userManager; /** @var IL10N */ private $l10n; - /** @var Board */ - private $board; /** * Array of stacks * @@ -75,12 +72,6 @@ class TrelloImportService extends AImportService { private $cards = []; /** @var IUser[] */ private $members = []; - /** - * Data object created from JSON of origin system - * - * @var \StdClass - */ - private $data; public function __construct( BoardService $boardService, @@ -104,31 +95,21 @@ class TrelloImportService extends AImportService { $this->l10n = $l10n; } - public function setData(\stdClass $data) { - $this->data = $data; - } - - public function getData() { - return $this->data; - } - - public function validateOwner(): void { - $owner = $this->userManager->get($this->getConfig('owner')); - if (!$owner) { - throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); - } - $this->setConfig('owner', $owner); + public function validate(): ABoardImportService { + $this->boardImportTrelloService->validateOwner(); + $this->boardImportTrelloService->validateUsers(); + return $this; } /** - * @return void + * @return ABoardImportService */ - public function validateUsers() { - if (empty($this->getConfig('uidRelation'))) { - return; + public function validateUsers(): self { + if (empty($this->getImportService()->getConfig('uidRelation'))) { + return $this; } - foreach ($this->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { - $user = array_filter($this->data->members, function ($u) use ($trelloUid) { + foreach ($this->getImportService()->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { + $user = array_filter($this->getImportService()->getData()->members, function ($u) use ($trelloUid) { return $u->username === $trelloUid; }); if (!$user) { @@ -137,29 +118,35 @@ class TrelloImportService extends AImportService { if (!is_string($nextcloudUid)) { throw new \LogicException('User on setting uidRelation must be a string'); } - $this->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); - if (!$this->getConfig('uidRelation')->$trelloUid) { + $this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) { throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); } $user = current($user); - $this->members[$user->id] = $this->getConfig('uidRelation')->$trelloUid; + $this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid; } + return $this; } - public function assignUsersToBoard(): void { + /** + * @return Acl[] + */ + public function getAclList(): array { + $return = []; foreach ($this->members as $member) { - if ($member->getUID() === $this->getConfig('owner')->getUID()) { + if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { continue; } $acl = new Acl(); - $acl->setBoardId($this->board->getId()); + $acl->setBoardId($this->getImportService()->getBoard()->getId()); $acl->setType(Acl::PERMISSION_TYPE_USER); $acl->setParticipant($member->getUID()); $acl->setPermissionEdit(false); $acl->setPermissionShare(false); $acl->setPermissionManage(false); - $this->aclMapper->insert($acl); + $return[] = $acl; } + return $return; } private function checklistItem($item): string { @@ -181,14 +168,17 @@ class TrelloImportService extends AImportService { return $checklist_string; } - public function importCards(): void { + /** + * @return Card[] + */ + public function getCards(): array { $checklists = []; - foreach ($this->data->checklists as $checklist) { + foreach ($this->getImportService()->getData()->checklists as $checklist) { $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); } - $this->data->checklists = $checklists; + $this->getImportService()->getData()->checklists = $checklists; - foreach ($this->data->cards as $trelloCard) { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { $card = new Card(); $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); $card->setLastModified($lastModified->format('Y-m-d H:i:s')); @@ -196,7 +186,7 @@ class TrelloImportService extends AImportService { $card->setDeletedAt($lastModified->format('U')); } if ((count($trelloCard->idChecklists) !== 0)) { - foreach ($this->data->checklists[$trelloCard->id] as $checklist) { + foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) { $trelloCard->desc .= "\n" . $checklist; } } @@ -206,24 +196,25 @@ class TrelloImportService extends AImportService { $card->setStackId($this->stacks[$trelloCard->idList]->getId()); $card->setType('plain'); $card->setOrder($trelloCard->idShort); - $card->setOwner($this->getConfig('owner')->getUID()); + $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) ->format('Y-m-d H:i:s'); $card->setDuedate($duedate); } - $card = $this->cardMapper->insert($card); $this->cards[$trelloCard->id] = $card; - - $this->associateCardToLabels($card, $trelloCard); - $this->importComments($card, $trelloCard); - $this->assignToMember($card, $trelloCard); } + return $this->cards; + } + + public function updateCard($cardTrelloId, Card $card): self { + $this->cards[$cardTrelloId] = $card; + return $this; } /** - * @return void + * @return ABoardImportService */ private function appendAttachmentsToDescription($trelloCard) { if (empty($trelloCard->attachments)) { @@ -236,86 +227,92 @@ class TrelloImportService extends AImportService { $name = $attachment->name === $attachment->url ? null : $attachment->name; $trelloCard->desc .= "| {$attachment->url} | {$name} | {$attachment->date} |\n"; } + return $this; } - private function assignToMember(Card $card, $trelloCard): void { - foreach ($trelloCard->idMembers as $idMember) { - $assignment = new Assignment(); - $assignment->setCardId($card->getId()); - $assignment->setParticipant($this->members[$idMember]->getUID()); - $assignment->setType(Assignment::TYPE_USER); - $assignment = $this->assignmentMapper->insert($assignment); + public function importParticipants(): ABoardImportService { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->idMembers as $idMember) { + $assignment = new Assignment(); + $assignment->setCardId($this->cards[$trelloCard->id]->getId()); + $assignment->setParticipant($this->members[$idMember]->getUID()); + $assignment->setType(Assignment::TYPE_USER); + $assignment = $this->assignmentMapper->insert($assignment); + } } + return $this; } - private function importComments(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { - $comments = array_filter( - $this->data->actions, - function ($a) use ($trelloCard) { - return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + public function importComments(): ABoardImportService { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + $comments = array_filter( + $this->getImportService()->getData()->actions, + function ($a) use ($trelloCard) { + return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + } + ); + foreach ($comments as $trelloComment) { + if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + } else { + $actor = $this->getImportService()->getConfig('owner')->getUID(); + } + $comment = new Comment(); + $comment + ->setActor('users', $actor) + ->setMessage($this->replaceUsernames($trelloComment->data->text), 0) + ->setCreationDateTime( + \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) + ); + $this->getImportService()->insertComment( + $this->cards[$trelloCard->id]->getId(), + $comment + ); } - ); - foreach ($comments as $trelloComment) { - if (!empty($this->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { - $actor = $this->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); - } else { - $actor = $this->getConfig('owner')->getUID(); - } - $message = $this->replaceUsernames($trelloComment->data->text); - $qb = $this->connection->getQueryBuilder(); - - $values = [ - 'parent_id' => $qb->createNamedParameter(0), - 'topmost_parent_id' => $qb->createNamedParameter(0), - 'children_count' => $qb->createNamedParameter(0), - 'actor_type' => $qb->createNamedParameter('users'), - 'actor_id' => $qb->createNamedParameter($actor), - 'message' => $qb->createNamedParameter($message), - 'verb' => $qb->createNamedParameter('comment'), - 'creation_timestamp' => $qb->createNamedParameter( - \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) - ->format('Y-m-d H:i:s') - ), - 'latest_child_timestamp' => $qb->createNamedParameter(null), - 'object_type' => $qb->createNamedParameter('deckCard'), - 'object_id' => $qb->createNamedParameter($card->getId()), - ]; - - $qb->insert('comments') - ->values($values) - ->execute(); } + return $this; } private function replaceUsernames($text) { - foreach ($this->getConfig('uidRelation') as $trello => $nextcloud) { + foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { $text = str_replace($trello, $nextcloud->getUID(), $text); } return $text; } - private function associateCardToLabels(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { - foreach ($trelloCard->labels as $label) { - $this->cardMapper->assignLabel( - $card->getId(), - $this->labels[$label->id]->getId() - ); + public function assignCardsToLabels(): self { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->labels as $label) { + $this->getImportService()->assignCardToLabel( + $this->cards[$trelloCard->id]->getId(), + $this->labels[$label->id]->getId() + ); + } } + return $this; } - public function importStacks(): void { - $this->stacks = []; - foreach ($this->data->lists as $order => $list) { + /** + * @return Stack[] + */ + public function getStacks(): array { + $return = []; + foreach ($this->getImportService()->getData()->lists as $order => $list) { $stack = new Stack(); if ($list->closed) { $stack->setDeletedAt(time()); } $stack->setTitle($list->name); - $stack->setBoardId($this->board->getId()); + $stack->setBoardId($this->getImportService()->getBoard()->getId()); $stack->setOrder($order + 1); - $stack = $this->stackMapper->insert($stack); - $this->stacks[$list->id] = $stack; + $return[$list->id] = $stack; } + return $return; + } + + public function updateStack($id, $stack): self { + $this->stacks[$id] = $stack; + return $this; } private function translateColor($color): string { @@ -345,45 +342,28 @@ class TrelloImportService extends AImportService { } } - public function importBoard(): void { - $this->board = $this->boardService->create( - $this->data->name, - $this->getConfig('owner')->getUID(), - $this->getConfig('color') - ); + public function getBoard(): Board { + $board = new Board(); + $board->setTitle($this->getImportService()->getData()->name); + $board->setOwner($this->getImportService()->getConfig('owner')->getUID()); + $board->setColor($this->getImportService()->getConfig('color')); + return $board; } - public function importLabels(): void { - $this->labels = []; - foreach ($this->data->labels as $label) { + public function importLabels(): self { + foreach ($this->getImportService()->getData()->labels as $label) { if (empty($label->name)) { $labelTitle = 'Unnamed ' . $label->color . ' label'; } else { $labelTitle = $label->name; } - $newLabel = $this->labelService->create( + $newLabel = $this->getImportService()->createLabel( $labelTitle, $this->translateColor($label->color), - $this->board->getId() + $this->getImportService()->getBoard()->getId() ); $this->labels[$label->id] = $newLabel; } - } - - public function setUserId(): void { - if (!property_exists($this->labelService, 'permissionService')) { - return; - } - $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); - $propertyPermissionService->setAccessible(true); - $permissionService = $propertyPermissionService->getValue($this->labelService); - - if (!property_exists($permissionService, 'userId')) { - return; - } - - $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); - $propertyUserId->setAccessible(true); - $propertyUserId->setValue($permissionService, $this->getConfig('owner')->getUID()); + return $this; } } diff --git a/lib/Command/ImportHelper/fixtures/config-trello-schema.json b/lib/Service/fixtures/config-trello-schema.json similarity index 100% rename from lib/Command/ImportHelper/fixtures/config-trello-schema.json rename to lib/Service/fixtures/config-trello-schema.json diff --git a/tests/unit/Command/fixtures/config-trello.json b/tests/data/config-trello.json similarity index 100% rename from tests/unit/Command/fixtures/config-trello.json rename to tests/data/config-trello.json diff --git a/tests/unit/Command/fixtures/data-trello.json b/tests/data/data-trello.json similarity index 100% rename from tests/unit/Command/fixtures/data-trello.json rename to tests/data/data-trello.json diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index 77eac621b..0d369fbcc 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -24,6 +24,9 @@ namespace OCA\Deck\Command; use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\AImportService; +use OCA\Deck\Service\BoardImportService; +use OCA\Deck\Service\BoardService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -32,14 +35,21 @@ use Symfony\Component\Console\Output\OutputInterface; class BoardImportTest extends \Test\TestCase { /** @var TrelloHelper */ private $trelloHelper; + /** @var BoardImportService */ + private $boardImportService; /** @var BoardImport */ private $boardImport; public function setUp(): void { parent::setUp(); $this->trelloHelper = $this->createMock(TrelloHelper::class); + $this->boardImportService = $this->createMock(BoardImportService::class); + $this->boardImportService + ->method('getAllowedImportSystems') + ->willReturn(['trello']); $this->boardImport = new BoardImport( - $this->trelloHelper + $this->trelloHelper, + $this->boardImportService ); $questionHelper = new QuestionHelper(); $this->boardImport->setHelperSet( @@ -60,8 +70,8 @@ class BoardImportTest extends \Test\TestCase { ) ->will($this->returnValueMap([ ['system', 'trello'], - ['config', __DIR__ . '/fixtures/config-trello.json'], - ['data', __DIR__ . '/fixtures/data-trello.json'] + ['config', __DIR__ . '/../../data/config-trello.json'], + ['data', __DIR__ . '/../../data/data-trello.json'] ])); $output = $this->createMock(OutputInterface::class); @@ -69,6 +79,13 @@ class BoardImportTest extends \Test\TestCase { ->expects($this->once()) ->method('writeLn') ->with('Done!'); + $this->boardImportService + ->method('getSystem') + ->willReturn('trello'); + $importService = $this->createMock(AImportService::class); + $this->boardImportService + ->method('getImportService') + ->willReturn($importService); $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); $actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]); diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php index 006436ed0..25972a7d8 100644 --- a/tests/unit/Command/Helper/TrelloHelperTest.php +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -24,6 +24,8 @@ namespace OCA\Deck\Command; use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\AImportService; +use OCA\Deck\Service\BoardImportService; use OCA\Deck\Service\TrelloImportService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; @@ -33,6 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface; class TrelloHelperTest extends \Test\TestCase { /** @var TrelloImportService */ private $trelloImportService; + /** @var BoardImportService */ + private $boardImportService; /** @var TrelloHelper */ private $trelloHelper; public function setUp(): void { @@ -42,7 +46,14 @@ class TrelloHelperTest extends \Test\TestCase { $this->trelloImportService ); $questionHelper = new QuestionHelper(); - $command = new BoardImport($this->trelloHelper); + $this->boardImportService = $this->createMock(BoardImportService::class); + $this->boardImportService + ->method('getAllowedImportSystems') + ->willReturn(['trello']); + $command = new BoardImport( + $this->trelloHelper, + $this->boardImportService + ); $command->setHelperSet( new HelperSet([ $questionHelper @@ -61,10 +72,18 @@ class TrelloHelperTest extends \Test\TestCase { ) ->will($this->returnValueMap([ ['system', 'trello'], - ['config', __DIR__ . '/../fixtures/config-trello.json'] + ['config', __DIR__ . '/../../../data/config-trello.json'] ])); $output = $this->createMock(OutputInterface::class); + $this->boardImportService + ->method('getSystem') + ->willReturn('trello'); + $importService = $this->createMock(AImportService::class); + $this->boardImportService + ->method('getImportService') + ->willReturn($importService); + $this->invokePrivate($this->trelloHelper->getCommand(), 'validateSystem', [$input, $output]); $this->invokePrivate($this->trelloHelper->getCommand(), 'validateConfig', [$input, $output]); $actual = $this->trelloHelper->import($input, $output); diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php new file mode 100644 index 000000000..7de0cadd1 --- /dev/null +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -0,0 +1,46 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Deck\Service; + +class BoardImportServiceTest extends \Test\TestCase { + /** @var TrelloImportService */ + private $trelloImportService; + /** @var BoardImportService */ + private $boardImportService; + public function setUp(): void { + $this->trelloImportService = $this->createMock(TrelloImportService::class); + $this->boardImportService = new BoardImportService( + $this->trelloImportService + ); + } + + public function testImportSuccess() { + $config = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $actual = $this->boardImportService->import( + 'trello', + $config, + $data + ); + } +} diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php index 7dba39b5d..be2785378 100644 --- a/tests/unit/Service/BoardServiceTest.php +++ b/tests/unit/Service/BoardServiceTest.php @@ -73,6 +73,8 @@ class BoardServiceTest extends TestCase { private $changeHelper; /** @var IEventDispatcher */ private $eventDispatcher; + /** @var TrelloImportService */ + private $trelloImportService; private $userId = 'admin'; public function setUp(): void { @@ -91,6 +93,7 @@ class BoardServiceTest extends TestCase { $this->activityManager = $this->createMock(ActivityManager::class); $this->changeHelper = $this->createMock(ChangeHelper::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->trelloImportService = $this->createMock(TrelloImportService::class); $this->service = new BoardService( $this->boardMapper, @@ -107,6 +110,7 @@ class BoardServiceTest extends TestCase { $this->activityManager, $this->eventDispatcher, $this->changeHelper, + $this->trelloImportService, $this->userId );