From 2e67bd3b7833e5276e9f339c29836d28f98c9416 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 2 Jul 2018 20:02:20 +0200 Subject: [PATCH] New test code. --- .../V1/Controllers/JournalLinkController.php | 8 +- .../V1/Controllers/RecurrenceController.php | 2 +- app/Api/V1/Requests/RecurrenceRequest.php | 4 +- resources/lang/en_US/validation.php | 2 +- .../V1/Controllers/CategoryControllerTest.php | 8 +- .../Controllers/JournalLinkControllerTest.php | 239 +++ .../V1/Controllers/LinkTypeControllerTest.php | 200 ++ .../Controllers/RecurrenceControllerTest.php | 1631 +++++++++++++++++ 8 files changed, 2083 insertions(+), 11 deletions(-) create mode 100644 tests/Api/V1/Controllers/JournalLinkControllerTest.php create mode 100644 tests/Api/V1/Controllers/LinkTypeControllerTest.php create mode 100644 tests/Api/V1/Controllers/RecurrenceControllerTest.php diff --git a/app/Api/V1/Controllers/JournalLinkController.php b/app/Api/V1/Controllers/JournalLinkController.php index c54b42484e..0dde9742be 100644 --- a/app/Api/V1/Controllers/JournalLinkController.php +++ b/app/Api/V1/Controllers/JournalLinkController.php @@ -68,13 +68,13 @@ class JournalLinkController extends Controller /** * Delete the resource. * - * @param string $object + * @param TransactionJournalLink $link * * @return JsonResponse */ - public function delete(string $object): JsonResponse + public function delete(TransactionJournalLink $link): JsonResponse { - // todo delete object. + $this->repository->destroyLink($link); return response()->json([], 204); } @@ -88,8 +88,6 @@ class JournalLinkController extends Controller */ public function index(Request $request): JsonResponse { - - // create some objects: $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; diff --git a/app/Api/V1/Controllers/RecurrenceController.php b/app/Api/V1/Controllers/RecurrenceController.php index dce57a0d98..a5ec34e9c6 100644 --- a/app/Api/V1/Controllers/RecurrenceController.php +++ b/app/Api/V1/Controllers/RecurrenceController.php @@ -66,7 +66,7 @@ class RecurrenceController extends Controller /** * Delete the resource. * - * @param string $object + * @param Recurrence $recurrence * * @return JsonResponse */ diff --git a/app/Api/V1/Requests/RecurrenceRequest.php b/app/Api/V1/Requests/RecurrenceRequest.php index c7c3c04951..3aa948722a 100644 --- a/app/Api/V1/Requests/RecurrenceRequest.php +++ b/app/Api/V1/Requests/RecurrenceRequest.php @@ -30,6 +30,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\BelongsUser; use Illuminate\Validation\Validator; use InvalidArgumentException; +use Log; /** * Class RecurrenceRequest @@ -208,6 +209,7 @@ class RecurrenceRequest extends Request $repository = app(AccountRepositoryInterface::class); $repository->setUser(auth()->user()); $set = $repository->getAccountsById([$accountId]); + Log::debug(sprintf('Count of accounts found by ID %d is: %d', $accountId, $set->count())); if ($set->count() === 1) { /** @var Account $first */ $first = $set->first(); @@ -399,7 +401,7 @@ class RecurrenceRequest extends Request if (null !== $repetitions && null !== $repeatUntil) { // expect a date OR count: $validator->errors()->add('repeat_until', trans('validation.require_repeat_until')); - $validator->errors()->add('repetitions', trans('validation.require_repeat_until')); + $validator->errors()->add('nr_of_repetitions', trans('validation.require_repeat_until')); return; } diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 38371f9d97..f9e017f755 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -38,7 +38,7 @@ return [ 'belongs_user' => 'This value is invalid for this field.', 'at_least_one_transaction' => 'Need at least one transaction.', 'at_least_one_repetition' => 'Need at least one repetition.', - 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeats_until). Not both.', + 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', 'require_currency_info' => 'The content of this field is invalid without currency information.', 'equal_description' => 'Transaction description should not equal global description.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', diff --git a/tests/Api/V1/Controllers/CategoryControllerTest.php b/tests/Api/V1/Controllers/CategoryControllerTest.php index 4782bac76c..672a1a366d 100644 --- a/tests/Api/V1/Controllers/CategoryControllerTest.php +++ b/tests/Api/V1/Controllers/CategoryControllerTest.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace Tests\Api\V1\Controllers; -use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use Laravel\Passport\Passport; use Log; @@ -87,6 +87,7 @@ class CategoryControllerTest extends TestCase $response = $this->get('/api/v1/categories'); $response->assertStatus(200); $response->assertSee($categories->first()->name); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); } /** @@ -107,6 +108,7 @@ class CategoryControllerTest extends TestCase $response = $this->get('/api/v1/categories/' . $category->id); $response->assertStatus(200); $response->assertSee($category->name); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); } /** @@ -116,7 +118,7 @@ class CategoryControllerTest extends TestCase */ public function testStore(): void { - /** @var Budget $category */ + /** @var Category $category */ $category = $this->user()->categories()->first(); // mock stuff: @@ -150,7 +152,7 @@ class CategoryControllerTest extends TestCase // mock repositories $repository = $this->mock(CategoryRepositoryInterface::class); - /** @var Budget $category */ + /** @var Category $category */ $category = $this->user()->categories()->first(); // mock calls: diff --git a/tests/Api/V1/Controllers/JournalLinkControllerTest.php b/tests/Api/V1/Controllers/JournalLinkControllerTest.php new file mode 100644 index 0000000000..59720eb5fd --- /dev/null +++ b/tests/Api/V1/Controllers/JournalLinkControllerTest.php @@ -0,0 +1,239 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Api\V1\Controllers; + + +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournalLink; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface; +use Illuminate\Support\Collection; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; + +/** + * + * Class JournalLinkControllerTest + */ +class JournalLinkControllerTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::debug(sprintf('Now in %s.', \get_class($this))); + + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + */ + public function testDelete(): void + { + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $journalRepos->shouldReceive('setUser')->once(); + $repository->shouldReceive('destroyLink')->once()->andReturn(true); + + // get a link + /** @var TransactionJournalLink $journalLink */ + $journalLink = TransactionJournalLink::first(); + + // call API + $response = $this->delete('/api/v1/journal_links/' . $journalLink->id); + $response->assertStatus(204); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + */ + public function testIndex(): void + { + $journalLinks = TransactionJournalLink::get(); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('findByName')->once()->andReturn(null); + $repository->shouldReceive('getJournalLinks')->once()->andReturn($journalLinks); + + $journalRepos->shouldReceive('setUser')->once(); + + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + // call API + $response = $this->get('/api/v1/journal_links'); + $response->assertStatus(200); + $response->assertSee($journalLinks->first()->id); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + */ + public function testShow(): void + { + $journalLink = TransactionJournalLink::first(); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $journalRepos->shouldReceive('setUser')->once(); + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + // call API + $response = $this->get('/api/v1/journal_links/' . $journalLink->id); + $response->assertStatus(200); + $response->assertSee($journalLink->id); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + * @covers \FireflyIII\Api\V1\Requests\JournalLinkRequest + */ + public function testStore(): void + { + $journalLink = TransactionJournalLink::first(); + $journal = $this->user()->transactionJournals()->find(1); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $journalRepos->shouldReceive('setUser')->once(); + $collector->shouldReceive('setUser')->withAnyArgs(); + + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + $journalRepos->shouldReceive('findNull')->andReturn($journal); + $repository->shouldReceive('storeLink')->once()->andReturn($journalLink); + + + // data to submit + $data = [ + 'link_type_id' => '1', + 'inward_id' => '1', + 'outward_id' => '2', + 'notes' => 'Some notes', + ]; + + // test API + $response = $this->post('/api/v1/journal_links', $data); + $response->assertStatus(200); + $response->assertSee($journalLink->created_at->toAtomString()); // the creation moment. + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + * @covers \FireflyIII\Api\V1\Requests\JournalLinkRequest + */ + public function testUpdate(): void + { + + // mock repositories + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + $journalLink = TransactionJournalLink::first(); + $journal = $this->user()->transactionJournals()->find(1); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + + + // mock calls: + $repository->shouldReceive('setUser'); + $journalRepos->shouldReceive('setUser')->once(); + $collector->shouldReceive('setUser')->withAnyArgs(); + + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + $journalRepos->shouldReceive('findNull')->andReturn($journal); + $repository->shouldReceive('updateLink')->once()->andReturn($journalLink); + + // data to submit + $data = [ + 'link_type_id' => '1', + 'inward_id' => '1', + 'outward_id' => '2', + 'notes' => 'Some notes', + ]; + + // test API + $response = $this->put('/api/v1/journal_links/' . $journalLink->id, $data, ['Accept' => 'application/json']); + $response->assertStatus(200); + $response->assertSee($journalLink->created_at->toAtomString()); // the creation moment. + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } +} \ No newline at end of file diff --git a/tests/Api/V1/Controllers/LinkTypeControllerTest.php b/tests/Api/V1/Controllers/LinkTypeControllerTest.php new file mode 100644 index 0000000000..c23a9c06a5 --- /dev/null +++ b/tests/Api/V1/Controllers/LinkTypeControllerTest.php @@ -0,0 +1,200 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Api\V1\Controllers; + +use FireflyIII\Models\LinkType; +use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface; +use FireflyIII\Repositories\User\UserRepositoryInterface; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; + + +/** + * + * Class LinkTypeControllerTest + */ +class LinkTypeControllerTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::debug(sprintf('Now in %s.', \get_class($this))); + + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\LinkTypeController + */ + public function testDelete(): void + { + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $userRepository = $this->mock(UserRepositoryInterface::class); + + // create editable link type: + $linkType = LinkType::create( + [ + 'name' => 'random' . random_int(1, 100000), + 'outward' => 'outward' . random_int(1, 100000), + 'inward' => 'inward ' . random_int(1, 100000), + 'editable' => true, + + ] + ); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('destroy')->once()->andReturn(true); + + // call API + $response = $this->delete('/api/v1/link_types/' . $linkType->id); + $response->assertStatus(204); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\LinkTypeController + */ + public function testIndex(): void + { + $linkTypes = LinkType::get(); + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $userRepository = $this->mock(UserRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('get')->once()->andReturn($linkTypes); + + // call API + $response = $this->get('/api/v1/link_types'); + $response->assertStatus(200); + $response->assertSee($linkTypes->first()->created_at->toAtomString()); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\LinkTypeController + */ + public function testShow(): void + { + $linkType = LinkType::first(); + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $userRepository = $this->mock(UserRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + + // call API + $response = $this->get('/api/v1/link_types/' . $linkType->id); + $response->assertStatus(200); + $response->assertSee($linkType->first()->created_at->toAtomString()); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\LinkTypeController + * @covers \FireflyIII\Api\V1\Requests\LinkTypeRequest + */ + public function testStore(): void + { + $linkType = LinkType::first(); + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $userRepository = $this->mock(UserRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('store')->once()->andReturn($linkType); + $userRepository->shouldReceive('hasRole')->once()->andReturn(true); + + + // data to submit + $data = [ + 'name' => 'random' . random_int(1, 100000), + 'outward' => 'outward' . random_int(1, 100000), + 'inward' => 'inward ' . random_int(1, 100000), + 'editable' => true, + + ]; + + // test API + $response = $this->post('/api/v1/link_types', $data); + $response->assertStatus(200); + $response->assertSee($linkType->created_at->toAtomString()); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\LinkTypeController + * @covers \FireflyIII\Api\V1\Requests\LinkTypeRequest + */ + public function testUpdate(): void + { + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $userRepository = $this->mock(UserRepositoryInterface::class); + $userRepository->shouldReceive('hasRole')->once()->andReturn(true); + + // create editable link type: + $linkType = LinkType::create( + [ + 'name' => 'random' . random_int(1, 100000), + 'outward' => 'outward' . random_int(1, 100000), + 'inward' => 'inward ' . random_int(1, 100000), + 'editable' => true, + + ] + ); + + // mock calls: + $repository->shouldReceive('setUser'); + $repository->shouldReceive('update')->once()->andReturn($linkType); + + // data to submit + $data = [ + 'name' => 'random' . random_int(1, 100000), + 'outward' => 'outward' . random_int(1, 100000), + 'inward' => 'inward ' . random_int(1, 100000), + 'editable' => true, + + ]; + + // test API + $response = $this->put('/api/v1/link_types/' . $linkType->id, $data, ['Accept' => 'application/json']); + $response->assertStatus(200); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + $response->assertSee($linkType->created_at->toAtomString()); + } + + +} \ No newline at end of file diff --git a/tests/Api/V1/Controllers/RecurrenceControllerTest.php b/tests/Api/V1/Controllers/RecurrenceControllerTest.php new file mode 100644 index 0000000000..56f469e4a7 --- /dev/null +++ b/tests/Api/V1/Controllers/RecurrenceControllerTest.php @@ -0,0 +1,1631 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Api\V1\Controllers; + +use Carbon\Carbon; +use FireflyIII\Factory\CategoryFactory; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Recurrence; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface; +use Illuminate\Support\Collection; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; + +class RecurrenceControllerTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::debug(sprintf('Now in %s.', \get_class($this))); + + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + */ + public function testDelete(): void + { + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('destroy')->once()->andReturn(true); + + // get a recurrence: + $recurrence = $this->user()->recurrences()->first(); + + // call API + $response = $this->delete('/api/v1/recurrences/' . $recurrence->id); + $response->assertStatus(204); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + */ + public function testIndex(): void + { + /** @var Recurrence $recurrences */ + $recurrences = $this->user()->recurrences()->get(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser'); + $repository->shouldReceive('getAll')->once()->andReturn($recurrences); + $repository->shouldReceive('getNoteText')->andReturn('Notes.'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + + // call API + $response = $this->get('/api/v1/recurrences'); + $response->assertStatus(200); + $response->assertSee($recurrences->first()->title); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + */ + public function testShow(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser'); + $repository->shouldReceive('getNoteText')->andReturn('Notes.'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // call API + $response = $this->get('/api/v1/recurrences/' . $recurrence->id); + $response->assertStatus(200); + $response->assertSee($recurrence->title); + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreAssetId(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source name field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreAssetName(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[0]])->once()->andReturn(new Collection); + // used by the validator to find the source_name: + $accountRepos->shouldReceive('findByName')->withArgs(['Checking Account', [AccountType::ASSET]])->once()->andReturn($assetAccount); + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_name' => 'Checking Account', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Submit a deposit. Since most validators have been tested in other methods, dont bother too much. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreDeposit(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'deposit', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description deposit', + 'source_name' => 'Some expense account', + 'destination_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Add a recurring with correct reference to a destination (expense). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreDestinationId(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + $expenseAccount = $this->user()->accounts()->where('account_type_id', 4)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + $accountRepos->shouldReceive('getAccountsById')->withArgs([[$expenseAccount->id]])->once() + ->andReturn(new Collection([$expenseAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + 'destination_id' => $expenseAccount->id, + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Add a recurring with correct reference to a destination (expense). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreDestinationName(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + $expenseAccount = $this->user()->accounts()->where('account_type_id', 4)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + 'destination_name' => $expenseAccount->name, + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + + /** + * Includes both repetition count and an end date. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailBothRepetitions(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $repeatUntil = new Carbon; + $repeatUntil->addMonth(); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'repeat_until' => $repeatUntil->format('Y-m-d'), + 'nr_of_repetitions' => 10, + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repeat_until' => [ + 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + ], + 'nr_of_repetitions' => [ + 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit foreign amount but no currency information. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailForeignCurrency(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[0]])->once()->andReturn(new Collection); + // used by the validator to find the source_name: + $accountRepos->shouldReceive('findByName')->withArgs(['Checking Account', [AccountType::ASSET]])->once()->andReturn($assetAccount); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'foreign_amount' => '100', + 'description' => 'Test description', + 'source_name' => 'Checking Account', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.foreign_amount' => [ + 'The content of this field is invalid without currency information.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidDaily(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '1', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidWeekly(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'weekly', + 'moment' => '8', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidMonthly(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'monthly', + 'moment' => '32', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidNdom(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'ndom', + 'moment' => '9,9', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidNdomHigh(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'ndom', + 'moment' => '4,9', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit the minimum amount to store a recurring transaction (using source ID field). + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidNdomCount(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [ + [ + 'type' => 'ndom', + 'moment' => '9', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'repetitions.0.moment' => [ + 'Invalid repetition moment for this type of repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Add a recurring but refer to an asset as destination. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailInvalidDestinationId(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + $accountRepos->shouldReceive('getAccountsById')->withArgs([[$assetAccount->id]])->once() + ->andReturn(new Collection([$assetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + 'destination_id' => $assetAccount->id, + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.destination_id' => [ + 'This value is invalid for this field.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit without a source account. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailNoAsset(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '0', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.source_id' => [ + 'This value is invalid for this field.', + 'The transactions.0.source_id field is required.', + ], + ], + ] + ); + $response->assertStatus(422); + + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit with an expense account. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailNotAsset(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // expense account: + $expenseAccount = $this->user()->accounts()->where('account_type_id', 4)->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[$expenseAccount->id]])->once() + ->andReturn(new Collection([$expenseAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => $expenseAccount->id, + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.source_id' => [ + 'This value is invalid for this field.', + ], + ], + ] + ); + $response->assertStatus(422); + + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit with an invalid asset account name. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailNotAssetName(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // expense account: + $expenseAccount = $this->user()->accounts()->where('account_type_id', 4)->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[0]])->once() + ->andReturn(new Collection); + // used to search by name. + $accountRepos->shouldReceive('findByName')->withArgs(['Fake name', [AccountType::ASSET]])->once() + ->andReturn(null); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_name' => 'Fake name', + 'source_id' => '0', + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.source_id' => [ + 'This value is invalid for this field.', + ], + 'transactions.0.source_name' => [ + 'This value is invalid for this field.', + ], + ], + ] + ); + $response->assertStatus(422); + + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Dont include enough repetitions. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailRepetitions(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[1]])->once() + ->andReturn(new Collection([$assetAccount])); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description', + 'source_id' => '1', + ], + ], + 'repetitions' => [], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'description' => [ + 'Need at least one repetition.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Dont include enough repetitions. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreFailTransactions(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'withdrawal', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertExactJson( + [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'description' => [ + 'Need at least one transaction.', + ], + ], + ] + ); + $response->assertStatus(422); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * Submit a transfer. Since most validators have been tested in other methods, dont bother too much. + * + * @covers \FireflyIII\Api\V1\Controllers\RecurrenceController + * @covers \FireflyIII\Api\V1\Requests\RecurrenceRequest + */ + public function testStoreTransfer(): void + { + /** @var Recurrence $recurrence */ + $recurrence = $this->user()->recurrences()->first(); + + // mock stuff: + $repository = $this->mock(RecurringRepositoryInterface::class); + $factory = $this->mock(CategoryFactory::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $assetAccount = $this->user()->accounts()->where('account_type_id', 3)->first(); + $otherAssetAccount = $this->user()->accounts()->where('account_type_id', 3)->where('id', '!=', $assetAccount->id)->first(); + + // mock calls: + $repository->shouldReceive('setUser'); + $factory->shouldReceive('setUser'); + $budgetRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('setUser'); + $repository->shouldReceive('store')->once()->andReturn($recurrence); + + + // used by the validator to find the source_id: + $accountRepos->shouldReceive('getAccountsById')->withArgs([[$assetAccount->id]])->once()->andReturn(new Collection([$assetAccount])); + $accountRepos->shouldReceive('getAccountsById')->withArgs([[$otherAssetAccount->id]])->once()->andReturn(new Collection([$otherAssetAccount])); + + + // entries used by the transformer + $repository->shouldReceive('getNoteText')->andReturn('Note text'); + $repository->shouldReceive('repetitionDescription')->andReturn('Some description.'); + $repository->shouldReceive('getXOccurrences')->andReturn([]); + + // entries used by the transformer (the fake entry has a category + a budget): + $factory->shouldReceive('findOrCreate')->andReturn(null); + $budgetRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $firstDate = new Carbon; + $firstDate->addDays(2); + $data = [ + 'type' => 'transfer', + 'title' => 'Hello', + 'first_date' => $firstDate->format('Y-m-d'), + 'apply_rules' => 1, + 'active' => 1, + 'transactions' => [ + [ + 'amount' => '100', + 'currency_id' => '1', + 'description' => 'Test description transfer', + 'source_id' => $assetAccount->id, + 'destination_id' => $otherAssetAccount->id, + ], + ], + 'repetitions' => [ + [ + 'type' => 'daily', + 'moment' => '', + 'skip' => '0', + 'weekend' => '1', + + ], + ], + ]; + + // test API + $response = $this->post('/api/v1/recurrences', $data, ['Accept' => 'application/json']); + $response->assertSee($recurrence->title); + $response->assertStatus(200); + + $response->assertHeader('Content-Type', 'application/vnd.api+json'); + } + +} \ No newline at end of file