From 6bb8967c9efa6d61ea08fe2d8f7984bda548b281 Mon Sep 17 00:00:00 2001 From: philipp lang <philipp@aweos.de> Date: Wed, 16 Nov 2022 22:59:49 +0100 Subject: [PATCH] Add memberships test --- .../Actions/MembershipDestroyAction.php | 37 +++++ .../Actions/MembershipStoreAction.php | 84 ++++++++++ tests/Feature/Member/StoreTest.php | 81 ++++++++-- tests/Feature/Member/UpdateTest.php | 63 ++------ tests/Feature/Membership/StoreTest.php | 143 ++++++++++++++++-- .../RequestFactories/MemberRequestFactory.php | 42 +++++ .../MembershipRequestFactory.php | 47 ++++++ 7 files changed, 429 insertions(+), 68 deletions(-) create mode 100644 app/Membership/Actions/MembershipDestroyAction.php create mode 100644 app/Membership/Actions/MembershipStoreAction.php create mode 100644 tests/RequestFactories/MemberRequestFactory.php create mode 100644 tests/RequestFactories/MembershipRequestFactory.php diff --git a/app/Membership/Actions/MembershipDestroyAction.php b/app/Membership/Actions/MembershipDestroyAction.php new file mode 100644 index 00000000..4fb30944 --- /dev/null +++ b/app/Membership/Actions/MembershipDestroyAction.php @@ -0,0 +1,37 @@ +<?php + +namespace App\Membership\Actions; + +use App\Member\Member; +use App\Member\Membership; +use App\Setting\NamiSettings; +use Illuminate\Http\RedirectResponse; +use Lorisleiva\Actions\ActionRequest; +use Lorisleiva\Actions\Concerns\AsAction; + +class MembershipDestroyAction +{ + use AsAction; + + public function handle(Member $member, Membership $membership, NamiSettings $settings): void + { + $api = $settings->login(); + $settings->login()->deleteMembership( + $member->nami_id, + $api->membership($member->nami_id, $membership->nami_id) + ); + $membership->delete(); + $member->syncVersion(); + } + + public function asController(Member $member, Membership $membership, ActionRequest $request, NamiSettings $settings): RedirectResponse + { + $this->handle( + $member, + $membership, + $settings, + ); + + return redirect()->back(); + } +} diff --git a/app/Membership/Actions/MembershipStoreAction.php b/app/Membership/Actions/MembershipStoreAction.php new file mode 100644 index 00000000..f48b5273 --- /dev/null +++ b/app/Membership/Actions/MembershipStoreAction.php @@ -0,0 +1,84 @@ +<?php + +namespace App\Membership\Actions; + +use App\Activity; +use App\Member\Member; +use App\Member\Membership; +use App\Setting\NamiSettings; +use App\Subactivity; +use Illuminate\Http\RedirectResponse; +use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\In; +use Illuminate\Validation\ValidationException; +use Lorisleiva\Actions\ActionRequest; +use Lorisleiva\Actions\Concerns\AsAction; +use Zoomyboy\LaravelNami\Data\Membership as NamiMembership; +use Zoomyboy\LaravelNami\NamiException; + +class MembershipStoreAction +{ + use AsAction; + + public function handle(Member $member, Activity $activity, ?Subactivity $subactivity, NamiSettings $settings): Membership + { + $from = now()->startOfDay(); + + try { + $namiId = $settings->login()->putMembership($member->nami_id, NamiMembership::fromArray([ + 'startsAt' => $from, + 'groupId' => $member->group->nami_id, + 'activityId' => $activity->nami_id, + 'subactivityId' => $subactivity ? $subactivity->nami_id : null, + ])); + } catch (NamiException $e) { + throw ValidationException::withMessages(['nami' => htmlspecialchars($e->getMessage())]); + } + + $membership = $member->memberships()->create([ + 'activity_id' => $activity->id, + 'subactivity_id' => $subactivity ? $subactivity->id : null, + ...['nami_id' => $namiId, 'group_id' => $member->group->id, 'from' => $from], + ]); + + $member->syncVersion(); + + return $membership; + } + + /** + * @return array<string, array<int, string|In>> + */ + public function rules(): array + { + $subactivityRule = request()->activity_id ? ['nullable', Rule::exists('activity_subactivity', 'subactivity_id')->where('activity_id', request()->activity_id)] : ['nullable']; + + return [ + 'activity_id' => ['bail', 'required', 'exists:activities,id'], + 'subactivity_id' => $subactivityRule, + ]; + } + + /** + * @return array<string, string> + */ + public function getValidationAttributes(): array + { + return [ + 'activity_id' => 'Tätigkeit', + 'subactivity_id' => 'Untertätigkeit', + ]; + } + + public function asController(Member $member, ActionRequest $request, NamiSettings $settings): RedirectResponse + { + $this->handle( + $member, + Activity::find($request->activity_id), + $request->subactivity_id ? Subactivity::find($request->subactivity_id) : null, + $settings, + ); + + return redirect()->back(); + } +} diff --git a/tests/Feature/Member/StoreTest.php b/tests/Feature/Member/StoreTest.php index 80984f69..68b6dc65 100644 --- a/tests/Feature/Member/StoreTest.php +++ b/tests/Feature/Member/StoreTest.php @@ -7,15 +7,13 @@ use App\Country; use App\Fee; use App\Gender; use App\Letter\BillKind; -use App\Member\CreateJob; +use App\Member\Actions\NamiPutMemberAction; use App\Member\Member; use App\Nationality; use App\Payment\Subscription; use App\Region; -use App\Setting\NamiSettings; use App\Subactivity; use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Support\Facades\Queue; use Tests\Lib\MergesAttributes; use Tests\TestCase; @@ -26,12 +24,7 @@ class StoreTest extends TestCase public function testItCanStoreAMember(): void { - Queue::fake(); Fee::factory()->create(); - NamiSettings::fake([ - 'default_group_id' => 55, - 'password' => 'tt', - ]); $this->withoutExceptionHandling()->login()->loginNami(); $country = Country::factory()->create(); $gender = Gender::factory()->create(); @@ -41,6 +34,7 @@ class StoreTest extends TestCase $subactivity = Subactivity::factory()->create(); $subscription = Subscription::factory()->create(); $billKind = BillKind::factory()->create(); + NamiPutMemberAction::allowToRun(); $response = $this ->from('/member/create') @@ -58,7 +52,6 @@ class StoreTest extends TestCase $response->assertStatus(302)->assertSessionHasNoErrors(); $response->assertRedirect('/member'); $member = Member::firstWhere('firstname', 'Joe'); - Queue::assertPushed(CreateJob::class, fn ($job) => $job->memberId === $member->id); $this->assertDatabaseHas('members', [ 'address' => 'Bavert 50', 'bill_kind_id' => $billKind->id, @@ -81,6 +74,76 @@ class StoreTest extends TestCase 'zip' => '42719', 'fax' => '+49 666', ]); + NamiPutMemberAction::spy()->shouldHaveReceived('handle')->withArgs(fn (Member $memberParam, Activity $activityParam, Subactivity $subactivityParam) => $memberParam->is($member) + && $activityParam->is($activity) + && $subactivityParam->is($subactivity) + )->once(); + } + + public function testItCanStoreAMemberWithoutNami(): void + { + Fee::factory()->create(); + $this->withoutExceptionHandling()->login()->loginNami(); + $country = Country::factory()->create(); + $gender = Gender::factory()->create(); + $region = Region::factory()->create(); + $nationality = Nationality::factory()->create(); + $subscription = Subscription::factory()->create(); + $billKind = BillKind::factory()->create(); + $activity = Activity::factory()->create(); + $subactivity = Subactivity::factory()->create(); + NamiPutMemberAction::allowToRun(); + + $response = $this + ->from('/member/create') + ->post('/member', $this->attributes([ + 'country_id' => $country->id, + 'gender_id' => $gender->id, + 'region_id' => $region->id, + 'nationality_id' => $nationality->id, + 'first_activity_id' => $activity->id, + 'first_subactivity_id' => $subactivity->id, + 'subscription_id' => $subscription->id, + 'bill_kind_id' => $billKind->id, + 'has_nami' => false, + ])); + + $response->assertStatus(302)->assertSessionHasNoErrors(); + $response->assertRedirect('/member'); + $member = Member::firstWhere('firstname', 'Joe'); + $this->assertDatabaseHas('members', [ + 'nami_id' => null, + ]); + NamiPutMemberAction::spy()->shouldNotHaveReceived('handle'); + } + + public function testSubscriptionIsRequiredIfFirstActivityIsPaid(): void + { + $this->login()->loginNami(); + Fee::factory()->create(); + $country = Country::factory()->create(); + $gender = Gender::factory()->create(); + $region = Region::factory()->create(); + $nationality = Nationality::factory()->create(); + $subscription = Subscription::factory()->create(); + $billKind = BillKind::factory()->create(); + $activity = Activity::factory()->create(); + $subactivity = Subactivity::factory()->create(); + + $response = $this + ->from('/member/create') + ->post('/member', $this->attributes([ + 'country_id' => $country->id, + 'gender_id' => $gender->id, + 'region_id' => $region->id, + 'nationality_id' => $nationality->id, + 'first_activity_id' => $activity->id, + 'first_subactivity_id' => $subactivity->id, + 'subscription_id' => null, + 'bill_kind_id' => $billKind->id, + ])); + + $this->assertErrors(['subscription_id' => 'Beitragsart ist erforderlich.'], $response); } public function defaults(): array diff --git a/tests/Feature/Member/UpdateTest.php b/tests/Feature/Member/UpdateTest.php index 68d10f19..109eb2e7 100644 --- a/tests/Feature/Member/UpdateTest.php +++ b/tests/Feature/Member/UpdateTest.php @@ -4,8 +4,11 @@ namespace Tests\Feature\Member; use App\Confession; use App\Country; +use App\Enum\Activity; +use App\Enum\Subactivity; use App\Fee; use App\Group; +use App\Member\Actions\NamiPutMemberAction; use App\Member\Member; use App\Nationality; use App\Payment\Subscription; @@ -23,43 +26,17 @@ class UpdateTest extends TestCase $this->withoutExceptionHandling()->login()->loginNami(); $member = $this->member(); $this->fakeRequest(); + NamiPutMemberAction::allowToRun(); $response = $this ->from("/member/{$member->id}") ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true])); $response->assertRedirect('/member'); - } - - public function testItHasPutRequest(): void - { - $this->withoutExceptionHandling()->login()->loginNami(); - $member = $this->member(); - $this->fakeRequest(); - - $response = $this - ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true, 'firstname' => '::firstname::'])); - - Http::assertSent(fn ($request) => 'PUT' === $request->method() - && '::firstname::' === $request['vorname'] - ); - } - - public function testItMergesExistingData(): void - { - $this->withoutExceptionHandling()->login()->loginNami(); - $member = $this->member(); - $this->fakeRequest(); - - $response = $this - ->from("/member/{$member->id}") - ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true, 'firstname' => '::firstname::'])); - - Http::assertSent(fn ($request) => 'PUT' === $request->method() - && '{"a":"b"}' === $request['kontoverbindung'] - && 'missingvalue' === $request['missingkey'] - && '::firstname::' === $request['vorname'] - ); + NamiPutMemberAction::spy()->shouldHaveReceived('handle')->withArgs(fn (Member $memberParam, ?Activity $activityParam, ?Subactivity $subactivityParam) => $memberParam->is($member) + && null === $activityParam + && null === $subactivityParam + )->once(); } public function testItChecksVersion(): void @@ -76,23 +53,10 @@ class UpdateTest extends TestCase $response->assertRedirect("/member/{$member->id}/edit?conflict=1"); } - public function testItUpdatesVersion(): void - { - $this->withoutExceptionHandling()->login()->loginNami(); - $member = $this->member(); - $this->fakeRequest(); - - $response = $this - ->from("/member/{$member->id}") - ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true])); - - $this->assertEquals(44, $member->fresh()->version); - } - public function testItUpdatesCriminalRecord(): void { $this->withoutExceptionHandling()->login()->loginNami(); - $member = $this->member(); + $member = $this->member(['nami_id' => null]); $this->fakeRequest(); $response = $this @@ -105,7 +69,7 @@ class UpdateTest extends TestCase 'efz' => '2021-02-03', 'without_education_at' => '2021-02-04', 'without_efz_at' => '2021-02-05', - 'has_nami' => true, + 'has_nami' => false, 'multiply_pv' => true, 'multiply_more_pv' => true, ])); @@ -121,7 +85,10 @@ class UpdateTest extends TestCase $this->assertEquals('2021-02-05', $member->fresh()->without_efz_at); } - private function member(): Member + /** + * @param array<string, string|Activity|null> $overwrites + */ + private function member(array $overwrites = []): Member { return Member::factory() ->for(Group::factory()->state(['nami_id' => 10])) @@ -129,7 +96,7 @@ class UpdateTest extends TestCase ->for(Nationality::factory()) ->for(Subscription::factory()->for(Fee::factory())) ->for(Country::factory()) - ->create(['nami_id' => 135]); + ->create(['nami_id' => 135, ...$overwrites]); } private function fakeRequest(): void diff --git a/tests/Feature/Membership/StoreTest.php b/tests/Feature/Membership/StoreTest.php index a380bd90..1b07ce9e 100644 --- a/tests/Feature/Membership/StoreTest.php +++ b/tests/Feature/Membership/StoreTest.php @@ -8,6 +8,7 @@ use App\Member\Member; use App\Subactivity; use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Tests\RequestFactories\MembershipRequestFactory; use Tests\TestCase; use Zoomyboy\LaravelNami\Fakes\MemberFake; use Zoomyboy\LaravelNami\Fakes\MembershipFake; @@ -16,27 +17,35 @@ class StoreTest extends TestCase { use DatabaseTransactions; - public function testItCanCreateAMembership(): void + public function setUp(): void { + parent::setUp(); + Carbon::setTestNow(Carbon::parse('2022-02-03 03:00:00')); - $this->withoutExceptionHandling()->login()->loginNami(); + $this->login()->loginNami(); + } + + public function testItCreatesAMembership(): void + { + $this->withoutExceptionHandling(); app(MembershipFake::class)->createsSuccessfully(6, 133); app(MemberFake::class)->shows(1400, 6, ['version' => 1506]); $member = Member::factory() ->defaults() ->for(Group::factory()->inNami(1400)) ->inNami(6) - ->createOne(); + ->create(); $activity = Activity::factory() - ->inNami(1) - ->hasAttached(Subactivity::factory()->inNami(2)) + ->inNami(6) + ->hasAttached(Subactivity::factory()->inNami(4)) ->createOne(); - $this->post("/member/{$member->id}/membership", [ - 'activity_id' => $activity->id, - 'subactivity_id' => $activity->subactivities->first()->id, - ]); + $response = $this->from('/member')->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->in($activity, $activity->subactivities->first())->create() + ); + $response->assertRedirect('/member'); $this->assertEquals(1506, $member->fresh()->version); $this->assertDatabaseHas('memberships', [ 'member_id' => $member->id, @@ -45,11 +54,123 @@ class StoreTest extends TestCase 'nami_id' => 133, ]); app(MembershipFake::class)->assertCreated(6, [ - 'untergliederungId' => 2, - 'taetigkeitId' => 1, + 'untergliederungId' => 4, + 'taetigkeitId' => 6, 'gruppierungId' => 1400, 'aktivVon' => '2022-02-03T00:00:00', 'aktivBis' => null, ]); } + + public function testActivityIsRequired(): void + { + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->inNami(6) + ->create(); + + $response = $this->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->missingAll()->create(), + ); + + $this->assertErrors(['activity_id' => 'Tätigkeit ist erforderlich.'], $response); + } + + public function testActivityShouldBeValid(): void + { + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->inNami(6) + ->create(); + + $response = $this->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->invalidActivity()->create(), + ); + + $this->assertErrors(['activity_id' => 'Tätigkeit ist nicht vorhanden.'], $response); + } + + public function testSubactivityShouldBeFromActivity(): void + { + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->inNami(6) + ->create(); + + $response = $this->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->unmatchingSubactivity()->create(), + ); + + $this->assertErrors(['subactivity_id' => 'Untertätigkeit ist nicht vorhanden.'], $response); + } + + public function testSubactivityCanBeEmpty(): void + { + $this->withoutExceptionHandling(); + app(MembershipFake::class)->createsSuccessfully(6, 133); + app(MemberFake::class)->shows(1400, 6, ['version' => 1506]); + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->inNami(6) + ->create(); + $activity = Activity::factory() + ->inNami(6) + ->createOne(); + + $this->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->in($activity, null)->create() + ); + + $this->assertEquals(1506, $member->fresh()->version); + $this->assertDatabaseHas('memberships', [ + 'member_id' => $member->id, + 'activity_id' => $activity->id, + 'subactivity_id' => null, + 'nami_id' => 133, + ]); + app(MembershipFake::class)->assertCreated(6, [ + 'untergliederungId' => null, + 'taetigkeitId' => 6, + 'gruppierungId' => 1400, + 'aktivVon' => '2022-02-03T00:00:00', + 'aktivBis' => null, + ]); + } + + /** + * @testWith ["namierror<br>", "namierror<br>"] + * ["", "Erstellen der Mitgliedschaft fehlgeschlagen"] + */ + public function testItReturnsNamiError(string $namiError, string $validationError): void + { + app(MembershipFake::class)->failsCreating(6, $namiError); + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->inNami(6) + ->create(); + $activity = Activity::factory() + ->inNami(6) + ->hasAttached(Subactivity::factory()->inNami(4)) + ->createOne(); + + $response = $this->post( + "/member/{$member->id}/membership", + MembershipRequestFactory::new()->in($activity, $activity->subactivities->first())->create() + ); + + $this->assertErrors(['nami' => $validationError], $response); + + $this->assertDatabaseMissing('memberships', [ + 'member_id' => $member->id, + ]); + } } diff --git a/tests/RequestFactories/MemberRequestFactory.php b/tests/RequestFactories/MemberRequestFactory.php new file mode 100644 index 00000000..9adb65f2 --- /dev/null +++ b/tests/RequestFactories/MemberRequestFactory.php @@ -0,0 +1,42 @@ +<?php + +namespace Tests\RequestFactories; + +use Worksome\RequestFactories\RequestFactory; + +class MemberRequestFactory extends RequestFactory +{ + public function definition(): array + { + return [ + 'address' => 'Bavert 50', + 'birthday' => '2013-02-19', + 'children_phone' => '+49 123 44444', + 'efz' => '', + 'email' => '', + 'email_parents' => 'osloot@aol.com', + 'fax' => '+49 666', + 'firstname' => 'Joe', + 'further_address' => '', + 'has_nami' => true, + 'has_svk' => false, + 'has_vk' => false, + 'joined_at' => '2022-08-12', + 'lastname' => 'Muster', + 'letter_address' => '', + 'location' => 'Solingen', + 'main_phone' => '+49 212 2334322', + 'mobile_phone' => '+49 157 53180451', + 'more_ps_at' => '', + 'multiply_more_pv' => false, + 'multiply_pv' => false, + 'other_country' => '', + 'ps_at' => '', + 'send_newspaper' => true, + 'without_education_at' => '', + 'without_efz_at' => '', + 'work_phone' => '', + 'zip' => '42719', + ]; + } +} diff --git a/tests/RequestFactories/MembershipRequestFactory.php b/tests/RequestFactories/MembershipRequestFactory.php new file mode 100644 index 00000000..6a8051cb --- /dev/null +++ b/tests/RequestFactories/MembershipRequestFactory.php @@ -0,0 +1,47 @@ +<?php + +namespace Tests\RequestFactories; + +use App\Activity; +use App\Subactivity; +use Worksome\RequestFactories\RequestFactory; + +class MembershipRequestFactory extends RequestFactory +{ + public function definition(): array + { + return []; + } + + public function in(Activity $activity, ?Subactivity $subactivity = null): self + { + return $this->state([ + 'activity_id' => $activity->id, + 'subactivity_id' => $subactivity ? $subactivity->id : null, + ]); + } + + public function missingAll(): self + { + return $this->state([ + 'activity_id' => null, + 'subactivity_id' => null, + ]); + } + + public function invalidActivity(): self + { + return $this->state([ + 'activity_id' => 10000, + 'subactivity_id' => null, + ]); + } + + public function unmatchingSubactivity(): self + { + return $this->state([ + 'activity_id' => Activity::factory()->create()->id, + 'subactivity_id' => Subactivity::factory()->create()->id, + ]); + } +}