Merge pull request #5004 from nextcloud/backport/5003/stable26

This commit is contained in:
Julius Härtl
2023-08-12 10:01:11 +02:00
committed by GitHub
24 changed files with 1928 additions and 147 deletions

View File

@@ -71,7 +71,7 @@ jobs:
path: apps/${{ env.APP_NAME }} path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }} - name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.24.0 uses: shivammathur/setup-php@2.25.5
with: with:
php-version: ${{ matrix.php-versions }} php-version: ${{ matrix.php-versions }}
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, apcu extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, apcu
@@ -98,7 +98,7 @@ jobs:
cat config/config.php cat config/config.php
./occ user:list ./occ user:list
./occ app:enable --force ${{ env.APP_NAME }} ./occ app:enable --force ${{ env.APP_NAME }}
./occ config:system:set query_log_file --value '/home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/query.log' ./occ config:system:set query_log_file --value "$PWD/query.log"
php -S localhost:8080 & php -S localhost:8080 &
- name: Run behat - name: Run behat
@@ -123,12 +123,12 @@ jobs:
myError += data.toString() myError += data.toString()
} }
} }
await exec.exec(`/bin/bash -c "cat /home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/query.log | wc -l"`, [], options) await exec.exec(`/bin/bash -c "cat query.log | wc -l"`, [], options)
msg = myOutput msg = myOutput
const queryCount = parseInt(myOutput, 10) const queryCount = parseInt(myOutput, 10)
myOutput = '' myOutput = ''
await exec.exec('cat', ['/home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/apps/${{ env.APP_NAME }}/tests/integration/base-query-count.txt'], options) await exec.exec('cat', ['apps/${{ env.APP_NAME }}/tests/integration/base-query-count.txt'], options)
const baseCount = parseInt(myOutput, 10) const baseCount = parseInt(myOutput, 10)
const absoluteIncrease = queryCount - baseCount const absoluteIncrease = queryCount - baseCount

View File

@@ -16,5 +16,9 @@
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands.js' import './commands.js'
Cypress.on('uncaught:exception', (err) => {
return !err.message.includes('ResizeObserver loop limit exceeded')
})
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

98
docs/export-import.md Normal file
View File

@@ -0,0 +1,98 @@
## Export
Deck currently supports exporting all boards a user owns in a single JSON file. The format is based on the database schema that deck uses. It can be used to re-import boards on the same or other instances.
The export currently has some kown limitations in terms of specific data not included:
- Activity information
- File attachments to deck cards
- Comments
-
```
occ deck:export > my-file.json
```
## Import boards
Importing can be done using the API or the `occ` `deck:import` command.
It is possible to import from the following sources:
### Deck JSON
A json file that has been obtained from the above described `occ deck:export [userid]` command can be imported.
```
occ deck:import my-file.json
```
In case you are importing from a different instance you may use an additional config file to provide custom user id mapping in case users have different identifiers.
```
{
"owner": "admin",
"uidRelation": {
"johndoe": "test-user-1"
}
}
```
#### Trello JSON
Limitations:
* Comments with more than 1000 characters are placed as attached files to the card.
Steps:
* Create the data file
* Access Trello
* go to the board you want to export
* Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON
* Create the configuration file
* Execute the import informing the import file path, data file and source as `Trello JSON`
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/main/lib/Service/Importer/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
Example configuration file:
```json
{
"owner": "admin",
"color": "0800fd",
"uidRelation": {
"johndoe": "johndoe"
}
}
```
**Limitations**:
Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has.
#### Trello API
Import using API is recommended for boards with more than 1000 actions.
Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello.
* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization)
* Get the ID of the board you want to import by making a request to:
https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name
This ID you will use in the configuration file in the `board` property
* Create the configuration file
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/main/lib/Service/Importer/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
Example configuration file:
```json
{
"owner": "admin",
"color": "0800fd",
"api": {
"key": "0cc175b9c0f1b6a831c399e269772661",
"token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33"
},
"board": "8277e0910d750195b4487976",
"uidRelation": {
"johndoe": "johndoe"
}
}
```

View File

@@ -25,17 +25,15 @@ namespace OCA\Deck\Command;
use OCA\Deck\Service\Importer\BoardImportCommandService; use OCA\Deck\Service\Importer\BoardImportCommandService;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class BoardImport extends Command { class BoardImport extends Command {
private BoardImportCommandService $boardImportCommandService;
public function __construct( public function __construct(
BoardImportCommandService $boardImportCommandService private BoardImportCommandService $boardImportCommandService
) { ) {
$this->boardImportCommandService = $boardImportCommandService;
parent::__construct(); parent::__construct();
} }
@@ -44,7 +42,9 @@ class BoardImport extends Command {
*/ */
protected function configure() { protected function configure() {
$allowedSystems = $this->boardImportCommandService->getAllowedImportSystems(); $allowedSystems = $this->boardImportCommandService->getAllowedImportSystems();
$names = array_column($allowedSystems, 'name'); $names = array_map(function ($name) {
return '"' . $name . '"';
}, array_column($allowedSystems, 'internalName'));
$this $this
->setName('deck:import') ->setName('deck:import')
->setDescription('Import data') ->setDescription('Import data')
@@ -53,7 +53,7 @@ class BoardImport extends Command {
null, null,
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'Source system for import. Available options: ' . implode(', ', $names) . '.', 'Source system for import. Available options: ' . implode(', ', $names) . '.',
null 'DeckJson',
) )
->addOption( ->addOption(
'config', 'config',
@@ -69,6 +69,11 @@ class BoardImport extends Command {
'Data file to import.', 'Data file to import.',
'data.json' 'data.json'
) )
->addArgument(
'file',
InputArgument::OPTIONAL,
'File to import',
)
; ;
} }

View File

@@ -29,39 +29,23 @@ use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper; use OCA\Deck\Db\StackMapper;
use OCA\Deck\Model\CardDetails; use OCA\Deck\Model\CardDetails;
use OCA\Deck\Service\BoardService; use OCA\Deck\Service\BoardService;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\App\IAppManager;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception;
use OCP\IGroupManager;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class UserExport extends Command { class UserExport extends Command {
protected $boardService; public function __construct(
protected $cardMapper; private IAppManager $appManager,
private $userManager; private BoardMapper $boardMapper,
private $groupManager; private BoardService $boardService,
private $assignedUsersMapper; private StackMapper $stackMapper,
private CardMapper $cardMapper,
public function __construct(BoardMapper $boardMapper, private AssignmentMapper $assignedUsersMapper,
BoardService $boardService, ) {
StackMapper $stackMapper,
CardMapper $cardMapper,
AssignmentMapper $assignedUsersMapper,
IUserManager $userManager,
IGroupManager $groupManager) {
parent::__construct(); parent::__construct();
$this->cardMapper = $cardMapper;
$this->boardService = $boardService;
$this->stackMapper = $stackMapper;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->boardMapper = $boardMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
} }
protected function configure() { protected function configure() {
@@ -73,30 +57,27 @@ class UserExport extends Command {
InputArgument::REQUIRED, InputArgument::REQUIRED,
'User ID of the user' 'User ID of the user'
) )
->addOption('legacy-format', 'l')
; ;
} }
/** /**
* @param InputInterface $input * @throws Exception
* @param OutputInterface $output
* @return int
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws \ReflectionException
*/ */
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument('user-id'); $userId = $input->getArgument('user-id');
$legacyFormat = $input->getOption('legacy-format');
$this->boardService->setUserId($userId); $this->boardService->setUserId($userId);
$boards = $this->boardService->findAll(); $boards = $this->boardService->findAll(fullDetails: false);
$data = []; $data = [];
foreach ($boards as $board) { foreach ($boards as $board) {
$fullBoard = $this->boardMapper->find($board->getId(), true, true); $fullBoard = $this->boardMapper->find($board->getId(), true, true);
$data[$board->getId()] = (array)$fullBoard->jsonSerialize(); $data[$board->getId()] = $fullBoard->jsonSerialize();
$stacks = $this->stackMapper->findAll($board->getId()); $stacks = $this->stackMapper->findAll($board->getId());
foreach ($stacks as $stack) { foreach ($stacks as $stack) {
$data[$board->getId()]['stacks'][] = (array)$stack->jsonSerialize(); $data[$board->getId()]['stacks'][$stack->getId()] = $stack->jsonSerialize();
$cards = $this->cardMapper->findAllByStack($stack->getId()); $cards = $this->cardMapper->findAllByStack($stack->getId());
foreach ($cards as $card) { foreach ($cards as $card) {
$fullCard = $this->cardMapper->find($card->getId()); $fullCard = $this->cardMapper->find($card->getId());
@@ -108,7 +89,12 @@ class UserExport extends Command {
} }
} }
} }
$output->writeln(json_encode($data, JSON_PRETTY_PRINT)); $output->writeln(json_encode(
$legacyFormat ? $data : [
'version' => $this->appManager->getAppVersion('deck'),
'boards' => $data
],
JSON_PRETTY_PRINT));
return 0; return 0;
} }
} }

View File

@@ -41,4 +41,17 @@ class Assignment extends RelationalEntity implements JsonSerializable {
$this->addType('type', 'integer'); $this->addType('type', 'integer');
$this->addResolvable('participant'); $this->addResolvable('participant');
} }
public function getTypeString(): string {
switch ($this->getType()) {
case self::TYPE_USER:
return 'user';
case self::TYPE_GROUP:
return 'group';
case self::TYPE_CIRCLE:
return 'circle';
}
return 'unknown';
}
} }

