diff --git a/app/Repositories/ImportJob/ImportJobRepository.php b/app/Repositories/ImportJob/ImportJobRepository.php index c21cb4c030..c375c36bf1 100644 --- a/app/Repositories/ImportJob/ImportJobRepository.php +++ b/app/Repositories/ImportJob/ImportJobRepository.php @@ -30,6 +30,7 @@ use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournalMeta; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; +use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; use Illuminate\Support\Str; use Log; @@ -42,12 +43,12 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; */ class ImportJobRepository implements ImportJobRepositoryInterface { - /** @var User */ - private $user; - /** @var int */ - private $maxUploadSize; /** @var \Illuminate\Contracts\Filesystem\Filesystem */ protected $uploadDisk; + /** @var int */ + private $maxUploadSize; + /** @var User */ + private $user; public function __construct() { @@ -70,6 +71,24 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $this->setExtendedStatus($job, $extended); } + /** + * Add message to job. + * + * @param ImportJob $job + * @param string $error + * + * @return ImportJob + */ + public function addErrorMessage(ImportJob $job, string $error): ImportJob + { + $errors = $job->errors; + $errors[] = $error; + $job->errors = $errors; + $job->save(); + + return $job; + } + /** * @param ImportJob $job * @param int $steps @@ -176,6 +195,18 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $result; } + /** + * Return all attachments for job. + * + * @param ImportJob $job + * + * @return Collection + */ + public function getAttachments(ImportJob $job): Collection + { + return $job->attachments()->get(); + } + /** * Return configuration of job. * @@ -393,6 +424,20 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $this->setExtendedStatus($job, $status); } + /** + * @param ImportJob $job + * @param Tag $tag + * + * @return ImportJob + */ + public function setTag(ImportJob $job, Tag $tag): ImportJob + { + $job->tag()->associate($tag); + $job->save(); + + return $job; + } + /** * @param ImportJob $job * @param int $count @@ -408,43 +453,6 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $this->setExtendedStatus($job, $status); } - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - - /** - * @param ImportJob $job - * @param string $status - * - * @return ImportJob - */ - public function updateStatus(ImportJob $job, string $status): ImportJob - { - $job->status = $status; - $job->save(); - - return $job; - } - - /** - * Return import file content. - * - * @deprecated - * - * @param ImportJob $job - * - * @return string - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function uploadFileContents(ImportJob $job): string - { - return $job->uploadFileContents(); - } - /** * @param ImportJob $job * @param array $transactions @@ -460,52 +468,13 @@ class ImportJobRepository implements ImportJobRepositoryInterface } /** - * Add message to job. - * - * @param ImportJob $job - * @param string $error - * - * @return ImportJob + * @param User $user */ - public function addErrorMessage(ImportJob $job, string $error): ImportJob + public function setUser(User $user) { - $errors = $job->errors; - $errors[] = $error; - $job->errors = $errors; - $job->save(); - - return $job; + $this->user = $user; } - /** - * @param ImportJob $job - * @param Tag $tag - * - * @return ImportJob - */ - public function setTag(ImportJob $job, Tag $tag): ImportJob - { - $job->tag()->associate($tag); - $job->save(); - - return $job; - } - - /** - * @codeCoverageIgnore - * - * @param UploadedFile $file - * - * @return bool - */ - protected function validSize(UploadedFile $file): bool - { - $size = $file->getSize(); - - return $size > $this->maxUploadSize; - } - - /** * Handle upload for job. * @@ -526,7 +495,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $messages; } $count = $job->attachments()->get()->filter( - function (Attachment $att) use($name) { + function (Attachment $att) use ($name) { return $att->filename === $name; } )->count(); @@ -560,4 +529,47 @@ class ImportJobRepository implements ImportJobRepositoryInterface // return it. return new MessageBag; } + + /** + * @param ImportJob $job + * @param string $status + * + * @return ImportJob + */ + public function updateStatus(ImportJob $job, string $status): ImportJob + { + $job->status = $status; + $job->save(); + + return $job; + } + + /** + * Return import file content. + * + * @deprecated + * + * @param ImportJob $job + * + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function uploadFileContents(ImportJob $job): string + { + return $job->uploadFileContents(); + } + + /** + * @codeCoverageIgnore + * + * @param UploadedFile $file + * + * @return bool + */ + protected function validSize(UploadedFile $file): bool + { + $size = $file->getSize(); + + return $size > $this->maxUploadSize; + } } diff --git a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php index 0bfc61e75e..8e5fba8756 100644 --- a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php +++ b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php @@ -26,6 +26,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; use FireflyIII\Models\Tag; use FireflyIII\User; +use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -34,6 +35,14 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; */ interface ImportJobRepositoryInterface { + /** + * Return all attachments for job. + * + * @param ImportJob $job + * + * @return Collection + */ + public function getAttachments(ImportJob $job): Collection; /** * Handle upload for job. diff --git a/app/Support/Import/Configuration/File/ConfigureMappingHandler.php b/app/Support/Import/Configuration/File/ConfigureMappingHandler.php index caad8cdc1f..651db8cc0f 100644 --- a/app/Support/Import/Configuration/File/ConfigureMappingHandler.php +++ b/app/Support/Import/Configuration/File/ConfigureMappingHandler.php @@ -26,7 +26,6 @@ namespace FireflyIII\Support\Import\Configuration\File; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Import\Mapper\MapperInterface; -use FireflyIII\Import\MapperPreProcess\PreProcessorInterface; use FireflyIII\Import\Specifics\SpecificInterface; use FireflyIII\Models\Attachment; use FireflyIII\Models\ImportJob; @@ -52,86 +51,21 @@ class ConfigureMappingHandler implements ConfigurationInterface /** @var ImportJobRepositoryInterface */ private $repository; - /** - * Store data associated with current stage. - * - * @param array $data - * - * @return MessageBag - */ - public function configureJob(array $data): MessageBag - { - $config = $this->importJob->configuration; - - if (isset($data['mapping']) && \is_array($data['mapping'])) { - foreach ($data['mapping'] as $index => $array) { - $config['column-mapping-config'][$index] = []; - foreach ($array as $value => $mapId) { - $mapId = (int)$mapId; - if (0 !== $mapId) { - $config['column-mapping-config'][$index][$value] = $mapId; - } - } - } - } - $this->repository->setConfiguration($this->importJob, $config); - $this->repository->setStage($this->importJob, 'ready_to_run'); - - return new MessageBag; - } - - /** - * Get the data necessary to show the configuration screen. - * - * @return array - * @throws FireflyException - */ - public function getNextData(): array - { - $config = $this->importJob->configuration; - $columnConfig = $this->doColumnConfig($config); - - // in order to actually map we also need to read the FULL file. - try { - $reader = $this->getReader(); - } catch (Exception $e) { - Log::error($e->getMessage()); - throw new FireflyException('Cannot get reader: ' . $e->getMessage()); - } - - // get ALL values for the mappable columns from the CSV file: - $columnConfig = $this->getValuesForMapping($reader, $config, $columnConfig); - - return $columnConfig; - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job): void - { - $this->importJob = $job; - $this->repository = app(ImportJobRepositoryInterface::class); - $this->repository->setUser($job->user); - $this->attachments = app(AttachmentHelperInterface::class); - $this->columnConfig = []; - } - /** * Apply the users selected specifics on the current row. * * @param array $config - * @param array $validSpecifics * @param array $row * * @return array */ - private function applySpecifics(array $config, array $validSpecifics, array $row): array + public function applySpecifics(array $config, array $row): array { // run specifics here: // and this is the point where the specifix go to work. - $specifics = $config['specifics'] ?? []; - $names = array_keys($specifics); + $validSpecifics = array_keys(config('csv.import_specifics')); + $specifics = $config['specifics'] ?? []; + $names = array_keys($specifics); foreach ($names as $name) { if (!\in_array($name, $validSpecifics)) { continue; @@ -148,23 +82,31 @@ class ConfigureMappingHandler implements ConfigurationInterface } /** - * Create the "mapper" class that will eventually return the correct data for the user - * to map against. For example: a list of asset accounts. A list of budgets. A list of tags. + * Store data associated with current stage. * - * @param string $column + * @param array $data * - * @return MapperInterface - * @throws FireflyException + * @return MessageBag */ - private function createMapper(string $column): MapperInterface + public function configureJob(array $data): MessageBag { - $mapperClass = config('csv.import_roles.' . $column . '.mapper'); - $mapperName = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass); - if (!class_exists($mapperName)) { - throw new FireflyException(sprintf('Class "%s" does not exist. Cannot map "%s"', $mapperName, $column)); - } + $config = $this->importJob->configuration; - return app($mapperName); + if (isset($data['mapping']) && \is_array($data['mapping'])) { + foreach ($data['mapping'] as $index => $array) { + $config['column-mapping-config'][$index] = []; + foreach ($array as $value => $mapId) { + $mapId = (int)$mapId; + if (0 !== $mapId) { + $config['column-mapping-config'][$index][(string)$value] = $mapId; + } + } + } + } + $this->repository->setConfiguration($this->importJob, $config); + $this->repository->setStage($this->importJob, 'ready_to_run'); + + return new MessageBag; } /** @@ -178,7 +120,7 @@ class ConfigureMappingHandler implements ConfigurationInterface * @return array the column configuration. * @throws FireflyException */ - private function doColumnConfig(array $config): array + public function doColumnConfig(array $config): array { /** @var array $requestMapping */ $requestMapping = $config['column-do-mapping'] ?? []; @@ -217,11 +159,36 @@ class ConfigureMappingHandler implements ConfigurationInterface * * @return bool */ - private function doMapOfColumn(string $name, bool $requested): bool + public function doMapOfColumn(string $name, bool $requested): bool { $canBeMapped = config('csv.import_roles.' . $name . '.mappable'); - return $canBeMapped && $requested; + return $canBeMapped === true && $requested === true; + } + + /** + * Get the data necessary to show the configuration screen. + * + * @return array + * @throws FireflyException + */ + public function getNextData(): array + { + $config = $this->importJob->configuration; + $columnConfig = $this->doColumnConfig($config); + + // in order to actually map we also need to read the FULL file. + try { + $reader = $this->getReader(); + } catch (Exception $e) { + Log::error($e->getMessage()); + throw new FireflyException('Cannot get reader: ' . $e->getMessage()); + } + + // get ALL values for the mappable columns from the CSV file: + $columnConfig = $this->getValuesForMapping($reader, $config, $columnConfig); + + return $columnConfig; } /** @@ -233,7 +200,7 @@ class ConfigureMappingHandler implements ConfigurationInterface * * @return string */ - private function getPreProcessorName(string $column): string + public function getPreProcessorName(string $column): string { $name = ''; $hasPreProcess = config(sprintf('csv.import_roles.%s.pre-process-map', $column)); @@ -251,11 +218,11 @@ class ConfigureMappingHandler implements ConfigurationInterface * * @throws \League\Csv\Exception */ - private function getReader(): Reader + public function getReader(): Reader { $content = ''; /** @var Collection $collection */ - $collection = $this->importJob->attachments; + $collection = $this->repository->getAttachments($this->importJob); /** @var Attachment $attachment */ foreach ($collection as $attachment) { if ($attachment->filename === 'import_file') { @@ -283,53 +250,45 @@ class ConfigureMappingHandler implements ConfigurationInterface * @return array * @throws FireflyException */ - private function getValuesForMapping(Reader $reader, array $config, array $columnConfig): array + public function getValuesForMapping(Reader $reader, array $config, array $columnConfig): array { $offset = isset($config['has-headers']) && $config['has-headers'] === true ? 1 : 0; try { $stmt = (new Statement)->offset($offset); + // @codeCoverageIgnoreStart } catch (Exception $e) { throw new FireflyException(sprintf('Could not create reader: %s', $e->getMessage())); } - $results = $stmt->process($reader); - $validSpecifics = array_keys(config('csv.import_specifics')); - $validIndexes = array_keys($columnConfig); // the actually columns that can be mapped. - foreach ($results as $rowIndex => $row) { - $row = $this->applySpecifics($config, $validSpecifics, $row); + // @codeCoverageIgnoreEnd + $results = $stmt->process($reader); + $mappableColumns = array_keys($columnConfig); // the actually columns that can be mapped. + foreach ($results as $lineIndex => $line) { + Log::debug(sprintf('Trying to collect values for line #%d', $lineIndex)); + $line = $this->applySpecifics($config, $line); - //do something here - /** @var int $currentIndex */ - foreach ($validIndexes as $currentIndex) { // this is simply 1, 2, 3, etc. - if (!isset($row[$currentIndex])) { + /** @var int $columnIndex */ + foreach ($mappableColumns as $columnIndex) { // this is simply 1, 2, 3, etc. + if (!isset($line[$columnIndex])) { // don't need to handle this. Continue. continue; } - $value = trim($row[$currentIndex]); + $value = trim($line[$columnIndex]); if (\strlen($value) === 0) { + // value is empty, ignore it. continue; } - // we can do some preprocessing here, - // which is exclusively to fix the tags: - if (null !== $columnConfig[$currentIndex]['preProcessMap'] && \strlen($columnConfig[$currentIndex]['preProcessMap']) > 0) { - /** @var PreProcessorInterface $preProcessor */ - $preProcessor = app($columnConfig[$currentIndex]['preProcessMap']); - $result = $preProcessor->run($value); - // can merge array, this is usually the case: - $columnConfig[$currentIndex]['values'] = array_merge($columnConfig[$currentIndex]['values'], $result); - continue; - } - $columnConfig[$currentIndex]['values'][] = $value; + $columnConfig[$columnIndex]['values'][] = $value; } } // loop array again. This time, do uniqueness. // and remove arrays that have 0 values. - foreach ($validIndexes as $currentIndex) { - $columnConfig[$currentIndex]['values'] = array_unique($columnConfig[$currentIndex]['values']); - asort($columnConfig[$currentIndex]['values']); + foreach ($mappableColumns as $columnIndex) { + $columnConfig[$columnIndex]['values'] = array_unique($columnConfig[$columnIndex]['values']); + asort($columnConfig[$columnIndex]['values']); // if the count of this array is zero, there is nothing to map. - if (\count($columnConfig[$currentIndex]['values']) === 0) { - unset($columnConfig[$currentIndex]); + if (\count($columnConfig[$columnIndex]['values']) === 0) { + unset($columnConfig[$columnIndex]); } } @@ -344,7 +303,7 @@ class ConfigureMappingHandler implements ConfigurationInterface * * @return string */ - private function sanitizeColumnName(string $name): string + public function sanitizeColumnName(string $name): string { /** @var array $validColumns */ $validColumns = array_keys(config('csv.import_roles')); @@ -354,4 +313,36 @@ class ConfigureMappingHandler implements ConfigurationInterface return $name; } + + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job): void + { + $this->importJob = $job; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($job->user); + $this->attachments = app(AttachmentHelperInterface::class); + $this->columnConfig = []; + } + + /** + * Create the "mapper" class that will eventually return the correct data for the user + * to map against. For example: a list of asset accounts. A list of budgets. A list of tags. + * + * @param string $column + * + * @return MapperInterface + * @throws FireflyException + */ + private function createMapper(string $column): MapperInterface + { + $mapperClass = config('csv.import_roles.' . $column . '.mapper'); + $mapperName = sprintf('FireflyIII\\Import\Mapper\\%s', $mapperClass); + if (!class_exists($mapperName)) { + throw new FireflyException(sprintf('Class "%s" does not exist. Cannot map "%s"', $mapperName, $column)); // @codeCoverageIgnore + } + + return app($mapperName); + } } \ No newline at end of file diff --git a/tests/Unit/Support/Import/Configuration/File/ConfigureMappingHandlerTest.php b/tests/Unit/Support/Import/Configuration/File/ConfigureMappingHandlerTest.php new file mode 100644 index 0000000000..394fc5bc51 --- /dev/null +++ b/tests/Unit/Support/Import/Configuration/File/ConfigureMappingHandlerTest.php @@ -0,0 +1,488 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Support\Import\Configuration\File; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Import\Mapper\Budgets; +use FireflyIII\Import\Specifics\IngDescription; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler; +use Illuminate\Support\Collection; +use League\Csv\Exception; +use League\Csv\Reader; +use Mockery; +use Tests\TestCase; + +/** + * Class ConfigureMappingHandlerTest + * + * @package Tests\Unit\Support\Import\Configuration\File + */ +class ConfigureMappingHandlerTest extends TestCase +{ + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testApplySpecifics(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapG' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $expected = ['a' => 'ING']; + + // mock ING description (see below) + $ingDescr = $this->mock(IngDescription::class); + $ingDescr->shouldReceive('run')->once()->andReturn($expected); + + $config = [ + 'specifics' => [ + 'IngDescription' => 1, + 'bad-specific' => 1, + ], + ]; + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + $result = $handler->applySpecifics($config, []); + $this->assertEquals($expected, $result); + + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testConfigureJob(): void + { + // create fake input for class method: + $input = [ + 'mapping' => [ + + 0 => [// column + 'fake-iban' => 1, + 'other-fake-value' => '2', // string + ], + 1 => [ + 3 => 2, // fake number + 'final-fake-value' => 3, + 'mapped-to-zero' => 0, + ], + + ], + ]; + $expectedResult = [ + 'column-mapping-config' => + [ + 0 => [ + 'fake-iban' => 1, + 'other-fake-value' => 2, + ], + 1 => [ + '3' => 2, + 'final-fake-value' => 3, + ], + ], + + ]; + + + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapA' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + + // mock repos + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // run configure mapping handler. + // expect specific results: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStage')->once()->withArgs([Mockery::any(), 'ready_to_run']); + $repository->shouldReceive('setConfiguration')->once()->withArgs([Mockery::any(), $expectedResult]); + + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + $handler->configureJob($input); + + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testDoColumnConfig(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapE' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $fakeBudgets = [ + 0 => 'dont map', + 1 => 'Fake budget A', + 4 => 'Other fake budget', + ]; + + // fake budget mapper (see below) + $budgetMapper = $this->mock(Budgets::class); + $budgetMapper->shouldReceive('getMap')->once()->andReturn($fakeBudgets); + + // input array: + $input = [ + 'column-roles' => [ + 0 => 'description', // cannot be mapped + 1 => 'sepa-ct-id', // cannot be mapped + 2 => 'tags-space', // cannot be mapped, has a pre-processor. + 3 => 'account-id', // can be mapped + 4 => 'budget-id' // can be mapped. + ], + 'column-do-mapping' => [ + 0 => false, // don't try to map description + 1 => true, // try to map sepa (cannot) + 2 => true, // try to map tags (cannot) + 3 => false, // dont map mappable + 4 => true, // want to map, AND can map. + ], + ]; + + $expected = [ + 4 => [ + 'name' => 'budget-id', + 'options' => $fakeBudgets, + 'preProcessMap' => '', + 'values' => [], + ], + ]; + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + try { + $result = $handler->doColumnConfig($input); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + + $this->assertEquals($expected, $result); + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testDoMapOfColumn(): void + { + + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapC' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $combinations = [ + ['role' => 'description', 'expected' => false, 'requested' => false], // description cannot be mapped. Will always return false. + ['role' => 'description', 'expected' => false, 'requested' => true], // description cannot be mapped. Will always return false. + ['role' => 'currency-id', 'expected' => false, 'requested' => false], // if not requested, return false. + ['role' => 'currency-id', 'expected' => true, 'requested' => true], // if requested, return true. + ]; + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + foreach ($combinations as $info) { + $this->assertEquals($info['expected'], $handler->doMapOfColumn($info['role'], $info['requested'])); + } + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testGetNextData(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapH' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = [ + 'column-roles' => [ + 0 => 'description', // cannot be mapped + 1 => 'sepa-ct-id', // cannot be mapped + 2 => 'tags-space', // cannot be mapped, has a pre-processor. + 3 => 'account-id', // can be mapped + 4 => 'budget-id' // can be mapped. + ], + 'column-do-mapping' => [ + 0 => false, // don't try to map description + 1 => true, // try to map sepa (cannot) + 2 => true, // try to map tags (cannot) + 3 => false, // dont map mappable + 4 => true, // want to map, AND can map. + ], + 'delimiter' => ',', + ]; + $job->save(); + + // make one attachment. + $att = new Attachment; + $att->filename = 'import_file'; + $att->user_id = $this->user()->id; + $att->attachable_id = $job->id; + $att->attachable_type = Attachment::class; + $att->md5 = md5('hello'); + $att->mime = 'fake'; + $att->size = 3; + $att->save(); + + // fake some data. + $fileContent = "column1,column2,column3,column4,column5\nvalue1,value2,value3,value4,value5"; + $fakeBudgets = [ + 0 => 'dont map', + 1 => 'Fake budget A', + 4 => 'Other fake budget', + ]; + // mock some helpers: + $attachments = $this->mock(AttachmentHelperInterface::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + $repository->shouldReceive('getConfiguration')->once()->withArgs([Mockery::any()])->andReturn($job->configuration); + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('getAttachments')->once()->withArgs([Mockery::any()])->andReturn(new Collection([$att])); + $attachments->shouldReceive('getAttachmentContent')->withArgs([Mockery::any()])->andReturn($fileContent); + $budgetMapper = $this->mock(Budgets::class); + $budgetMapper->shouldReceive('getMap')->once()->andReturn($fakeBudgets); + + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + try { + $result = $handler->getNextData(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + $expected = [ + 4 => [ // is the one with the budget id, remember? + 'name' => 'budget-id', + 'options' => $fakeBudgets, + 'preProcessMap' => '', + 'values' => ['column5', 'value5'], // see $fileContent + ], + ]; + + $this->assertEquals($expected, $result); + + + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testGetPreProcessorName(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapD' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $combinations = [ + ['role' => 'tags-space', 'expected' => '\\FireflyIII\\Import\\MapperPreProcess\\TagsSpace'], // tags- space has a pre-processor. Return it. + ['role' => 'description', 'expected' => ''], // description has not. + ['role' => 'no-such-role', 'expected' => ''], // not existing role has not. + ]; + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + foreach ($combinations as $info) { + $this->assertEquals($info['expected'], $handler->getPreProcessorName($info['role'])); + } + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testGetReader(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapF' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // make one attachment. + $att = new Attachment; + $att->filename = 'import_file'; + $att->user_id = $this->user()->id; + $att->attachable_id = $job->id; + $att->attachable_type = Attachment::class; + $att->md5 = md5('hello'); + $att->mime = 'fake'; + $att->size = 3; + $att->save(); + $config = [ + 'delimiter' => ',', + ]; + + $fileContent = "column1,column2,column3\nvalue1,value2,value3"; + + // mock some helpers: + $attachments = $this->mock(AttachmentHelperInterface::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + $repository->shouldReceive('getConfiguration')->once()->withArgs([Mockery::any()])->andReturn($config); + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('getAttachments')->once()->withArgs([Mockery::any()])->andReturn(new Collection([$att])); + $attachments->shouldReceive('getAttachmentContent')->withArgs([Mockery::any()])->andReturn($fileContent); + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + try { + $reader = $handler->getReader(); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testGetValuesForMapping(): void + { + // create a reader to use in method. + // 5 columns, of which #4 (index 3) is budget-id + // 5 columns, of which #5 (index 4) is tags-space + $file = "value1,value2,value3,1,some tags here\nvalue4,value5,value6,2,more tags there\nvalueX,valueY,valueZ\nA,B,C,,\nd,e,f,1,xxx"; + $reader = Reader::createFromString($file); + + // make config for use in method. + $config = [ + 'has-headers' => false, + ]; + + // make column config + $columnConfig = [ + 3 => [ + 'name' => 'budget-id', + 'options' => [ + 0 => 'dont map', + 1 => 'Fake budget A', + 4 => 'Other fake budget', + ], + 'preProcessMap' => '', + 'values' => [], + ], + ]; + + // expected result + $expected = [ + 3 => [ + 'name' => 'budget-id', + 'options' => [ + 0 => 'dont map', + 1 => 'Fake budget A', + 4 => 'Other fake budget', + ], + 'preProcessMap' => '', + 'values' => ['1', '2'] // all values from column 3 of "CSV" file, minus double values + ], + ]; + + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapB' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + $result = []; + try { + $result = $handler->getValuesForMapping($reader, $config, $columnConfig); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertEquals($expected, $result); + + + } + + /** + * @covers \FireflyIII\Support\Import\Configuration\File\ConfigureMappingHandler + */ + public function testSanitizeColumnName(): void + { + + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'mapB' . random_int(1, 1000); + $job->status = 'new'; + $job->stage = 'new'; + $job->provider = 'fake'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + $handler = new ConfigureMappingHandler; + $handler->setJob($job); + $keys = array_keys(config('csv.import_roles')); + foreach ($keys as $key) { + $this->assertEquals($key, $handler->sanitizeColumnName($key)); + } + $this->assertEquals('_ignore', $handler->sanitizeColumnName('some-bad-name')); + } + +} \ No newline at end of file diff --git a/tests/Unit/TransactionRules/Triggers/HasNoCategoryTest.php b/tests/Unit/TransactionRules/Triggers/HasNoCategoryTest.php index 18875e841c..8eae77fd89 100644 --- a/tests/Unit/TransactionRules/Triggers/HasNoCategoryTest.php +++ b/tests/Unit/TransactionRules/Triggers/HasNoCategoryTest.php @@ -75,12 +75,16 @@ class HasNoCategoryTest extends TestCase */ public function testTriggeredTransaction() { - $journal = TransactionJournal::inRandomOrder()->whereNull('deleted_at')->first(); + $count = 0; + while ($count === 0) { + $journal = TransactionJournal::inRandomOrder()->whereNull('deleted_at')->first(); + $count = $journal->transactions()->count(); + } $transaction = $journal->transactions()->first(); $category = $journal->user->categories()->first(); $journal->categories()->detach(); - $transaction->categories()->save($category); + $transaction->categories()->sync([$category->id]); $this->assertEquals(0, $journal->categories()->count()); $this->assertEquals(1, $transaction->categories()->count());