From e33bbc6f16c18942d4f4c3e5173a1db7a58e65ea Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 30 Sep 2018 11:57:51 +0200 Subject: [PATCH] Expand test coverage. --- app/Helpers/Attachments/AttachmentHelper.php | 4 +- .../Json/AutoCompleteController.php | 527 +++++------------- app/Http/Requests/SplitJournalFormRequest.php | 2 + app/Import/Storage/ImportArrayStorage.php | 4 +- .../ExecuteRuleOnExistingTransactions.php | 4 - routes/web.php | 14 +- .../Json/AutoCompleteControllerTest.php | 119 +++- .../Prerequisites/YnabPrerequisitesTest.php | 161 ++++++ tests/Unit/Import/Routine/YnabRoutineTest.php | 319 +++++++++++ 9 files changed, 746 insertions(+), 408 deletions(-) create mode 100644 tests/Unit/Import/Prerequisites/YnabPrerequisitesTest.php create mode 100644 tests/Unit/Import/Routine/YnabRoutineTest.php diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index 637a74045f..09f22838f1 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -192,7 +192,7 @@ class AttachmentHelper implements AttachmentHelperInterface public function saveAttachmentsForModel(object $model, ?array $files): bool { if(!($model instanceof Model)) { - return false; + return false; // @codeCoverageIgnore } Log::debug(sprintf('Now in saveAttachmentsForModel for model %s', \get_class($model))); if (\is_array($files)) { @@ -270,7 +270,7 @@ class AttachmentHelper implements AttachmentHelperInterface $fileObject->rewind(); if(0 === $file->getSize()) { - throw new FireflyException('Cannot upload empty or non-existent file.'); + throw new FireflyException('Cannot upload empty or non-existent file.'); // @codeCoverageIgnore } $content = $fileObject->fread($file->getSize()); diff --git a/app/Http/Controllers/Json/AutoCompleteController.php b/app/Http/Controllers/Json/AutoCompleteController.php index f441948828..2fa745ba89 100644 --- a/app/Http/Controllers/Json/AutoCompleteController.php +++ b/app/Http/Controllers/Json/AutoCompleteController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; @@ -37,58 +38,16 @@ use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Collection; /** - * TODO refactor so each auto-complete thing is a function call because lots of code duplication. * Class AutoCompleteController. * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AutoCompleteController extends Controller { - /** - * Returns a JSON list of all accounts. - * - * @param Request $request - * @param AccountRepositoryInterface $repository - * - * @return JsonResponse - */ - public function allAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-all-accounts'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); - } - // find everything: - $return = array_values( - array_unique( - $repository->getAccountsByType( - [AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET] - )->pluck('name')->toArray() - ) - ); - if ('' !== $search) { - $return = array_values( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ); - } - $cache->store($return); - - return response()->json($return); - } - /** * List of all journals. * @@ -106,7 +65,7 @@ class AutoCompleteController extends Controller $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $cache->addProperty($key); if ($cache->has()) { - return response()->json($cache->get()); + return response()->json($cache->get()); // @codeCoverageIgnore } // find everything: $collector->setLimit(250)->setPage(1); @@ -129,251 +88,85 @@ class AutoCompleteController extends Controller } /** - * List of revenue accounts. - * - * @param Request $request - * @param AccountRepositoryInterface $repository + * @param Request $request + * @param string $subject * + * @throws FireflyException * @return JsonResponse */ - public function assetAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse + public function autoComplete(Request $request, string $subject): JsonResponse { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-asset-accounts'); + $search = (string)$request->get('search'); + $unfiltered = null; + $filtered = null; + $cache = new CacheProperties; + $cache->addProperty($subject); // very unlikely a user will actually search for this string. $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $cache->addProperty($key); if ($cache->has()) { - return response()->json($cache->get()); + return response()->json($cache->get()); // @codeCoverageIgnore } - // find everything: - $set = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - $filtered = $set->filter( - function (Account $account) { - if (true === $account->active) { - return $account; - } - - return false; // @codeCoverageIgnore - } - ); - $return = array_values(array_unique($filtered->pluck('name')->toArray())); - - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) + // search for all accounts. + if ('all-accounts' === $subject) { + $unfiltered = $this->getAccounts( + [AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, + AccountType::DEBT, AccountType::MORTGAGE] ); } - $cache->store($return); - return response()->json($return); - } - - /** - * Returns a JSON list of all bills. - * - * @param Request $request - * @param BillRepositoryInterface $repository - * - * @return JsonResponse - */ - public function bills(Request $request, BillRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-bills'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + // search for expense accounts. + if ('expense-accounts' === $subject) { + $unfiltered = $this->getAccounts([AccountType::EXPENSE, AccountType::BENEFICIARY]); } - // find everything: - $return = array_unique($repository->getActiveBills()->pluck('name')->toArray()); - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); + // search for revenue accounts. + if ('revenue-accounts' === $subject) { + $unfiltered = $this->getAccounts([AccountType::REVENUE]); } - $cache->store($return); - return response()->json($return); - } - - /** - * List of budgets. - * - * @param Request $request - * @param BudgetRepositoryInterface $repository - * - * @return JsonResponse - */ - public function budgets(Request $request, BudgetRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-budgets'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + // search for asset accounts. + if ('asset-accounts' === $subject) { + $unfiltered = $this->getAccounts([AccountType::ASSET, AccountType::DEFAULT]); } - // find everything: - $return = array_unique($repository->getBudgets()->pluck('name')->toArray()); - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); + // search for categories. + if ('categories' === $subject) { + $unfiltered = $this->getCategories(); } - $cache->store($return); - return response()->json($return); - } - - /** - * Returns a list of categories. - * - * @param Request $request - * @param CategoryRepositoryInterface $repository - * - * @return JsonResponse - */ - public function categories(Request $request, CategoryRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-categories'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + // search for budgets. + if ('budgets' === $subject) { + $unfiltered = $this->getBudgets(); } - // find everything: - $return = array_unique($repository->getCategories()->pluck('name')->toArray()); - if ('' !== $search) { - $return = array_values( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ); + + // search for tags + if ('tags' === $subject) { + $unfiltered = $this->getTags(); } - $cache->store($return); - return response()->json($return); - } - - /** - * List of currency names. - * - * @param Request $request - * @param CurrencyRepositoryInterface $repository - * - * @return JsonResponse - */ - public function currencyNames(Request $request, CurrencyRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-currency-names'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + // search for bills + if ('bills' === $subject) { + $unfiltered = $this->getBills(); } - // find everything: - $return = $repository->get()->pluck('name')->toArray(); - sort($return); - - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); + // search for currency names. + if ('currency-names' === $subject) { + $unfiltered = $this->getCurrencyNames(); } - $cache->store($return); - - return response()->json($return); - } - - /** - * Returns a JSON list of all beneficiaries. - * - * @param Request $request - * @param AccountRepositoryInterface $repository - * - * @return JsonResponse - */ - public function expenseAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-expense-accounts'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + if ('transaction_types' === $subject) { + $unfiltered = $this->getTransactionTypes(); } - // find everything: - $set = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $filtered = $set->filter( - function (Account $account) { - if (true === $account->active) { - return $account; - } - return false; - } - ); - $return = array_unique($filtered->pluck('name')->toArray()); - sort($return); + // filter results + $filtered = $this->filterResult($unfiltered, $search); - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); + if (null === $filtered) { + throw new FireflyException(sprintf('Auto complete handler cannot handle "%s"', $subject)); // @codeCoverageIgnore } - $cache->store($return); + $cache->store($filtered); - return response()->json($return); + return response()->json($filtered); } /** @@ -394,7 +187,7 @@ class AutoCompleteController extends Controller $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $cache->addProperty($key); if ($cache->has()) { - return response()->json($cache->get()); + return response()->json($cache->get()); // @codeCoverageIgnore } // find everything: $collector->setLimit(400)->setPage(1); @@ -430,94 +223,6 @@ class AutoCompleteController extends Controller return response()->json($return); } - /** - * List of revenue accounts. - * - * @param Request $request - * @param AccountRepositoryInterface $repository - * - * @return JsonResponse - */ - public function revenueAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-revenue-accounts'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); - } - // find everything: - $set = $repository->getAccountsByType([AccountType::REVENUE]); - $filtered = $set->filter( - function (Account $account) { - if (true === $account->active) { - return $account; - } - - return false; - } - ); - $return = array_unique($filtered->pluck('name')->toArray()); - sort($return); - - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); - } - $cache->store($return); - - return response()->json($return); - } - - /** - * Returns a JSON list of all beneficiaries. - * - * @param Request $request - * @param TagRepositoryInterface $tagRepository - * - * @return JsonResponse - */ - public function tags(Request $request, TagRepositoryInterface $tagRepository): JsonResponse - { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-revenue-accounts'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); - } - // find everything: - $return = array_unique($tagRepository->get()->pluck('tag')->toArray()); - sort($return); - - if ('' !== $search) { - $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) - ) - ); - } - $cache->store($return); - - return response()->json($return); - } - /** * List of journals by type. * @@ -536,7 +241,7 @@ class AutoCompleteController extends Controller $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $cache->addProperty($key); if ($cache->has()) { - return response()->json($cache->get()); + return response()->json($cache->get()); // @codeCoverageIgnore } // find everything: $type = config('firefly.transactionTypesByWhat.' . $what); @@ -563,43 +268,117 @@ class AutoCompleteController extends Controller } /** - * List if transaction types. + * @param array $unfiltered + * @param string $query * - * @param Request $request - * @param JournalRepositoryInterface $repository - * - * @return JsonResponse + * @return array|null */ - public function transactionTypes(Request $request, JournalRepositoryInterface $repository): JsonResponse + private function filterResult(?array $unfiltered, string $query): ?array { - $search = (string)$request->get('search'); - $cache = new CacheProperties; - $cache->addProperty('ac-revenue-accounts'); - // very unlikely a user will actually search for this string. - $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; - $cache->addProperty($key); - if ($cache->has()) { - return response()->json($cache->get()); + if (null === $unfiltered) { + return null; // @codeCoverageIgnore } - // find everything: - $return = array_unique($repository->getTransactionTypes()->pluck('type')->toArray()); - sort($return); + if ('' === $query) { + sort($unfiltered); - if ('' !== $search) { + return $unfiltered; + } + $return = []; + if ('' !== $query) { $return = array_values( - array_unique( - array_filter( - $return, function (string $value) use ($search) { - return !(false === stripos($value, $search)); - }, ARRAY_FILTER_USE_BOTH - ) + array_filter( + $unfiltered, function (string $value) use ($query) { + return !(false === stripos($value, $query)); + }, ARRAY_FILTER_USE_BOTH ) ); } - $cache->store($return); + sort($return); - return response()->json($return); + return $return; + } + + /** + * @param string $query + * @param array $types + * + * @return array + */ + private function getAccounts(array $types): array + { + $repository = app(AccountRepositoryInterface::class); + // find everything: + /** @var Collection $collection */ + $collection = $repository->getAccountsByType($types); + $filtered =$collection->filter(function(Account $account) { + return $account->active === true; + }); + $return = array_values(array_unique($filtered->pluck('name')->toArray())); + + return $return; } + + /** + * @return array + */ + private function getBills(): array + { + $repository = app(BillRepositoryInterface::class); + + return array_unique($repository->getActiveBills()->pluck('name')->toArray()); + } + + /** + * @return array + */ + private function getBudgets(): array + { + $repository = app(BudgetRepositoryInterface::class); + + return array_unique($repository->getBudgets()->pluck('name')->toArray()); + } + + /** + * @return array + */ + private function getCategories(): array + { + $repository = app(CategoryRepositoryInterface::class); + + return array_unique($repository->getCategories()->pluck('name')->toArray()); + } + + /** + * @return array + */ + private function getCurrencyNames(): array + { + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + + return $repository->get()->pluck('name')->toArray(); + } + + /** + * @return array + */ + private function getTags(): array + { + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + + return array_unique($repository->get()->pluck('tag')->toArray()); + } + + /** + * @return array + */ + private function getTransactionTypes(): array + { + $repository = app(JournalRepositoryInterface::class); + + return array_unique($repository->getTransactionTypes()->pluck('type')->toArray()); + } } diff --git a/app/Http/Requests/SplitJournalFormRequest.php b/app/Http/Requests/SplitJournalFormRequest.php index 7cedd16a18..0ad4321f26 100644 --- a/app/Http/Requests/SplitJournalFormRequest.php +++ b/app/Http/Requests/SplitJournalFormRequest.php @@ -168,8 +168,10 @@ class SplitJournalFormRequest extends Request /** @var array $array */ foreach ($transactions as $array) { if (null !== $array['destination_id'] && null !== $array['source_id'] && $array['destination_id'] === $array['source_id']) { + // @codeCoverageIgnoreStart $validator->errors()->add('journal_source_id', (string)trans('validation.source_equals_destination')); $validator->errors()->add('journal_destination_id', (string)trans('validation.source_equals_destination')); + // @codeCoverageIgnoreEnd } } diff --git a/app/Import/Storage/ImportArrayStorage.php b/app/Import/Storage/ImportArrayStorage.php index 76e2605347..179340e50e 100644 --- a/app/Import/Storage/ImportArrayStorage.php +++ b/app/Import/Storage/ImportArrayStorage.php @@ -193,9 +193,11 @@ class ImportArrayStorage unset($transaction['importHashV2'], $transaction['original-source']); $json = json_encode($transaction); if (false === $json) { + // @codeCoverageIgnoreStart /** @noinspection ForgottenDebugOutputInspection */ Log::error('Could not encode import array.', print_r($transaction, true)); - throw new FireflyException('Could not encode import array. Please see the logs.'); // @codeCoverageIgnore + throw new FireflyException('Could not encode import array. Please see the logs.'); + // @codeCoverageIgnoreEnd } $hash = hash('sha256', $json, false); Log::debug(sprintf('The hash is: %s', $hash)); diff --git a/app/Jobs/ExecuteRuleOnExistingTransactions.php b/app/Jobs/ExecuteRuleOnExistingTransactions.php index 16d0e660d2..51c7943e8b 100644 --- a/app/Jobs/ExecuteRuleOnExistingTransactions.php +++ b/app/Jobs/ExecuteRuleOnExistingTransactions.php @@ -179,10 +179,6 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue ++$misses; } Log::info(sprintf('Current progress: %d Transactions. Hits: %d, misses: %d', $total, $hits, $misses)); - // Stop processing this group if the rule specifies 'stop_processing' - if ($processor->getRule()->stop_processing) { - break; - } } Log::info(sprintf('Total transactions: %d. Hits: %d, misses: %d', $total, $hits, $misses)); } diff --git a/routes/web.php b/routes/web.php index 37b1e07c94..1034e926f6 100755 --- a/routes/web.php +++ b/routes/web.php @@ -543,19 +543,12 @@ Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'json', 'as' => 'json.'], function () { // for auto complete - Route::get('expense-accounts', ['uses' => 'Json\AutoCompleteController@expenseAccounts', 'as' => 'expense-accounts']); - Route::get('all-accounts', ['uses' => 'Json\AutoCompleteController@allAccounts', 'as' => 'all-accounts']); - Route::get('revenue-accounts', ['uses' => 'Json\AutoCompleteController@revenueAccounts', 'as' => 'revenue-accounts']); - Route::get('asset-accounts', ['uses' => 'Json\AutoCompleteController@assetAccounts', 'as' => 'asset-accounts']); - Route::get('categories', ['uses' => 'Json\AutoCompleteController@categories', 'as' => 'categories']); - Route::get('budgets', ['uses' => 'Json\AutoCompleteController@budgets', 'as' => 'budgets']); - Route::get('tags', ['uses' => 'Json\AutoCompleteController@tags', 'as' => 'tags']); - Route::get('bills', ['uses' => 'Json\AutoCompleteController@bills', 'as' => 'bills']); - Route::get('currency-names', ['uses' => 'Json\AutoCompleteController@currencyNames', 'as' => 'currency-names']); + + Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']); Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']); Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']); - Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']); +// Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']); // boxes Route::get('box/balance', ['uses' => 'Json\BoxController@balance', 'as' => 'box.balance']); @@ -578,6 +571,7 @@ Route::group( Route::post('intro/enable/{route}/{specificPage?}', ['uses' => 'Json\IntroController@postEnable', 'as' => 'intro.enable']); Route::get('intro/{route}/{specificPage?}', ['uses' => 'Json\IntroController@getIntroSteps', 'as' => 'intro']); + Route::get('/{subject}', ['uses' => 'Json\AutoCompleteController@autoComplete', 'as' => 'autocomplete']); } ); diff --git a/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php b/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php index 718eb6f86a..1e503348aa 100644 --- a/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php +++ b/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php @@ -29,6 +29,7 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -47,8 +48,6 @@ use Tests\TestCase; */ class AutoCompleteControllerTest extends TestCase { - - /** * */ @@ -68,11 +67,12 @@ class AutoCompleteControllerTest extends TestCase $collection = new Collection([$accountA]); $accountRepos = $this->mock(AccountRepositoryInterface::class); $accountRepos->shouldReceive('getAccountsByType') - ->withArgs([[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]]) + ->withArgs([[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, + AccountType::DEBT, AccountType::MORTGAGE]]) ->andReturn($collection); $this->be($this->user()); - $response = $this->get(route('json.all-accounts')); + $response = $this->get(route('json.autocomplete',['all-accounts'])); $response->assertStatus(200); $response->assertExactJson([$accountA->name]); @@ -88,10 +88,10 @@ class AutoCompleteControllerTest extends TestCase $collection = new Collection([$accountA]); $accountRepos = $this->mock(AccountRepositoryInterface::class); $accountRepos->shouldReceive('getAccountsByType') - ->withArgs([[AccountType::DEFAULT, AccountType::ASSET]])->andReturn($collection); + ->withArgs([[AccountType::ASSET, AccountType::DEFAULT]])->andReturn($collection); $this->be($this->user()); - $response = $this->get(route('json.asset-accounts')); + $response = $this->get(route('json.autocomplete',['asset-accounts'])); $response->assertStatus(200); $response->assertExactJson([$accountA->name]); @@ -102,16 +102,39 @@ class AutoCompleteControllerTest extends TestCase */ public function testAllTransactionJournals(): void { + $transaction = new Transaction(); + $transaction->description = 'hi there'; + $collection = new Collection([$transaction]); + $collector = $this->mock(TransactionCollectorInterface::class); $collector->shouldReceive('setLimit')->withArgs([250])->andReturnSelf(); $collector->shouldReceive('setPage')->withArgs([1])->andReturnSelf(); - $collector->shouldReceive('getTransactions')->andReturn(new Collection); + $collector->shouldReceive('getTransactions')->andReturn($collection); $this->be($this->user()); $response = $this->get(route('json.all-transaction-journals')); $response->assertStatus(200); } + /** + * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController + */ + public function testAllTransactionJournalsSearch(): void + { + $transaction = new Transaction(); + $transaction->description = 'hi there'; + $collection = new Collection([$transaction]); + + $collector = $this->mock(TransactionCollectorInterface::class); + $collector->shouldReceive('setLimit')->withArgs([250])->andReturnSelf(); + $collector->shouldReceive('setPage')->withArgs([1])->andReturnSelf(); + $collector->shouldReceive('getTransactions')->andReturn($collection); + + $this->be($this->user()); + $response = $this->get(route('json.all-transaction-journals').'?search=hi'); + $response->assertStatus(200); + } + /** * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController */ @@ -123,7 +146,22 @@ class AutoCompleteControllerTest extends TestCase $repository->shouldReceive('getActiveBills')->andReturn($bills); $this->be($this->user()); - $response = $this->get(route('json.bills')); + $response = $this->get(route('json.autocomplete',['bills'])); + $response->assertStatus(200); + } + + /** + * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController + */ + public function testBillsSearch(): void + { + $repository = $this->mock(BillRepositoryInterface::class); + $bills = factory(Bill::class, 10)->make(); + + $repository->shouldReceive('getActiveBills')->andReturn($bills); + + $this->be($this->user()); + $response = $this->get(route('json.autocomplete',['bills']).'?search=1234'); $response->assertStatus(200); } @@ -139,7 +177,7 @@ class AutoCompleteControllerTest extends TestCase $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); $categoryRepos->shouldReceive('getBudgets')->andReturn(new Collection([$budget])); $this->be($this->user()); - $response = $this->get(route('json.budgets')); + $response = $this->get(route('json.autocomplete',['budgets'])); $response->assertStatus(200); $response->assertExactJson([$budget->name]); } @@ -156,7 +194,7 @@ class AutoCompleteControllerTest extends TestCase $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); $categoryRepos->shouldReceive('getCategories')->andReturn(new Collection([$category])); $this->be($this->user()); - $response = $this->get(route('json.categories')); + $response = $this->get(route('json.autocomplete',['categories'])); $response->assertStatus(200); $response->assertExactJson([$category->name]); } @@ -172,7 +210,7 @@ class AutoCompleteControllerTest extends TestCase $repository->shouldReceive('get')->andReturn(new Collection([$currency]))->once(); $this->be($this->user()); - $response = $this->get(route('json.currency-names')); + $response = $this->get(route('json.autocomplete',['currency-names'])); $response->assertStatus(200); $response->assertExactJson(['Euro']); } @@ -194,7 +232,7 @@ class AutoCompleteControllerTest extends TestCase $accountRepos->shouldReceive('getAccountsByType')->withArgs([[AccountType::EXPENSE, AccountType::BENEFICIARY]])->once()->andReturn($collection); $this->be($this->user()); - $response = $this->get(route('json.expense-accounts')); + $response = $this->get(route('json.autocomplete',['expense-accounts'])); $response->assertStatus(200); $response->assertExactJson([$accountA->name]); } @@ -218,6 +256,25 @@ class AutoCompleteControllerTest extends TestCase $response->assertExactJson([['id' => $journal->id, 'name' => $journal->id . ': ' . $journal->description]]); } + /** + * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController + */ + public function testJournalsWithIdSearch(): void + { + $journal = $this->user()->transactionJournals()->where('id', '!=', 1)->first(); + $journal->journal_id = $journal->id; + $collection = new Collection([$journal]); + $collector = $this->mock(TransactionCollectorInterface::class); + $collector->shouldReceive('setLimit')->withArgs([400])->andReturnSelf(); + $collector->shouldReceive('setPage')->withArgs([1])->andReturnSelf(); + $collector->shouldReceive('getTransactions')->andReturn($collection); + + $this->be($this->user()); + $response = $this->get(route('json.journals-with-id', [1]).'?search=a' ); + $response->assertStatus(200); + $response->assertExactJson([['id' => $journal->id, 'name' => $journal->id . ': ' . $journal->description]]); + } + /** * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController */ @@ -235,7 +292,7 @@ class AutoCompleteControllerTest extends TestCase $accountRepos->shouldReceive('getAccountsByType')->withArgs([[AccountType::REVENUE]])->once()->andReturn($collection); $this->be($this->user()); - $response = $this->get(route('json.revenue-accounts')); + $response = $this->get(route('json.autocomplete',['revenue-accounts'])); $response->assertStatus(200); $response->assertExactJson([$accountA->name]); } @@ -253,7 +310,7 @@ class AutoCompleteControllerTest extends TestCase $tagRepos->shouldReceive('get')->andReturn(new Collection([$tag]))->once(); $this->be($this->user()); - $response = $this->get(route('json.tags')); + $response = $this->get(route('json.autocomplete', ['tags'])); $response->assertStatus(200); $response->assertExactJson([$tag->tag]); } @@ -263,6 +320,10 @@ class AutoCompleteControllerTest extends TestCase */ public function testTransactionJournals(): void { + $transaction = new Transaction(); + $transaction->description = 'hi there'; + $collection = new Collection([$transaction]); + // mock stuff $collector = $this->mock(TransactionCollectorInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); @@ -270,12 +331,36 @@ class AutoCompleteControllerTest extends TestCase $collector->shouldReceive('setTypes')->andReturnSelf(); $collector->shouldReceive('setLimit')->andReturnSelf(); $collector->shouldReceive('setPage')->andReturnSelf(); - $collector->shouldReceive('getTransactions')->andReturn(new Collection); + $collector->shouldReceive('getTransactions')->andReturn($collection); $this->be($this->user()); $response = $this->get(route('json.transaction-journals', ['deposit'])); $response->assertStatus(200); - $response->assertExactJson([]); + $response->assertExactJson(['hi there']); + } + + /** + * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController + */ + public function testTransactionJournalsSearch(): void + { + $transaction = new Transaction(); + $transaction->description = 'hi there'; + $collection = new Collection([$transaction]); + + // mock stuff + $collector = $this->mock(TransactionCollectorInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $collector->shouldReceive('setTypes')->andReturnSelf(); + $collector->shouldReceive('setLimit')->andReturnSelf(); + $collector->shouldReceive('setPage')->andReturnSelf(); + $collector->shouldReceive('getTransactions')->andReturn($collection); + + $this->be($this->user()); + $response = $this->get(route('json.transaction-journals', ['deposit']).'?search=hi'); + $response->assertStatus(200); + $response->assertExactJson(['hi there']); } /** @@ -289,7 +374,7 @@ class AutoCompleteControllerTest extends TestCase $journalRepos->shouldReceive('getTransactionTypes')->once()->andReturn(new Collection); $this->be($this->user()); - $response = $this->get(route('json.transaction-types', ['deposit'])); + $response = $this->get(route('json.autocomplete',['transaction_types'])); $response->assertStatus(200); $response->assertExactJson([]); } diff --git a/tests/Unit/Import/Prerequisites/YnabPrerequisitesTest.php b/tests/Unit/Import/Prerequisites/YnabPrerequisitesTest.php new file mode 100644 index 0000000000..fa45b049c6 --- /dev/null +++ b/tests/Unit/Import/Prerequisites/YnabPrerequisitesTest.php @@ -0,0 +1,161 @@ +. + */ + +declare(strict_types=1); + +namespace tests\Unit\Import\Prerequisites; + +use FireflyIII\Import\Prerequisites\YnabPrerequisites; +use FireflyIII\Models\Preference; +use Log; +use Mockery; +use Preferences; +use Tests\TestCase; + +/** + * Class YnabPrerequisitesTest + */ +class YnabPrerequisitesTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + + /** + * @covers \FireflyIII\Import\Prerequisites\YnabPrerequisites + */ + public function testGetView(): void + { + + $object = new YnabPrerequisites; + $object->setUser($this->user()); + $this->assertEquals('import.ynab.prerequisites', $object->getView()); + } + + /** + * First test, user has nothing. + * + * @covers \FireflyIII\Import\Prerequisites\YnabPrerequisites + */ + public function testGetViewParametersNull(): void + { + + Preferences::shouldReceive('getForUser')->once()->withArgs([Mockery::any(), 'ynab_client_id', null])->andReturn(null); + Preferences::shouldReceive('getForUser')->once()->withArgs([Mockery::any(), 'ynab_client_secret', null])->andReturn(null); + + $object = new YnabPrerequisites(); + $object->setUser($this->user()); + $result = $object->getViewParameters(); + + $expected = ['client_id' => '', 'client_secret' => '', 'callback_uri' => 'http://localhost/import/ynab-callback', 'is_https' => false]; + + $this->assertEquals($expected, $result); + } + + /** + * + */ + public function testStorePrerequisites(): void { + + Preferences::shouldReceive('setForUser')->once()->withArgs([Mockery::any(), 'ynab_client_id', 'hello']); + Preferences::shouldReceive('setForUser')->once()->withArgs([Mockery::any(), 'ynab_client_secret', 'hi there']); + + $data = [ + 'client_id' => 'hello', + 'client_secret' => 'hi there' + ]; + + $object = new YnabPrerequisites(); + $object->setUser($this->user()); + $object->storePrerequisites($data); + } + + /** + * First test, user has empty. + * + * @covers \FireflyIII\Import\Prerequisites\YnabPrerequisites + */ + public function testGetViewParametersEmpty(): void + { + $clientId = new Preference; + $clientId->data = ''; + + $clientSecret = new Preference; + $clientSecret->data = ''; + + Preferences::shouldReceive('getForUser')->once()->withArgs([Mockery::any(), 'ynab_client_id', null])->andReturn($clientId); + Preferences::shouldReceive('getForUser')->once()->withArgs([Mockery::any(), 'ynab_client_secret', null])->andReturn($clientSecret); + + $object = new YnabPrerequisites(); + $object->setUser($this->user()); + $result = $object->getViewParameters(); + + $expected = ['client_id' => '', 'client_secret' => '', 'callback_uri' => 'http://localhost/import/ynab-callback', 'is_https' => false]; + + $this->assertEquals($expected, $result); + } + + /** + * @covers \FireflyIII\Import\Prerequisites\YnabPrerequisites + */ + public function testIsComplete(): void + { + + Preferences::shouldReceive('getForUser')->once()->withArgs([Mockery::any(), 'ynab_client_id', null])->andReturn(null); + + $object = new YnabPrerequisites(); + $object->setUser($this->user()); + $result = $object->isComplete(); + + $this->assertFalse($result); + } + + /** + * First test, user has nothing. + * + * @covers \FireflyIII\Import\Prerequisites\YnabPrerequisites + */ + public function testGetViewParametersFilled(): void + { + $clientId = new Preference; + $clientId->data = 'client-id'; + + $clientSecret = new Preference; + $clientSecret->data = 'client-secret'; + + Preferences::shouldReceive('getForUser')->twice()->withArgs([Mockery::any(), 'ynab_client_id', null])->andReturn($clientId); + Preferences::shouldReceive('getForUser')->twice()->withArgs([Mockery::any(), 'ynab_client_secret', null])->andReturn($clientSecret); + + $object = new YnabPrerequisites(); + $object->setUser($this->user()); + $result = $object->getViewParameters(); + + $expected = ['client_id' => 'client-id', 'client_secret' => 'client-secret', 'callback_uri' => 'http://localhost/import/ynab-callback', 'is_https' => false]; + + $this->assertEquals($expected, $result); + } +} \ No newline at end of file diff --git a/tests/Unit/Import/Routine/YnabRoutineTest.php b/tests/Unit/Import/Routine/YnabRoutineTest.php new file mode 100644 index 0000000000..00c0018895 --- /dev/null +++ b/tests/Unit/Import/Routine/YnabRoutineTest.php @@ -0,0 +1,319 @@ +. + */ + +declare(strict_types=1); + +namespace tests\Unit\Import\Routine; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Import\Routine\YnabRoutine; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Import\Routine\Ynab\GetAccountsHandler; +use FireflyIII\Support\Import\Routine\Ynab\ImportDataHandler; +use FireflyIII\Support\Import\Routine\Ynab\StageGetAccessHandler; +use FireflyIII\Support\Import\Routine\Ynab\StageGetBudgetsHandler; +use Log; +use Mockery; +use Tests\TestCase; + +/** + * Class YnabRoutineTest + */ +class YnabRoutineTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunGetAccessToken(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_1_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'get_access_token'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(StageGetAccessHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'running'])->once(); + + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->once(); + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'get_budgets'])->once(); + + // mock calls for handler + $handler->shouldReceive('setImportJob')->once(); + $handler->shouldReceive('run')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunMultiBudgets(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_2_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'get_budgets'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(StageGetBudgetsHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + $config = ['budgets' => [1, 2, 3]]; + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'running'])->once(); + $repository->shouldReceive('getConfiguration')->once()->andReturn($config); + + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'need_job_config'])->once(); + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'select_budgets'])->once(); + + // mock calls for handler + $handler->shouldReceive('setImportJob')->once(); + $handler->shouldReceive('run')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunSingleBudget(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_3_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'get_budgets'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(StageGetBudgetsHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + $config = ['budgets' => [1]]; + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'running'])->once(); + $repository->shouldReceive('getConfiguration')->once()->andReturn($config); + + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'ready_to_run'])->once(); + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'get_accounts'])->once(); + + // mock calls for handler + $handler->shouldReceive('setImportJob')->once(); + $handler->shouldReceive('run')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunGetAccounts(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_4_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'get_accounts'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(GetAccountsHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'running'])->once(); + + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'select_accounts'])->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'need_job_config'])->once(); + + + // mock calls for handler + $handler->shouldReceive('setImportJob')->once(); + $handler->shouldReceive('run')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunGoForImport(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_5_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'go-for-import'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(ImportDataHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'running'])->once(); + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'do_import'])->once(); + + $repository->shouldReceive('setStatus')->withArgs([Mockery::any(), 'provider_finished'])->once(); + $repository->shouldReceive('setStage')->withArgs([Mockery::any(), 'final'])->once(); + + + + // mock calls for handler + $handler->shouldReceive('setImportJob')->once(); + $handler->shouldReceive('run')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunException(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_6_' . random_int(1, 10000); + $job->status = 'ready_to_run'; + $job->stage = 'bad_state'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(ImportDataHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertEquals('YNAB import routine cannot handle stage "bad_state"', $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Import\Routine\YnabRoutine + */ + public function testRunBadStatus(): void + { + $job = new ImportJob; + $job->user_id = $this->user()->id; + $job->key = 'ynab_r_7_' . random_int(1, 10000); + $job->status = 'not_ready_to_run'; + $job->stage = 'bad_state'; + $job->provider = 'ynab'; + $job->file_type = ''; + $job->configuration = []; + $job->save(); + + // mock handler and repository + $handler = $this->mock(ImportDataHandler::class); + $repository = $this->mock(ImportJobRepositoryInterface::class); + + // mock calls for repository + $repository->shouldReceive('setUser')->once(); + + $routine = new YnabRoutine; + $routine->setImportJob($job); + try { + $routine->run(); + } catch (FireflyException $e) { + $this->assertEquals('YNAB import routine cannot handle stage "bad_state"', $e->getMessage()); + } + } +} \ No newline at end of file