View File

@@ -532,12 +532,12 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
if ($boardId) { if ($boardId) {
unset($this->boardCache[$boardId]); unset($this->boardCache[$boardId]);
} else { } else {
$this->boardCache = null; $this->boardCache = new CappedMemoryCache();
} }
if ($userId) { if ($userId) {
unset($this->userBoardCache[$userId]); unset($this->userBoardCache[$userId]);
} else { } else {
$this->userBoardCache = null; $this->userBoardCache = new CappedMemoryCache();
} }
} }
} }

View File

@@ -115,7 +115,9 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
} }
public function insert(Entity $entity): Entity { public function insert(Entity $entity): Entity {
$entity->setLastModified(time()); if (!in_array('lastModified', $entity->getUpdatedFields())) {
$entity->setLastModified(time());
}
return parent::insert($entity); return parent::insert($entity);
} }

View File

@@ -138,7 +138,7 @@ class RelationalEntity extends Entity implements \JsonSerializable {
$attr = lcfirst(substr($methodName, 3)); $attr = lcfirst(substr($methodName, 3));
if (array_key_exists($attr, $this->_resolvedProperties) && strpos($methodName, 'set') === 0) { if (array_key_exists($attr, $this->_resolvedProperties) && strpos($methodName, 'set') === 0) {
if (!is_scalar($args[0])) { if ($args[0] !== null && !is_scalar($args[0])) {
$args[0] = $args[0]['primaryKey']; $args[0] = $args[0]['primaryKey'];
} }
parent::setter($attr, $args); parent::setter($attr, $args);

View File

@@ -61,6 +61,10 @@ abstract class ABoardImportService {
*/ */
abstract public function bootstrap(): void; abstract public function bootstrap(): void;
public function getBoards(): array {
return [$this->getImportService()->getData()];
}
abstract public function getBoard(): ?Board; abstract public function getBoard(): ?Board;
/** /**
@@ -133,4 +137,13 @@ abstract class ABoardImportService {
public function needValidateData(): bool { public function needValidateData(): bool {
return $this->needValidateData; return $this->needValidateData;
} }
public function reset(): void {
// FIXME: Would be cleaner if we could just get a new instance per board
// but currently https://github.com/nextcloud/deck/blob/7d820aa3f9fc69ada8188549b9a2fbb9093ffb95/lib/Service/Importer/BoardImportService.php#L194 returns a singleton
$this->labels = [];
$this->stacks = [];
$this->acls = [];
$this->cards = [];
}
} }

View File

@@ -78,6 +78,10 @@ class BoardImportCommandService extends BoardImportService {
protected function validateConfig(): void { protected function validateConfig(): void {
try { try {
$config = $this->getInput()->getOption('config'); $config = $this->getInput()->getOption('config');
if (!$config) {
return;
}
if (is_string($config)) { if (is_string($config)) {
if (!is_file($config)) { if (!is_file($config)) {
throw new NotFoundException('It\'s not a valid config file.'); throw new NotFoundException('It\'s not a valid config file.');
@@ -95,7 +99,7 @@ class BoardImportCommandService extends BoardImportService {
$helper = $this->getCommand()->getHelper('question'); $helper = $this->getCommand()->getHelper('question');
$question = new Question( $question = new Question(
"<info>You can get more info on https://deck.readthedocs.io/en/latest/User_documentation_en/#6-import-boards</info>\n" . "<info>You can get more info on https://deck.readthedocs.io/en/latest/User_documentation_en/#6-import-boards</info>\n" .
'Please inform a valid config json file: ', 'Please provide a valid config json file: ',
'config.json' 'config.json'
); );
$question->setValidator(function (string $answer) { $question->setValidator(function (string $answer) {
@@ -130,7 +134,7 @@ class BoardImportCommandService extends BoardImportService {
$allowedSystems = $this->getAllowedImportSystems(); $allowedSystems = $this->getAllowedImportSystems();
$names = array_column($allowedSystems, 'name'); $names = array_column($allowedSystems, 'name');
$question = new ChoiceQuestion( $question = new ChoiceQuestion(
'Please inform a source system', 'Please select a source system',
$names, $names,
0 0
); );
@@ -145,6 +149,18 @@ class BoardImportCommandService extends BoardImportService {
if (!$this->getImportSystem()->needValidateData()) { if (!$this->getImportSystem()->needValidateData()) {
return; return;
} }
$data = $this->getInput()->getArgument('file');
if (is_string($data)) {
if (!file_exists($data)) {
throw new \OCP\Files\NotFoundException('Could not find file ' . $data);
}
$data = json_decode(file_get_contents($data));
if ($data instanceof \stdClass) {
$this->setData($data);
return;
}
}
$data = $this->getInput()->getOption('data'); $data = $this->getInput()->getOption('data');
if (is_string($data)) { if (is_string($data)) {
$data = json_decode(file_get_contents($data)); $data = json_decode(file_get_contents($data));
@@ -174,26 +190,56 @@ class BoardImportCommandService extends BoardImportService {
public function bootstrap(): void { public function bootstrap(): void {
$this->setSystem($this->getInput()->getOption('system')); $this->setSystem($this->getInput()->getOption('system'));
parent::bootstrap(); parent::bootstrap();
$this->registerErrorCollector(function ($error, $exception) {
$message = $error;
if ($exception instanceof \Throwable) {
$message .= ': ' . $exception->getMessage();
}
$this->getOutput()->writeln('<error>' . $message . '</error>');
if ($exception instanceof \Throwable && $this->getOutput()->isVeryVerbose()) {
$this->getOutput()->writeln($exception->getTraceAsString());
}
});
$this->registerOutputCollector(function ($info) {
if ($this->getOutput()->isVerbose()) {
$this->getOutput()->writeln('<info>' . $info . '</info>', );
}
});
} }
public function import(): void { public function import(): void {
$this->getOutput()->writeln('Starting import...'); $this->getOutput()->writeln('Starting import...');
$this->bootstrap(); $this->bootstrap();
$this->getOutput()->writeln('Importing board...'); $this->validateSystem();
$this->importBoard(); $this->validateConfig();
$this->getOutput()->writeln('Assign users to board...'); $boards = $this->getImportSystem()->getBoards();
$this->importAcl();
$this->getOutput()->writeln('Importing labels...'); foreach ($boards as $board) {
$this->importLabels(); try {
$this->getOutput()->writeln('Importing stacks...'); $this->reset();
$this->importStacks(); $this->setData($board);
$this->getOutput()->writeln('Importing cards...'); $this->getOutput()->writeln('Importing board "' . $board->title . '".');
$this->importCards(); $this->importBoard();
$this->getOutput()->writeln('Assign cards to labels...'); $this->getOutput()->writeln('Assign users to board...');
$this->assignCardsToLabels(); $this->importAcl();
$this->getOutput()->writeln('Importing comments...'); $this->getOutput()->writeln('Importing labels...');
$this->importComments(); $this->importLabels();
$this->getOutput()->writeln('Importing participants...'); $this->getOutput()->writeln('Importing stacks...');
$this->importCardAssignments(); $this->importStacks();
$this->getOutput()->writeln('Importing cards...');
$this->importCards();
$this->getOutput()->writeln('Assign cards to labels...');
$this->assignCardsToLabels();
$this->getOutput()->writeln('Importing comments...');
$this->importComments();
$this->getOutput()->writeln('Importing participants...');
$this->importCardAssignments();
$this->getOutput()->writeln('<info>Finished board import of "' . $this->getBoard()->getTitle() . '"</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>Import failed for board ' . $board->title . ': ' . $e->getMessage() . '</error>');
}
}
} }
} }

View File

@@ -40,6 +40,7 @@ use OCA\Deck\Event\BoardImportGetAllowedEvent;
use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\Exceptions\ConflictException;
use OCA\Deck\NotFoundException; use OCA\Deck\NotFoundException;
use OCA\Deck\Service\FileService; use OCA\Deck\Service\FileService;
use OCA\Deck\Service\Importer\Systems\DeckJsonService;
use OCA\Deck\Service\Importer\Systems\TrelloApiService; use OCA\Deck\Service\Importer\Systems\TrelloApiService;
use OCA\Deck\Service\Importer\Systems\TrelloJsonService; use OCA\Deck\Service\Importer\Systems\TrelloJsonService;
use OCP\Comments\IComment; use OCP\Comments\IComment;
@@ -48,20 +49,11 @@ use OCP\Comments\NotFoundException as CommentNotFoundException;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Server; use OCP\Server;
use Psr\Log\LoggerInterface;
class BoardImportService { class BoardImportService {
private IUserManager $userManager;
private BoardMapper $boardMapper;
private AclMapper $aclMapper;
private LabelMapper $labelMapper;
private StackMapper $stackMapper;
private CardMapper $cardMapper;
private AssignmentMapper $assignmentMapper;
private AttachmentMapper $attachmentMapper;
private ICommentsManager $commentsManager;
private IEventDispatcher $eventDispatcher;
private string $system = ''; private string $system = '';
private ?ABoardImportService $systemInstance; private ?ABoardImportService $systemInstance = null;
private array $allowedSystems = []; private array $allowedSystems = [];
/** /**
* Data object created from config JSON * Data object created from config JSON
@@ -79,30 +71,50 @@ class BoardImportService {
private $data; private $data;
private Board $board; private Board $board;
/** @var callable[] */
private array $errorCollectors = [];
/** @var callable[] */
private array $outputCollectors = [];
public function __construct( public function __construct(
IUserManager $userManager, private IUserManager $userManager,
BoardMapper $boardMapper, private BoardMapper $boardMapper,
AclMapper $aclMapper, private AclMapper $aclMapper,
LabelMapper $labelMapper, private LabelMapper $labelMapper,
StackMapper $stackMapper, private StackMapper $stackMapper,
AssignmentMapper $assignmentMapper, private AssignmentMapper $assignmentMapper,
AttachmentMapper $attachmentMapper, private AttachmentMapper $attachmentMapper,
CardMapper $cardMapper, private CardMapper $cardMapper,
ICommentsManager $commentsManager, private ICommentsManager $commentsManager,
IEventDispatcher $eventDispatcher private IEventDispatcher $eventDispatcher,
private LoggerInterface $logger
) { ) {
$this->userManager = $userManager;
$this->boardMapper = $boardMapper;
$this->aclMapper = $aclMapper;
$this->labelMapper = $labelMapper;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->assignmentMapper = $assignmentMapper;
$this->attachmentMapper = $attachmentMapper;
$this->commentsManager = $commentsManager;
$this->eventDispatcher = $eventDispatcher;
$this->board = new Board(); $this->board = new Board();
$this->disableCommentsEvents(); $this->disableCommentsEvents();
$this->config = new \stdClass();
}
public function registerErrorCollector(callable $errorCollector): void {
$this->errorCollectors[] = $errorCollector;
}
public function registerOutputCollector(callable $outputCollector): void {
$this->outputCollectors[] = $outputCollector;
}
private function addError(string $message, $exception): void {
$message .= ' (on board ' . $this->getBoard()->getTitle() . ')';
foreach ($this->errorCollectors as $errorCollector) {
$errorCollector($message, $exception);
}
$this->logger->error($message, ['exception' => $exception]);
}
private function addOutput(string $message): void {
foreach ($this->outputCollectors as $outputCollector) {
$outputCollector($message);
}
} }
private function disableCommentsEvents(): void { private function disableCommentsEvents(): void {
@@ -120,17 +132,23 @@ class BoardImportService {
public function import(): void { public function import(): void {
$this->bootstrap(); $this->bootstrap();
try { $boards = $this->getImportSystem()->getBoards();
$this->importBoard(); foreach ($boards as $board) {
$this->importAcl(); try {
$this->importLabels(); $this->reset();
$this->importStacks(); $this->setData($board);
$this->importCards(); $this->importBoard();
$this->assignCardsToLabels(); $this->importAcl();
$this->importComments(); $this->importLabels();
$this->importCardAssignments(); $this->importStacks();
} catch (\Throwable $th) { $this->importCards();
throw new BadRequestException($th->getMessage()); $this->assignCardsToLabels();
$this->importComments();
$this->importCardAssignments();
} catch (\Throwable $th) {
$this->logger->error('Failed to import board', ['exception' => $th]);
throw new BadRequestException($th->getMessage());
}
} }
} }
@@ -138,7 +156,7 @@ class BoardImportService {
$allowedSystems = $this->getAllowedImportSystems(); $allowedSystems = $this->getAllowedImportSystems();
$allowedSystems = array_column($allowedSystems, 'internalName'); $allowedSystems = array_column($allowedSystems, 'internalName');
if (!in_array($this->getSystem(), $allowedSystems)) { if (!in_array($this->getSystem(), $allowedSystems)) {
throw new NotFoundException('Invalid system'); throw new NotFoundException('Invalid system: ' . $this->getSystem());
} }
} }
@@ -164,6 +182,11 @@ class BoardImportService {
public function getAllowedImportSystems(): array { public function getAllowedImportSystems(): array {
if (!$this->allowedSystems) { if (!$this->allowedSystems) {
$this->addAllowedImportSystem([
'name' => DeckJsonService::$name,
'class' => DeckJsonService::class,
'internalName' => 'DeckJson'
]);
$this->addAllowedImportSystem([ $this->addAllowedImportSystem([
'name' => TrelloApiService::$name, 'name' => TrelloApiService::$name,
'class' => TrelloApiService::class, 'class' => TrelloApiService::class,
@@ -195,8 +218,17 @@ class BoardImportService {
$this->systemInstance = $instance; $this->systemInstance = $instance;
} }
public function reset(): void {
$this->board = new Board();
$this->getImportSystem()->reset();
}
public function importBoard(): void { public function importBoard(): void {
$board = $this->getImportSystem()->getBoard(); $board = $this->getImportSystem()->getBoard();
if (!$this->userManager->userExists($board->getOwner())) {
throw new \Exception('Target owner ' . $board->getOwner() . ' not found. Please provide a mapping through the import config.');
}
if ($board) { if ($board) {
$this->boardMapper->insert($board); $this->boardMapper->insert($board);
$this->board = $board; $this->board = $board;
@@ -213,8 +245,12 @@ class BoardImportService {
public function importAcl(): void { public function importAcl(): void {
$aclList = $this->getImportSystem()->getAclList(); $aclList = $this->getImportSystem()->getAclList();
foreach ($aclList as $code => $acl) { foreach ($aclList as $code => $acl) {
$this->aclMapper->insert($acl); try {
$this->getImportSystem()->updateAcl($code, $acl); $this->aclMapper->insert($acl);
$this->getImportSystem()->updateAcl($code, $acl);
} catch (\Exception $e) {
$this->addError('Failed to import acl rule for ' . $acl->getParticipant(), $e);
}
} }
$this->getBoard()->setAcl($aclList); $this->getBoard()->setAcl($aclList);
} }
@@ -222,8 +258,12 @@ class BoardImportService {
public function importLabels(): void { public function importLabels(): void {
$labels = $this->getImportSystem()->getLabels(); $labels = $this->getImportSystem()->getLabels();
foreach ($labels as $code => $label) { foreach ($labels as $code => $label) {
$this->labelMapper->insert($label); try {
$this->getImportSystem()->updateLabel($code, $label); $this->labelMapper->insert($label);
$this->getImportSystem()->updateLabel($code, $label);
} catch (\Exception $e) {
$this->addError('Failed to import label ' . $label->getTitle(), $e);
}
} }
$this->getBoard()->setLabels($labels); $this->getBoard()->setLabels($labels);
} }
@@ -231,8 +271,12 @@ class BoardImportService {
public function importStacks(): void { public function importStacks(): void {
$stacks = $this->getImportSystem()->getStacks(); $stacks = $this->getImportSystem()->getStacks();
foreach ($stacks as $code => $stack) { foreach ($stacks as $code => $stack) {
$this->stackMapper->insert($stack); try {
$this->getImportSystem()->updateStack($code, $stack); $this->stackMapper->insert($stack);
$this->getImportSystem()->updateStack($code, $stack);
} catch (\Exception $e) {
$this->addError('Failed to import list ' . $stack->getTitle(), $e);
}
} }
$this->getBoard()->setStacks(array_values($stacks)); $this->getBoard()->setStacks(array_values($stacks));
} }
@@ -240,22 +284,26 @@ class BoardImportService {
public function importCards(): void { public function importCards(): void {
$cards = $this->getImportSystem()->getCards(); $cards = $this->getImportSystem()->getCards();
foreach ($cards as $code => $card) { foreach ($cards as $code => $card) {
$createdAt = $card->getCreatedAt(); try {
$lastModified = $card->getLastModified(); $createdAt = $card->getCreatedAt();
$this->cardMapper->insert($card); $lastModified = $card->getLastModified();
$updateDate = false; $this->cardMapper->insert($card);
if ($createdAt && $createdAt !== $card->getCreatedAt()) { $updateDate = false;
$card->setCreatedAt($createdAt); if ($createdAt && $createdAt !== $card->getCreatedAt()) {
$updateDate = true; $card->setCreatedAt($createdAt);
$updateDate = true;
}
if ($lastModified && $lastModified !== $card->getLastModified()) {
$card->setLastModified($lastModified);
$updateDate = true;
}
if ($updateDate) {
$this->cardMapper->update($card, false);
}
$this->getImportSystem()->updateCard($code, $card);
} catch (\Exception $e) {
$this->addError('Failed to import card ' . $card->getTitle(), $e);
} }
if ($lastModified && $lastModified !== $card->getLastModified()) {
$card->setLastModified($lastModified);
$updateDate = true;
}
if ($updateDate) {
$this->cardMapper->update($card, false);
}
$this->getImportSystem()->updateCard($code, $card);
} }
} }
@@ -276,11 +324,15 @@ class BoardImportService {
$data = $this->getImportSystem()->getCardLabelAssignment(); $data = $this->getImportSystem()->getCardLabelAssignment();
foreach ($data as $cardId => $assignemnt) { foreach ($data as $cardId => $assignemnt) {
foreach ($assignemnt as $assignmentId => $labelId) { foreach ($assignemnt as $assignmentId => $labelId) {
$this->assignCardToLabel( try {
$cardId, $this->assignCardToLabel(
$labelId $cardId,
); $labelId
$this->getImportSystem()->updateCardLabelsAssignment($cardId, $assignmentId, $labelId); );
$this->getImportSystem()->updateCardLabelsAssignment($cardId, $assignmentId, $labelId);
} catch (\Exception $e) {
$this->addError('Failed to assign label ' . $labelId . ' to ' . $cardId, $e);
}
} }
} }
} }
@@ -322,9 +374,14 @@ class BoardImportService {
public function importCardAssignments(): void { public function importCardAssignments(): void {
$allAssignments = $this->getImportSystem()->getCardAssignments(); $allAssignments = $this->getImportSystem()->getCardAssignments();
foreach ($allAssignments as $cardId => $assignments) { foreach ($allAssignments as $cardId => $assignments) {
foreach ($assignments as $assignmentId => $assignment) { foreach ($assignments as $assignment) {
$this->assignmentMapper->insert($assignment); try {
$this->getImportSystem()->updateCardAssignment($cardId, $assignmentId, $assignment); $assignment = $this->assignmentMapper->insert($assignment);
$this->getImportSystem()->updateCardAssignment($cardId, (string)$assignment->getId(), $assignment);
$this->addOutput('Assignment ' . $assignment->getParticipant() . ' added');
} catch (NotFoundException $e) {
$this->addError('No origin or mapping found for card "' . $cardId . '" and ' . $assignment->getTypeString() .' assignment "' . $assignment->getParticipant(), $e);
}
} }
} }
} }

View File

@@ -0,0 +1,260 @@
<?php
/**
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer\Systems;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Service\Importer\ABoardImportService;
use OCP\IUser;
use OCP\IUserManager;
class DeckJsonService extends ABoardImportService {
public static $name = 'Deck JSON';
/** @var IUser[] */
private array $members = [];
private array $tmpCards = [];
public function __construct(
private IUserManager $userManager,
) {
}
public function bootstrap(): void {
$this->validateUsers();
}
public function getJsonSchemaPath(): string {
return implode(DIRECTORY_SEPARATOR, [
__DIR__,
'..',
'fixtures',
'config-deckJson-schema.json',
]);
}
public function validateUsers(): void {
$relation = $this->getImportService()->getConfig('uidRelation');
if (empty($relation)) {
return;
}
foreach ($relation as $exportUid => $nextcloudUid) {
if (!is_string($nextcloudUid) && !is_numeric($nextcloudUid)) {
throw new \LogicException('User on setting uidRelation is invalid');
}
$nextcloudUid = (string) $nextcloudUid;
$this->getImportService()->getConfig('uidRelation')->$exportUid = $this->userManager->get($nextcloudUid);
if (!$this->getImportService()->getConfig('uidRelation')->$exportUid) {
throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid);
}
$this->members[$exportUid] = $this->getImportService()->getConfig('uidRelation')->$exportUid;
}
}
public function mapMember($uid): ?string {
$ownerMap = $this->mapOwner($uid);
$sourceId = ($this->getImportService()->getData()->owner->primaryKey ?? $this->getImportService()->getData()->owner);
if ($uid === $sourceId && $ownerMap !== $sourceId) {
return $ownerMap;
}
$uidCandidate = isset($this->members[$uid]) ? $this->members[$uid]?->getUID() ?? null : null;
if ($uidCandidate) {
return $uidCandidate;
}
if ($this->userManager->userExists($uid)) {
return $uid;
}
return null;
}
public function mapOwner(string $uid): string {
$configOwner = $this->getImportService()->getConfig('owner');
if ($configOwner) {
return $configOwner->getUID();
}
return $uid;
}
public function getCardAssignments(): array {
$assignments = [];
foreach ($this->tmpCards as $sourceCard) {
foreach ($sourceCard->assignedUsers as $idMember) {
$assignment = new Assignment();
$assignment->setCardId($this->cards[$sourceCard->id]->getId());
$assignment->setParticipant($this->mapMember($idMember->participant->uid ?? $idMember->participant));
$assignment->setType($idMember->participant->type);
$assignments[$sourceCard->id][] = $assignment;
}
}
return $assignments;
}
public function getComments(): array {
// Comments are not implemented in export
return [];
}
public function getCardLabelAssignment(): array {
$cardsLabels = [];
foreach ($this->tmpCards as $sourceCard) {
foreach ($sourceCard->labels as $label) {
$cardId = $this->cards[$sourceCard->id]->getId();
$labelId = $this->labels[$label->id]->getId();
$cardsLabels[$cardId][] = $labelId;
}
}
return $cardsLabels;
}
public function getBoard(): Board {
$board = $this->getImportService()->getBoard();
if (empty($this->getImportService()->getData()->title)) {
throw new BadRequestException('Invalid name of board');
}
$importBoard = $this->getImportService()->getData();
$board->setTitle($importBoard->title);
$board->setOwner($this->mapOwner($importBoard->owner?->uid ?? $importBoard->owner));
$board->setColor($importBoard->color);
$board->setArchived($importBoard->archived);
$board->setDeletedAt($importBoard->deletedAt);
$board->setLastModified($importBoard->lastModified);
return $board;
}
/**
* @return Label[]
*/
public function getLabels(): array {
foreach ($this->getImportService()->getData()->labels as $label) {
$newLabel = new Label();
$newLabel->setTitle($label->title);
$newLabel->setColor($label->color);
$newLabel->setBoardId($this->getImportService()->getBoard()->getId());
$newLabel->setLastModified($label->lastModified);
$this->labels[$label->id] = $newLabel;
}
return $this->labels;
}
/**
* @return Stack[]
*/
public function getStacks(): array {
$return = [];
foreach ($this->getImportService()->getData()->stacks as $index => $source) {
if ($source->title) {
$stack = new Stack();
$stack->setTitle($source->title);
$stack->setBoardId($this->getImportService()->getBoard()->getId());
$stack->setOrder($source->order);
$stack->setLastModified($source->lastModified);
$return[$source->id] = $stack;
}
if (isset($source->cards)) {
foreach ($source->cards as $card) {
$card->stackId = $index;
$this->tmpCards[] = $card;
}
}
}
return $return;
}
/**
* @return Card[]
*/
public function getCards(): array {
$cards = [];
foreach ($this->tmpCards as $cardSource) {
$card = new Card();
$card->setTitle($cardSource->title);
$card->setLastModified($cardSource->lastModified);
$card->setLastEditor($cardSource->lastEditor);
$card->setCreatedAt($cardSource->createdAt);
$card->setArchived($cardSource->archived);
$card->setDescription($cardSource->description);
$card->setStackId($this->stacks[$cardSource->stackId]->getId());
$card->setType('plain');
$card->setOrder($cardSource->order);
$boardOwner = $this->getBoard()->getOwner();
$card->setOwner($this->mapOwner(is_string($boardOwner) ? $boardOwner : $boardOwner->getUID()));
$card->setDuedate($cardSource->duedate);
$cards[$cardSource->id] = $card;
}
return $cards;
}
/**
* @return Acl[]
*/
public function getAclList(): array {
$board = $this->getImportService()->getData();
$return = [];
foreach ($board->acl as $aclData) {
$acl = new Acl();
$acl->setBoardId($this->getImportService()->getBoard()->getId());
$acl->setType($aclData->type);
$participant = $aclData->participant?->primaryKey ?? $aclData->participant;
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$participant = $this->mapMember($participant);
}
$acl->setParticipant($participant);
$acl->setPermissionEdit($aclData->permissionEdit);
$acl->setPermissionShare($aclData->permissionShare);
$acl->setPermissionManage($aclData->permissionManage);
if ($participant) {
$return[] = $acl;
}
}
return $return;
}
private function replaceUsernames(string $text): string {
foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) {
$text = str_replace($trello, $nextcloud->getUID(), $text);
}
return $text;
}
public function getBoards(): array {
// Old format has just the raw board data, new one a key boards
$data = $this->getImportService()->getData();
return array_values((array)($data->boards ?? $data));
}
public function reset(): void {
parent::reset();
$this->tmpCards = [];
}
}

View File

@@ -397,4 +397,12 @@ class TrelloJsonService extends ABoardImportService {
$trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n";
} }
} }
public function getBoards(): array {
if ($this->getImportService()->getData()->boards) {
return $this->getImportService()->getData()->boards;
}
return [$this->getImportService()->getData()];
}
} }

View File

@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"uidRelation": {
"type": "object",
"comment": "Relationship between Trello and Nextcloud usernames",
"example": {
"johndoe": "admin"
}
},
"owner": {
"type": "string",
"required": true,
"comment": "Nextcloud owner username"
}
}
}

View File

@@ -0,0 +1,7 @@
{
"owner": "admin",
"color": "0800fd",
"uidRelation": {
"johndoe": "test-user-1"
}
}

748
tests/data/deck.json Normal file
View File

@@ -0,0 +1,748 @@
{
"version": "1.11.0-dev",
"boards": {
"188": {
"id": 188,
"title": "My test board",
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"color": "e0ed31",
"archived": false,
"labels": [
{
"id": 239,
"title": "L2",
"color": "31CC7C",
"boardId": 188,
"cardId": null,
"lastModified": 1689667435,
"ETag": "63b77251cca5a56fe74a97e4baeab59c"
},
{
"id": 240,
"title": "L4",
"color": "317CCC",
"boardId": 188,
"cardId": null,
"lastModified": 1689667442,
"ETag": "15dcabeb47583ce5398faaeb65f7a4b6"
},
{
"id": 241,
"title": "L1",
"color": "FF7A66",
"boardId": 188,
"cardId": null,
"lastModified": 1689667432,
"ETag": "7d58be91f19ebc4f94b352db8c76c056"
},
{
"id": 242,
"title": "L3",
"color": "F1DB50",
"boardId": 188,
"cardId": null,
"lastModified": 1689667440,
"ETag": "160253b9d33ae0a7a3af90e7d418ba60"
}
],
"acl": [],
"permissions": {
"PERMISSION_READ": true,
"PERMISSION_EDIT": true,
"PERMISSION_MANAGE": true,
"PERMISSION_SHARE": true
},
"users": [],
"shared": 0,
"stacks": {
"64": {
"id": 64,
"title": "A",
"boardId": 188,
"deletedAt": 0,
"lastModified": 1689667779,
"order": 999,
"ETag": "ddfd0c27e53d8db94ac5e9aaa021746e",
"cards": [
{
"id": 114,
"title": "1",
"description": "",
"stackId": 64,
"type": "plain",
"lastModified": 1689667779,
"lastEditor": null,
"createdAt": 1689667569,
"labels": [
{
"id": 239,
"title": "L2",
"color": "31CC7C",
"boardId": 188,
"cardId": 114,
"lastModified": 1689667435,
"ETag": "63b77251cca5a56fe74a97e4baeab59c"
}
],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": "2050-07-24T22:00:00+00:00",
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "ddfd0c27e53d8db94ac5e9aaa021746e",
"overdue": 0,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
},
{
"id": 115,
"title": "2",
"description": "",
"stackId": 64,
"type": "plain",
"lastModified": 1689667752,
"lastEditor": null,
"createdAt": 1689667572,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": "2023-07-17T02:00:00+00:00",
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "9a8ed495f7d83f8310ae6291d6dc4624",
"overdue": 3,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
},
{
"id": 116,
"title": "3",
"description": "",
"stackId": 64,
"type": "plain",
"lastModified": 1689667760,
"lastEditor": "admin",
"createdAt": 1689667576,
"labels": [],
"assignedUsers": [
{
"id": 5,
"participant": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"cardId": 116,
"type": 0
}
],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "f908c4359e9ca0703f50da2bbe967594",
"overdue": 0,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
}
]
},
"65": {
"id": 65,
"title": "B",
"boardId": 188,
"deletedAt": 0,
"lastModified": 1689667796,
"order": 999,
"ETag": "b97a2b19e1cafc8f95e3f4db71097214",
"cards": [
{
"id": 117,
"title": "4",
"description": "",
"stackId": 65,
"type": "plain",
"lastModified": 1689667767,
"lastEditor": "admin",
"createdAt": 1689667578,
"labels": [
{
"id": 239,
"title": "L2",
"color": "31CC7C",
"boardId": 188,
"cardId": 117,
"lastModified": 1689667435,
"ETag": "63b77251cca5a56fe74a97e4baeab59c"
},
{
"id": 240,
"title": "L4",
"color": "317CCC",
"boardId": 188,
"cardId": 117,
"lastModified": 1689667442,
"ETag": "15dcabeb47583ce5398faaeb65f7a4b6"
},
{
"id": 241,
"title": "L1",
"color": "FF7A66",
"boardId": 188,
"cardId": 117,
"lastModified": 1689667432,
"ETag": "7d58be91f19ebc4f94b352db8c76c056"
}
],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "6b20cc46fa5d2e5f65251526b50cc130",
"overdue": 0,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
},
{
"id": 118,
"title": "5",
"description": "",
"stackId": 65,
"type": "plain",
"lastModified": 1689667773,
"lastEditor": "admin",
"createdAt": 1689667581,
"labels": [
{
"id": 239,
"title": "L2",
"color": "31CC7C",
"boardId": 188,
"cardId": 118,
"lastModified": 1689667435,
"ETag": "63b77251cca5a56fe74a97e4baeab59c"
}
],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "488145982535a91d9ab47db647ecf539",
"overdue": 0,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
},
{
"id": 119,
"title": "6",
"description": "# Test description\n\nHello world",
"stackId": 65,
"type": "plain",
"lastModified": 1689667796,
"lastEditor": null,
"createdAt": 1689667583,
"labels": [],
"assignedUsers": [
{
"id": 6,
"participant": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"cardId": 119,
"type": 0
}
],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "b97a2b19e1cafc8f95e3f4db71097214",
"overdue": 0,
"boardId": 188,
"board": {
"id": 188,
"title": "My test board"
}
}
]
},
"66": {
"id": 66,
"title": "C",
"boardId": 188,
"deletedAt": 0,
"lastModified": 0,
"order": 999,
"ETag": "cfcd208495d565ef66e7dff9f98764da"
}
},
"activeSessions": [],
"deletedAt": 0,
"lastModified": 1689667796,
"settings": [],
"ETag": "b97a2b19e1cafc8f95e3f4db71097214"
},
"189": {
"id": 189,
"title": "Shared board",
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"color": "30b6d8",
"archived": false,
"labels": [
{
"id": 243,
"title": "Finished",
"color": "31CC7C",
"boardId": 189,
"cardId": null,
"lastModified": 1689667413,
"ETag": "aa71367f6a9a2fc2d47fc46163a30208"
},
{
"id": 244,
"title": "To review",
"color": "317CCC",
"boardId": 189,
"cardId": null,
"lastModified": 1689667413,
"ETag": "aa71367f6a9a2fc2d47fc46163a30208"
},
{
"id": 245,
"title": "Action needed",
"color": "FF7A66",
"boardId": 189,
"cardId": null,
"lastModified": 1689667413,
"ETag": "aa71367f6a9a2fc2d47fc46163a30208"
},
{
"id": 246,
"title": "Later",
"color": "F1DB50",
"boardId": 189,
"cardId": null,
"lastModified": 1689667413,
"ETag": "aa71367f6a9a2fc2d47fc46163a30208"
}
],
"acl": [
{
"id": 4,
"participant": {
"primaryKey": "alice",
"uid": "alice",
"displayname": "alice",
"type": 0
},
"type": 0,
"boardId": 189,
"permissionEdit": true,
"permissionShare": false,
"permissionManage": false,
"owner": false
},
{
"id": 5,
"participant": {
"primaryKey": "jane",
"uid": "jane",
"displayname": "jane",
"type": 0
},
"type": 0,
"boardId": 189,
"permissionEdit": false,
"permissionShare": true,
"permissionManage": false,
"owner": false
},
{
"id": 6,
"participant": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 1
},
"type": 1,
"boardId": 189,
"permissionEdit": false,
"permissionShare": false,
"permissionManage": true,
"owner": false
}
],
"permissions": {
"PERMISSION_READ": true,
"PERMISSION_EDIT": true,
"PERMISSION_MANAGE": true,
"PERMISSION_SHARE": true
},
"users": [],
"shared": 0,
"stacks": {
"61": {
"id": 61,
"title": "ToDo",
"boardId": 189,
"deletedAt": 0,
"lastModified": 1689667537,
"order": 999,
"ETag": "6c315c83f146485e6b2b6fdc24ffa617",
"cards": [
{
"id": 107,
"title": "Write tests",
"description": "",
"stackId": 61,
"type": "plain",
"lastModified": 1689667521,
"lastEditor": null,
"createdAt": 1689667483,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 0,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "f0450d41827f55580554c993304c8073",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
},
{
"id": 111,
"title": "Write blog post",
"description": "",
"stackId": 61,
"type": "plain",
"lastModified": 1689667521,
"lastEditor": null,
"createdAt": 1689667518,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 1,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "f0450d41827f55580554c993304c8073",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
},
{
"id": 112,
"title": "Announce feature",
"description": "",
"stackId": 61,
"type": "plain",
"lastModified": 1689667527,
"lastEditor": null,
"createdAt": 1689667527,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "1956848c45be91fefc967ee8831ea4cf",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
},
{
"id": 113,
"title": "\ud83c\udf89 Party",
"description": "",
"stackId": 61,
"type": "plain",
"lastModified": 1689667537,
"lastEditor": null,
"createdAt": 1689667537,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "6c315c83f146485e6b2b6fdc24ffa617",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
}
]
},
"62": {
"id": 62,
"title": "In progress",
"boardId": 189,
"deletedAt": 0,
"lastModified": 1689667502,
"order": 999,
"ETag": "1498939b8816e6041da80050dacc3ed3",
"cards": [
{
"id": 108,
"title": "Write feature",
"description": "",
"stackId": 62,
"type": "plain",
"lastModified": 1689667488,
"lastEditor": null,
"createdAt": 1689667488,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 999,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "d2a8b634cdd96ab5ef48910bbbd715b1",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
}
]
},
"63": {
"id": 63,
"title": "Done",
"boardId": 189,
"deletedAt": 0,
"lastModified": 1689667518,
"order": 999,
"ETag": "09ba5a39921de760db53bcd56457eea5",
"cards": [
{
"id": 109,
"title": "Plan feature",
"description": "",
"stackId": 63,
"type": "plain",
"lastModified": 1689667506,
"lastEditor": null,
"createdAt": 1689667493,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 0,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "193163d8a8acedbfaba196b1f0d65bc8",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
},
{
"id": 110,
"title": "Design feature",
"description": "",
"stackId": 63,
"type": "plain",
"lastModified": 1689667506,
"lastEditor": null,
"createdAt": 1689667502,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": null,
"owner": {
"primaryKey": "admin",
"uid": "admin",
"displayname": "admin",
"type": 0
},
"order": 1,
"archived": false,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "193163d8a8acedbfaba196b1f0d65bc8",
"overdue": 0,
"boardId": 189,
"board": {
"id": 189,
"title": "Shared board"
}
}
]
}
},
"activeSessions": [],
"deletedAt": 0,
"lastModified": 1689667537,
"settings": [],
"ETag": "6c315c83f146485e6b2b6fdc24ffa617"
}
}
}

View File

@@ -277,7 +277,7 @@ class TransferOwnershipTest extends \Test\TestCase {
// Arrange separate board next to the one being transferred // Arrange separate board next to the one being transferred
$board = $this->boardService->create('Test 2', self::TEST_USER_1, '000000'); $board = $this->boardService->create('Test 2', self::TEST_USER_1, '000000');
$id = $board->getId(); $id = $board->getId();
$this->boardService->addAcl($id, Acl::PERMISSION_TYPE_USER, self::TEST_USER_1, true, true, true); // $this->boardService->addAcl($id, Acl::PERMISSION_TYPE_USER, self::TEST_USER_1, true, true, true);
$this->boardService->addAcl($id, Acl::PERMISSION_TYPE_GROUP, self::TEST_GROUP, true, true, true); $this->boardService->addAcl($id, Acl::PERMISSION_TYPE_GROUP, self::TEST_GROUP, true, true, true);
$this->boardService->addAcl($id, Acl::PERMISSION_TYPE_USER, self::TEST_USER_3, false, true, false); $this->boardService->addAcl($id, Acl::PERMISSION_TYPE_USER, self::TEST_USER_3, false, true, false);
$stacks[] = $this->stackService->create('Stack A', $id, 1); $stacks[] = $this->stackService->create('Stack A', $id, 1);

View File

@@ -0,0 +1,415 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Db;
use OCA\Deck\Command\BoardImport;
use OCA\Deck\Command\UserExport;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\Importer\BoardImportService;
use OCA\Deck\Service\Importer\Systems\DeckJsonService;
use OCA\Deck\Service\PermissionService;
use OCA\Deck\Service\StackService;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\Entity;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\Server;
use PHPUnit\Framework\ExpectationFailedException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @group DB
*/
class ImportExportTest extends \Test\TestCase {
private IDBConnection $connection;
private const TEST_USER1 = 'test-export-user1';
private const TEST_USER3 = 'test-export-user3';
private const TEST_USER2 = 'test-export-user2';
private const TEST_USER4 = 'test-export-user4';
private const TEST_GROUP1 = 'test-export-group1';
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
$backend = new \Test\Util\User\Dummy();
\OC_User::useBackend($backend);
Server::get(IUserManager::class)->registerBackend($backend);
$backend->createUser('alice', 'alice');
$backend->createUser('jane', 'jane');
$backend->createUser('johndoe', 'johndoe');
$backend->createUser(self::TEST_USER1, self::TEST_USER1);
$backend->createUser(self::TEST_USER2, self::TEST_USER2);
$backend->createUser(self::TEST_USER3, self::TEST_USER3);
$backend->createUser(self::TEST_USER4, self::TEST_USER4);
// create group
$groupBackend = new \Test\Util\Group\Dummy();
$groupBackend->createGroup(self::TEST_GROUP1);
$groupBackend->createGroup('group');
$groupBackend->createGroup('group1');
$groupBackend->createGroup('group2');
$groupBackend->createGroup('group3');
$groupBackend->addToGroup(self::TEST_USER1, 'group');
$groupBackend->addToGroup(self::TEST_USER2, 'group');
$groupBackend->addToGroup(self::TEST_USER3, 'group');
$groupBackend->addToGroup(self::TEST_USER2, 'group1');
$groupBackend->addToGroup(self::TEST_USER3, 'group2');
$groupBackend->addToGroup(self::TEST_USER4, 'group3');
$groupBackend->addToGroup(self::TEST_USER2, self::TEST_GROUP1);
Server::get(IGroupManager::class)->addBackend($groupBackend);
Server::get(PermissionService::class)->setUserId('admin');
}
public function setUp(): void {
parent::setUp();
$this->connection = \OCP\Server::get(IDBConnection::class);
$this->cleanDb();
$this->cleanDb(self::TEST_USER1);
}
public function testImportOcc() {
$this->importFromFile(__DIR__ . '/../../data/deck.json');
$this->assertDatabase();
}
/**
* This test runs an import, export and another import and
* assert that multiple attempts result in the same data structure
*
* In addition, it asserts that multiple import/export runs result in the same JSON
*/
public function testReimportOcc() {
$this->importFromFile(__DIR__ . '/../../data/deck.json');
$this->assertDatabase();
$tmpExportFile = $this->exportToTemp();
// Useful for double checking differences as there is no easy way to compare equal with skipping certain id keys, etag
// self::assertEquals(file_get_contents(__DIR__ . '/../../data/deck.json'), $jsonOutput);
self::assertEquals(
self::writeArrayStructure(array: json_decode(file_get_contents(__DIR__ . '/../../data/deck.json'), true)),
self::writeArrayStructure(array: json_decode(file_get_contents($tmpExportFile), true))
);
// cleanup test database
$this->cleanDb();
// Re-import from temporary file
$this->importFromFile($tmpExportFile);
$this->assertDatabase();
$tmpExportFile2 = $this->exportToTemp();
self::assertEquals(
self::writeArrayStructure(array: json_decode(file_get_contents($tmpExportFile), true)),
self::writeArrayStructure(array: json_decode(file_get_contents($tmpExportFile2), true))
);
}
public static function writeArrayStructure(string $prefix = '', array $array = [], array $skipKeyList = ['id', 'boardId', 'cardId', 'stackId', 'ETag', 'permissions', 'shared', 'version']): string {
$output = '';
$arrayIsList = array_keys($array) === range(0, count($array) - 1);
foreach ($array as $key => $value) {
$tmpPrefix = $prefix;
if (in_array($key, $skipKeyList)) {
continue;
}
if (is_array($value)) {
if ($key === 'participant' || $key === 'owner') {
$output .= $tmpPrefix . $key . ' => ' . $value['primaryKey'] . PHP_EOL;
continue;
}
$tmpPrefix .= (!$arrayIsList && !is_numeric($key) ? $key : '!!!') . ' => ';
$output .= self::writeArrayStructure($tmpPrefix, $value, $skipKeyList);
} else {
$output .= $tmpPrefix . $key . ' => ' . $value . PHP_EOL;
}
}
return $output;
}
public function cleanDb(string $owner = 'admin'): void {
$this->connection->executeQuery('DELETE from oc_deck_boards;');
}
private function importFromFile(string $filePath): void {
$input = $this->createMock(InputInterface::class);
$input->expects($this->any())
->method('getOption')
->willReturnCallback(function ($arg) use ($filePath) {
return match ($arg) {
'system' => 'DeckJson',
'data' => $filePath,
'config' => __DIR__ . '/../../data/config-trelloJson.json',
};
});
$output = $this->createMock(OutputInterface::class);
$importer = self::getFreshService(BoardImport::class);
$application = new Application();
$importer->setApplication($application);
$importer->run($input, $output);
}
/** Returns the path of a deck export json */
private function exportToTemp(): string {
\OCP\Server::get(BoardMapper::class)->flushCache();
$application = new Application();
$input = $this->createMock(InputInterface::class);
$input->expects($this->any())
->method('getArgument')
->with('user-id')
->willReturn('admin');
$output = new BufferedOutput();
$exporter = new UserExport(
\OCP\Server::get(IAppManager::class),
self::getFreshService(BoardMapper::class),
self::getFreshService(BoardService::class),
self::getFreshService(StackMapper::class),
self::getFreshService(CardMapper::class),
self::getFreshService(AssignmentMapper::class),
);
$exporter->setApplication($application);
$exporter->run($input, $output);
$jsonOutput = $output->fetch();
json_decode($jsonOutput);
self::assertTrue(json_last_error() === JSON_ERROR_NONE);
$tmpExportFile = tempnam('/tmp', 'export');
file_put_contents($tmpExportFile, $jsonOutput);
return $tmpExportFile;
}
public function testImport() {
$importer = self::getFreshService(BoardImportService::class);
$deckJsonService = self::getFreshService(DeckJsonService::class);
$deckJsonService->setImportService($importer);
$importer->setSystem('DeckJson');
$importer->setImportSystem($deckJsonService);
$importer->setConfigInstance(json_decode(file_get_contents(__DIR__ . '/../../data/config-trelloJson.json')));
$importer->setData(json_decode(file_get_contents(__DIR__ . '/../../data/deck.json')));
$importer->import();
$this->assertDatabase();
}
public function testImportAsOtherUser() {
$importer = self::getFreshService(BoardImportService::class);
$deckJsonService = self::getFreshService(DeckJsonService::class);
$deckJsonService->setImportService($importer);
$importer->setSystem('DeckJson');
$importer->setImportSystem($deckJsonService);
$importer->setConfigInstance((object)[
'owner' => self::TEST_USER1
]);
$importer->setData(json_decode(file_get_contents(__DIR__ . '/../../data/deck.json')));
$importer->import();
$this->assertDatabase(self::TEST_USER1);
}
public function testImportWithRemap() {
$importer = self::getFreshService(BoardImportService::class);
$deckJsonService = self::getFreshService(DeckJsonService::class);
$deckJsonService->setImportService($importer);
$importer->setSystem('DeckJson');
$importer->setImportSystem($deckJsonService);
$importer->setConfigInstance((object)[
'owner' => self::TEST_USER1,
'uidRelation' => (object)[
'alice' => self::TEST_USER2,
'jane' => self::TEST_USER3,
],
]);
$importer->setData(json_decode(file_get_contents(__DIR__ . '/../../data/deck.json')));
$importer->import();
$this->assertDatabase(self::TEST_USER1);
$otherUserboards = self::getFreshService(BoardMapper::class)->findAllByUser(self::TEST_USER2);
self::assertCount(1, $otherUserboards);
}
/**
* @template T
* @param class-string<T>|string $className
* @return T
*/
private function getFreshService(string $className): mixed {
$fresh = \OC::$server->getRegisteredAppContainer('deck')->resolve($className);
self::overwriteService($className, $fresh);
return $fresh;
}
public function assertDatabase(string $owner = 'admin') {
$permissionService = self::getFreshService(PermissionService::class);
$permissionService->setUserId($owner);
self::getFreshService(BoardService::class);
self::getFreshService(CardService::class);
$boardMapper = self::getFreshService(BoardMapper::class);
$stackMapper = self::getFreshService(StackMapper::class);
$cardMapper = self::getFreshService(CardMapper::class);
$boards = $boardMapper->findAllByOwner($owner);
$boardNames = array_map(fn ($board) => $board->getTitle(), $boards);
self::assertEquals(2, count($boards));
$board = $boards[0];
self::assertEntity(Board::fromRow([
'title' => 'My test board',
'color' => 'e0ed31',
'owner' => $owner,
'lastModified' => 1689667796,
]), $board);
$boardService = $this->getFreshService(BoardService::class);
$fullBoard = $boardService->find($board->getId(), true);
self::assertEntityInArray(Label::fromParams([
'title' => 'L2',
'color' => '31CC7C',
]), $fullBoard->getLabels(), true);
$stacks = $stackMapper->findAll($board->getId());
self::assertCount(3, $stacks);
self::assertEntity(Stack::fromRow([
'title' => 'A',
'order' => 999,
'boardId' => $boards[0]->getId(),
'lastModified' => 1689667779,
]), $stacks[0]);
self::assertEntity(Stack::fromRow([
'title' => 'B',
'order' => 999,
'boardId' => $boards[0]->getId(),
'lastModified' => 1689667796,
]), $stacks[1]);
self::assertEntity(Stack::fromRow([
'title' => 'C',
'order' => 999,
'boardId' => $boards[0]->getId(),
'lastModified' => 0,
]), $stacks[2]);
$cards = $cardMapper->findAll($stacks[0]->getId());
self::assertEntity(Card::fromRow([
'title' => '1',
'description' => '',
'type' => 'plain',
'lastModified' => 1689667779,
'createdAt' => 1689667569,
'owner' => $owner,
'duedate' => new \DateTime('2050-07-24T22:00:00.000000+0000'),
'order' => 999,
'stackId' => $stacks[0]->getId(),
]), $cards[0]);
self::assertEntity(Card::fromRow([
'title' => '2',
'duedate' => new \DateTime('2050-07-24T22:00:00.000000+0000'),
]), $cards[1], true);
self::assertEntity(Card::fromParams([
'title' => '3',
'duedate' => null,
]), $cards[2], true);
$cards = $cardMapper->findAll($stacks[1]->getId());
self::assertEntity(Card::fromParams([
'title' => '6',
'duedate' => null,
'description' => "# Test description\n\nHello world",
]), $cards[2], true);
// Shared board
$sharedBoard = $boards[1];
self::assertEntity(Board::fromRow([
'title' => 'Shared board',
'color' => '30b6d8',
'owner' => $owner,
]), $sharedBoard, true);
$stackService = self::getFreshService(StackService::class);
$stacks = $stackService->findAll($board->getId());
self::assertEntityInArray(Label::fromParams([
'title' => 'L2',
'color' => '31CC7C',
]), $stacks[0]->getCards()[0]->getLabels(), true);
self::assertEntity(Label::fromParams([
'title' => 'L2',
'color' => '31CC7C',
]), $stacks[0]->getCards()[0]->getLabels()[0], true);
$stacks = $stackMapper->findAll($sharedBoard->getId());
self::assertCount(3, $stacks);
}
public static function assertEntityInArray(Entity $expected, array $array, bool $checkProperties): void {
$exists = null;
foreach ($array as $entity) {
try {
self::assertEntity($expected, $entity, $checkProperties);
$exists = $entity;
} catch (ExpectationFailedException $e) {
}
}
if ($exists) {
self::assertEntity($expected, $exists, $checkProperties);
} else {
// THis is hard to debug if it fails as the actual diff is not returned but hidden in the above exception
self::assertEquals($expected, $exists);
}
}
public static function assertEntity(Entity $expected, Entity $actual, bool $checkProperties = false): void {
if ($checkProperties === true) {
$e = clone $expected;
$a = clone $actual;
foreach ($e->getUpdatedFields() as $property => $updated) {
$expectedValue = call_user_func([$e, 'get' . ucfirst($property)]);
$actualValue = call_user_func([$a, 'get' . ucfirst($property)]);
self::assertEquals(
$expectedValue,
$actualValue
);
}
} else {
$e = clone $expected;
$e->setId(null);
$a = clone $actual;
$a->setId(null);
$e->resetUpdatedFields();
$a->resetUpdatedFields();
self::assertEquals($e, $a);
}
}
public function tearDown(): void {
$this->cleanDb();
$this->cleanDb(self::TEST_USER1);
parent::tearDown();
}
}

View File

@@ -12,5 +12,8 @@
<testsuite name="integration-app"> <testsuite name="integration-app">
<directory>./integration/app</directory> <directory>./integration/app</directory>
</testsuite> </testsuite>
<testsuite name="integration-import">
<directory>./integration/import</directory>
</testsuite>
</testsuites> </testsuites>
</phpunit> </phpunit>

View File

@@ -193,6 +193,8 @@ namespace Symfony\Component\Console\Output {
class OutputInterface { class OutputInterface {
public const VERBOSITY_VERBOSE = 1; public const VERBOSITY_VERBOSE = 1;
public function writeln($text, int $flat = 0) {} public function writeln($text, int $flat = 0) {}
public function isVerbose(): bool {}
public function isVeryVerbose(): bool {}
} }
} }

View File

@@ -31,12 +31,14 @@ use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Stack; use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper; use OCA\Deck\Db\StackMapper;
use OCA\Deck\Service\BoardService; use OCA\Deck\Service\BoardService;
use OCP\App\IAppManager;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IUserManager; use OCP\IUserManager;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class UserExportTest extends \Test\TestCase { class UserExportTest extends \Test\TestCase {
protected $appManager;
protected $boardMapper; protected $boardMapper;
protected $boardService; protected $boardService;
protected $stackMapper; protected $stackMapper;
@@ -45,10 +47,11 @@ class UserExportTest extends \Test\TestCase {
protected $userManager; protected $userManager;
protected $groupManager; protected $groupManager;
private $userExport; private UserExport $userExport;
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->appManager = $this->createMock(IAppManager::class);
$this->boardMapper = $this->createMock(BoardMapper::class); $this->boardMapper = $this->createMock(BoardMapper::class);
$this->boardService = $this->createMock(BoardService::class); $this->boardService = $this->createMock(BoardService::class);
$this->stackMapper = $this->createMock(StackMapper::class); $this->stackMapper = $this->createMock(StackMapper::class);
@@ -56,7 +59,7 @@ class UserExportTest extends \Test\TestCase {
$this->assignedUserMapper = $this->createMock(AssignmentMapper::class); $this->assignedUserMapper = $this->createMock(AssignmentMapper::class);
$this->userManager = $this->createMock(IUserManager::class); $this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class); $this->groupManager = $this->createMock(IGroupManager::class);
$this->userExport = new UserExport($this->boardMapper, $this->boardService, $this->stackMapper, $this->cardMapper, $this->assignedUserMapper, $this->userManager, $this->groupManager); $this->userExport = new UserExport($this->appManager, $this->boardMapper, $this->boardService, $this->stackMapper, $this->cardMapper, $this->assignedUserMapper, $this->userManager, $this->groupManager);
} }
public function getBoard($id) { public function getBoard($id) {
@@ -114,5 +117,6 @@ class UserExportTest extends \Test\TestCase {
->method('findAll') ->method('findAll')
->willReturn([]); ->willReturn([]);
$result = $this->invokePrivate($this->userExport, 'execute', [$input, $output]); $result = $this->invokePrivate($this->userExport, 'execute', [$input, $output]);
self::assertEquals(0, $result);
} }
} }

View File

@@ -43,6 +43,7 @@ use OCP\IDBConnection;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class BoardImportServiceTest extends \Test\TestCase { class BoardImportServiceTest extends \Test\TestCase {
/** @var IDBConnection|MockObject */ /** @var IDBConnection|MockObject */
@@ -92,7 +93,8 @@ class BoardImportServiceTest extends \Test\TestCase {
$this->attachmentMapper, $this->attachmentMapper,
$this->cardMapper, $this->cardMapper,
$this->commentsManager, $this->commentsManager,
$this->eventDispatcher $this->eventDispatcher,
$this->createMock(LoggerInterface::class),
); );
$this->boardImportService->setSystem('trelloJson'); $this->boardImportService->setSystem('trelloJson');
@@ -118,6 +120,9 @@ class BoardImportServiceTest extends \Test\TestCase {
$this->trelloJsonService $this->trelloJsonService
->method('getJsonSchemaPath') ->method('getJsonSchemaPath')
->willReturn($configFile); ->willReturn($configFile);
$this->trelloJsonService
->method('getBoards')
->willReturn([$data]);
$this->boardImportService->setImportSystem($this->trelloJsonService); $this->boardImportService->setImportSystem($this->trelloJsonService);
$owner = $this->createMock(IUser::class); $owner = $this->createMock(IUser::class);
@@ -142,6 +147,9 @@ class BoardImportServiceTest extends \Test\TestCase {
} }
public function testImportSuccess() { public function testImportSuccess() {
$this->userManager->method('userExists')
->willReturn(true);
$this->boardMapper $this->boardMapper
->expects($this->once()) ->expects($this->once())
->method('insert'); ->method('insert');
@@ -192,8 +200,7 @@ class BoardImportServiceTest extends \Test\TestCase {
->expects($this->once()) ->expects($this->once())
->method('insert'); ->method('insert');
$actual = $this->boardImportService->import(); $this->boardImportService->import();
self::assertTrue(true);
$this->assertNull($actual);
} }
} }

View File

@@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer\Systems;
use OCA\Deck\Service\Importer\BoardImportService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
use PHPUnit\Framework\MockObject\MockObject;
/**
* @group DB
*/
class DeckJsonServiceTest extends \Test\TestCase {
private DeckJsonService $service;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var IUserManager|MockObject */
private $userManager;
/** @var IL10N */
private $l10n;
public function setUp(): void {
$this->userManager = $this->createMock(IUserManager::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->l10n = $this->createMock(IL10N::class);
$this->service = new DeckJsonService(
$this->userManager,
$this->urlGenerator,
$this->l10n
);
}
public function testGetBoardWithNoName() {
$this->expectExceptionMessage('Invalid name of board');
$importService = $this->createMock(BoardImportService::class);
$this->service->setImportService($importService);
$this->service->getBoard();
}
public function testGetBoardWithSuccess() {
$importService = Server::get(BoardImportService::class);
$data = json_decode(file_get_contents(__DIR__ . '/../../../../data/deck.json'));
$importService->setData($data);
$configInstance = json_decode(file_get_contents(__DIR__ . '/../../../../data/config-deckJson.json'));
$importService->setConfigInstance($configInstance);
$owner = $this->createMock(IUser::class);
$owner
->method('getUID')
->willReturn('admin');
$importService->setConfig('owner', $owner);
$this->service->setImportService($importService);
$boards = $this->service->getBoards();
$importService->setData($boards[0]);
$actual = $this->service->getBoard();
$this->assertEquals('My test board', $actual->getTitle());
$this->assertEquals('admin', $actual->getOwner());
$this->assertEquals('e0ed31', $actual->getColor());
}
}