Add Exceptions

This commit is contained in:
philipp lang 2023-02-08 01:26:01 +01:00
parent d51b0ec389
commit 5908568877
16 changed files with 217 additions and 146 deletions

View File

@ -3,7 +3,9 @@
namespace Zoomyboy\LaravelNami;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
@ -14,8 +16,12 @@ use Zoomyboy\LaravelNami\Data\Member;
use Zoomyboy\LaravelNami\Data\MemberEntry;
use Zoomyboy\LaravelNami\Data\Membership;
use Zoomyboy\LaravelNami\Data\MembershipEntry;
use Zoomyboy\LaravelNami\Exceptions\HttpException;
use Zoomyboy\LaravelNami\Exceptions\MemberDataCorruptedException;
use Zoomyboy\LaravelNami\Exceptions\NoJsonReceivedException;
use Zoomyboy\LaravelNami\Exceptions\NotAuthenticatedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Exceptions\RightException;
use Zoomyboy\LaravelNami\Support\Paginator;
class Api
@ -63,7 +69,7 @@ class Api
fn ($page, $start) => $this->http()->get($this->url.'/ica/rest/nami/search-multi/result-list?searchedValues='.rawurlencode(json_encode((object) $payload) ?: '{}').'&page='.$page.'&start='.$start.'&limit=100'),
function ($response) {
if (true !== $response->json()['success']) {
$this->exception('Search failed', '', $response->json(), []);
$this->exception(NotSuccessfulException::class, 'Search failed', '', $response->json(), []);
}
foreach ($response->json()['data'] as $member) {
yield MemberEntry::from($member);
@ -85,7 +91,7 @@ class Api
$response = $this->http()->asForm()->post($url, $payload);
if (true !== $response['success']) {
$this->exception('Deleting member failed', $url, $response->json(), $payload);
$this->exception(NotSuccessfulException::class, 'Deleting member failed', $url, $response->json(), $payload);
}
}
@ -121,7 +127,7 @@ class Api
$url = $this->url.'/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$member->groupId.'/'.$member->id;
$response = $this->http()->put($url, $payload);
if (true !== data_get($response->json(), 'success')) {
$this->exception('Update failed', $url, $response->json(), $member->toNami());
$this->exception(NotSuccessfulException::class, 'Update failed', $url, $response->json(), $member->toNami());
}
return $response->json()['data']['id'];
@ -133,7 +139,7 @@ class Api
'ersteUntergliederungId' => $firstSubactivity,
]);
if (true !== data_get($response->json(), 'success')) {
$this->exception('Update failed', $url, $response->json(), $member->toNami());
$this->exception(NotSuccessfulException::class, 'Update failed', $url, $response->json(), $member->toNami());
}
return $response->json()['data'];
@ -152,7 +158,7 @@ class Api
}
if (true !== data_get($response->json(), 'success')) {
$defaultError = 'Erstellen der Mitgliedschaft fehlgeschlagen';
$this->exception(data_get($response->json(), 'message', $defaultError) ?: $defaultError, $url, $response->json(), $data->toArray());
$this->exception(NotSuccessfulException::class, data_get($response->json(), 'message', $defaultError) ?: $defaultError, $url, $response->json(), $data->toArray());
}
if (data_get($data, 'id')) {
@ -180,12 +186,13 @@ class Api
public function deleteMembership(int $memberId, Membership $membership): void
{
$this->assertLoggedIn();
$url = "/ica/rest/nami/zugeordnete-taetigkeiten/filtered-for-navigation/gruppierung-mitglied/mitglied/{$memberId}/{$membership->id}";
try {
$this->delete("/ica/rest/nami/zugeordnete-taetigkeiten/filtered-for-navigation/gruppierung-mitglied/mitglied/{$memberId}/{$membership->id}", 'Deleting membership failed');
} catch (NamiException $e) {
$this->delete($url, 'Deleting membership failed');
} catch (NotSuccessfulException $e) {
if (is_null($membership->id)) {
throw new NamiException('ID not given in membership');
throw new Exception('ID not given in Membership');
}
$membership->endsAt = today();
$this->putMembership($memberId, $membership);
@ -202,7 +209,7 @@ class Api
)->map(fn ($subactivity) => Subactivity::fromNami($subactivity));
}
public function membership(int $memberId, int $membershipId): ?Membership
public function membership(int $memberId, int $membershipId): Membership
{
$this->assertLoggedIn();
$membership = $this->fetchData(
@ -210,7 +217,7 @@ class Api
"Fails fetching membership {$membershipId} for {$memberId}",
);
return $membership ? Membership::from($membership) : null;
return Membership::from($membership);
}
public function courses(): Collection
@ -229,15 +236,15 @@ class Api
$this->assertLoggedIn();
return $this->fetchCollection("/ica/rest/nami/mitglied-ausbildung/filtered-for-navigation/mitglied/mitglied/{$memberId}/flist", 'Courses fetch failed')
->map(fn ($course) => $this->course($memberId, $course['id']))
->map(fn ($course) => rescue(fn () => $this->course($memberId, $course['id']), null))
->filter(fn ($course) => null !== $course);
}
public function course(int $memberId, int $courseId): ?Course
public function course(int $memberId, int $courseId): Course
{
$single = $this->fetchData("/ica/rest/nami/mitglied-ausbildung/filtered-for-navigation/mitglied/mitglied/{$memberId}/{$courseId}", 'Error fetching single course');
return $single ? Course::from($single) : null;
return Course::from($single);
}
/**
@ -256,7 +263,7 @@ class Api
$response = $this->http()->post($url, $payload);
if (true !== data_get($response->json(), 'success')) {
$this->exception('Course creation failed', $url, $response->json(), $payload);
$this->exception(NotSuccessfulException::class, 'Course creation failed', $url, $response->json(), $payload);
}
return $response['data'];
@ -278,7 +285,7 @@ class Api
$response = $this->http()->put($url, $payload);
if (true !== data_get($response->json(), 'success')) {
$this->exception('Course update failed', $url, $response->json(), $payload);
$this->exception(NotSuccessfulException::class, 'Course update failed', $url, $response->json(), $payload);
}
}
@ -289,7 +296,7 @@ class Api
$response = $this->http()->delete($url);
if (null !== $response->json() && true !== data_get($response->json(), 'success')) {
$this->exception('Course deletion failed', $url, $response->json());
$this->exception(NotSuccessfulException::class, 'Course deletion failed', $url, $response->json());
}
}
@ -383,7 +390,7 @@ class Api
->map(fn ($confession) => Confession::fromNami($confession));
}
public function activities($groupId)
public function activities(int $groupId): Collection
{
$this->assertLoggedIn();
@ -391,40 +398,27 @@ class Api
->map(fn ($activity) => Activity::fromNami($activity));
}
public function memberOverviewOf(int $groupId): Collection
/**
* @param class-string<HttpException> $e
* @param array<string, mixed> $response
* @param array<string, mixed> $requestData
*/
private function exception(string $e, string $message, string $url, array $response, array $requestData = []): void
{
throw (new $e($message))->response($response)->request($url, $requestData);
}
/**
* @return array<string, mixed>
*/
private function rawMember(int $groupId, int $memberId): array
{
$this->assertLoggedIn();
return $this->fetchCollection('/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$groupId.'/flist', 'Fetch membership overview failed')
->map(function ($member) use ($groupId) {
$member = collect($member)->mapWithKeys(function ($value, $key) {
return [str_replace('entries_', '', $key) => $value];
});
$member['gruppierungId'] = $groupId;
return $member;
});
}
private function singleMemberFallback(int $groupId, int $memberId): array
{
$this->assertLoggedIn();
$member = $this->fetchCollection('/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$groupId.'/flist', 'Fetch single member fallback failed')->first(function ($member) use ($memberId) {
return $member['id'] == $memberId;
});
$member = collect($member)->mapWithKeys(function ($value, $key) {
return [str_replace('entries_', '', $key) => $value];
});
$member['gruppierungId'] = $groupId;
return $member->toArray();
}
private function exception(string $message, string $url, array $response, array $requestData = []): void
{
throw (new NamiException($message))->response($response)->request($url, $requestData);
return $this->fetchData(
'/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$groupId.'/'.$memberId,
'Fetch von Mitglied '.$memberId.' in Gruppe '.$groupId.' fehlgeschlagen'
);
}
private function assertLoggedIn(): void
@ -436,50 +430,30 @@ class Api
}
}
/**
* @return Collection<int, mixed>
*/
private function fetchCollection(string $url, string $error): Collection
{
$response = $this->http()->get($this->url.$url);
if (null === $response->json()) {
return collect([]);
}
$this->assertOk($response, $url, $error);
/** @var array<int, mixed> */
$data = $response['data'];
if (data_get($response, 'message') && Str::contains($response['message'], 'no right')) {
return collect([]);
}
if (data_get($response, 'message') && Str::contains($response['message'], 'Sicherheitsverletzung')) {
return collect([]);
}
if (false === $response['success']) {
$this->exception($error, $url, $response->json());
}
return collect($response['data']);
return collect($data);
}
private function fetchData(string $url, string $error): ?array
/**
* @return array<string, mixed>
*/
private function fetchData(string $url, string $error): array
{
$response = $this->http()->get($this->url.$url);
if (null === $response->json()) {
return null;
}
$this->assertOk($response, $url, $error);
if (data_get($response, 'message') && Str::contains($response['message'], 'no right')) {
return null;
}
if (data_get($response, 'message') && Str::contains($response['message'], 'Sicherheitsverletzung')) {
return null;
}
if (false === $response['success']) {
$this->exception($error, $url, $response->json());
}
return $response['data'];
return $response->json()['data'];
}
private function delete(string $url, string $error): void
@ -489,36 +463,25 @@ class Api
'accept' => 'application/json',
])->delete($this->url.$url);
$this->assertOk($response, $url, $error);
}
private function assertOk(Response $response, string $url, string $error): void
{
if (null === $response->json()) {
$this->exception($error, $url, []);
$this->exception(NoJsonReceivedException::class, $error, $url, []);
}
if (data_get($response, 'message') && Str::contains($response['message'], 'no right')) {
$this->exception(RightException::class, $error, $url, $response->json());
}
if (data_get($response, 'message') && Str::contains($response['message'], 'Sicherheitsverletzung')) {
$this->exception(RightException::class, $error, $url, $response->json());
}
if (false === $response['success']) {
$this->exception($error, $url, $response->json());
$this->exception(NotSuccessfulException::class, $error, $url, $response->json());
}
}
/**
* @return array<string, mixed>
*/
private function rawMember(int $groupId, int $memberId): array
{
$this->assertLoggedIn();
$url = $this->url.'/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$groupId.'/'.$memberId;
$response = $this->http()->get($url);
if (false === $response->json()['success'] && Str::startsWith($response['message'], 'Access denied')) {
return $this->singleMemberFallback($groupId, $memberId);
}
if (false === $response->json()['success'] && Str::startsWith($response['message'], 'Sicherheitsverletzung: Zugriff')) {
return $this->singleMemberFallback($groupId, $memberId);
}
if (true !== $response->json()['success']) {
$this->exception('Fetching member failed', $url, $response->json());
}
return $response->json()['data'];
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Zoomyboy\LaravelNami\Exceptions;
class DeletingMemberFailedException extends HttpException
{
}

View File

@ -0,0 +1,82 @@
<?php
namespace Zoomyboy\LaravelNami\Exceptions;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
abstract class HttpException extends Exception
{
/** @var array<string, mixed> */
private array $data;
/** @var array<string, mixed> */
private array $response;
private string $requestUrl;
/**
* @param array<string, mixed> $data
*/
public function setData(array $data): self
{
$this->data = $data;
return $this;
}
/**
* @return array<string, mixed>
*/
public function getData(): array
{
return $this->data;
}
/**
* @param array<string, mixed>|null $data
*/
public function request(string $url, ?array $data = []): self
{
$this->requestUrl = $url;
$this->data = $data;
return $this;
}
/**
* @param array<string, mixed> $response
*/
public function response(array $response): self
{
$this->response = $response;
return $this;
}
public function isConflict(): bool
{
return Str::contains(data_get($this->response, 'message'), 'Der Datensatz wurde zwischenzeitlich verändert');
}
public function report(): void
{
\Log::error($this->getMessage(), [
'requestUrl' => $this->requestUrl,
'data' => $this->data,
'response' => json_encode($this->response),
]);
}
public function render(): void
{
throw ValidationException::withMessages(['id' => 'Unbekannter Fehler']);
}
public function outputToConsole(Command $command): void
{
$command->info('Request URL: '.$this->requestUrl);
$command->info('response: '.json_encode($this->response));
$command->info($this->getMessage());
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Zoomyboy\LaravelNami\Exceptions;
class NoJsonReceivedException extends HttpException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Zoomyboy\LaravelNami\Exceptions;
class NotSuccessfulException extends HttpException
{
}

View File

@ -2,8 +2,6 @@
namespace Zoomyboy\LaravelNami\Exceptions;
use Exception;
class RightException extends Exception
class RightException extends HttpException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Zoomyboy\LaravelNami\Exceptions;
class SearchFailedException extends HttpException
{
}

View File

@ -75,13 +75,6 @@ class Group implements Arrayable
return Member::fromNami(Nami::member($this->id, $id));
}
public function memberOverview(): Collection
{
return Nami::memberOverviewOf($this->id)->map(function ($member) {
return Member::fromNami($member);
});
}
public function activities(): Collection
{
return Nami::activities($this->id);

View File

@ -6,6 +6,9 @@ use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
/**
* @todo remove this class
*/
class NamiException extends \Exception
{
private array $data;

View File

@ -3,8 +3,8 @@
namespace Zoomyboy\LaravelNami\Tests\Unit\Member;
use Zoomyboy\LaravelNami\Exceptions\MemberDataCorruptedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\MemberFake;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class FetchMemberTest extends TestCase
@ -128,7 +128,7 @@ class FetchMemberTest extends TestCase
public function testMemberFetchCanFail(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
app(MemberFake::class)->fetchFails(103, 16);
$this->login()->member(103, 16);

View File

@ -3,9 +3,12 @@
namespace Zoomyboy\LaravelNami\Tests\Unit\Api;
use Carbon\Carbon;
use Exception;
use Zoomyboy\LaravelNami\Data\Membership;
use Zoomyboy\LaravelNami\Exceptions\NoJsonReceivedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Exceptions\RightException;
use Zoomyboy\LaravelNami\Fakes\MembershipFake;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class MembershipTest extends TestCase
@ -47,18 +50,17 @@ class MembershipTest extends TestCase
public function testThrowExceptionWhenFetchingFails(): void
{
app(MembershipFake::class)->failsShowing(6, 11);
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
$this->login()->membership(6, 11);
}
public function testReturnsNothingWhenFetchingFailsWithHtml(): void
{
$this->expectException(NoJsonReceivedException::class);
app(MembershipFake::class)->failsShowingWithHtml(6, 11);
$membership = $this->login()->membership(6, 11);
$this->assertNull($membership);
$this->login()->membership(6, 11);
}
/**
@ -66,6 +68,7 @@ class MembershipTest extends TestCase
*/
public function testItGetsNoMembershipsWithNoRights(string $error): void
{
$this->expectException(RightException::class);
app(MembershipFake::class)->failsShowing(16, 68, $error);
$membership = $this->login()->membership(16, 68);
@ -137,7 +140,7 @@ class MembershipTest extends TestCase
public function testItDoesntUpdateMembershipWhenNoIdGiven(): void
{
$this->expectException(NamiException::class);
$this->expectException(Exception::class);
Carbon::setTestNow(Carbon::parse('2022-02-03 03:00:00'));
app(MembershipFake::class)->failsDeleting(6, null);
app(MembershipFake::class)->updatesSuccessfully(6, null);

View File

@ -3,9 +3,9 @@
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Zoomyboy\LaravelNami\Authentication\Auth;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\BausteinFake;
use Zoomyboy\LaravelNami\Nami;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class BausteinTest extends TestCase
@ -32,7 +32,7 @@ class BausteinTest extends TestCase
public function testThrowExceptionWhenBausteinFetchingFails(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
Auth::success(12345, 'secret');
app(BausteinFake::class)->failsToFetch();

View File

@ -3,11 +3,12 @@
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Zoomyboy\LaravelNami\Data\Course;
use Zoomyboy\LaravelNami\Exceptions\NoJsonReceivedException;
use Zoomyboy\LaravelNami\Exceptions\NotAuthenticatedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\CourseFake;
use Zoomyboy\LaravelNami\LoginException;
use Zoomyboy\LaravelNami\Nami;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class CourseTest extends TestCase
@ -62,11 +63,10 @@ class CourseTest extends TestCase
public function testReturnEmptyWhenCourseIndexReturnsHtml(): void
{
$this->expectException(NoJsonReceivedException::class);
app(CourseFake::class)->failsFetchingWithHtml(11111);
$courses = $this->login()->coursesFor(11111);
$this->assertCount(0, $courses);
$this->login()->coursesFor(11111);
}
public function testItNeedsLoginToGetCourses(): void
@ -128,7 +128,7 @@ class CourseTest extends TestCase
public function testThrowExceptionWhenCourseUpdateFailed(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
app(CourseFake::class)->failsUpdating(123, 999);
$this->login()->updateCourse(123, 999, [
@ -160,7 +160,7 @@ class CourseTest extends TestCase
public function testThrowExceptionWhenStoringFailed(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
app(CourseFake::class)->failsCreating(123);
$this->login()->createCourse(123, [
@ -182,7 +182,7 @@ class CourseTest extends TestCase
public function testShrowExceptionWhenDeletingFailed(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
app(CourseFake::class)->failsDeleting(123, 999);
$this->login()->deleteCourse(123, 999);

View File

@ -3,11 +3,12 @@
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Zoomyboy\LaravelNami\Authentication\Auth;
use Zoomyboy\LaravelNami\Exceptions\NoJsonReceivedException;
use Zoomyboy\LaravelNami\Exceptions\NotAuthenticatedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\GroupFake;
use Zoomyboy\LaravelNami\Group;
use Zoomyboy\LaravelNami\Nami;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class GroupsTest extends TestCase
@ -60,7 +61,7 @@ class GroupsTest extends TestCase
public function testThrowsExceptionWhenGroupFetchFailed(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
Auth::success(12345, 'secret');
app(GroupFake::class)->failsToFetch(null);
@ -69,7 +70,7 @@ class GroupsTest extends TestCase
public function testThrowsExceptionWhenSubgroupFetchFailed(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
Auth::success(12345, 'secret');
app(GroupFake::class)->fetches(null, [
1234 => ['name' => 'testgroup'],
@ -81,10 +82,10 @@ class GroupsTest extends TestCase
public function testItDoesntReturnGroupWhenNoJsonIsReturned(): void
{
$this->expectException(NoJsonReceivedException::class);
Auth::success(12345, 'secret');
app(GroupFake::class)->failsToFetchWithoutJson(null);
$group = Nami::login(12345, 'secret')->group(1234);
$this->assertNull($group);
Nami::login(12345, 'secret')->group(1234);
}
}

View File

@ -3,8 +3,9 @@
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Illuminate\Support\Facades\Http;
use Zoomyboy\LaravelNami\Exceptions\NoJsonReceivedException;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\SubactivityFake;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class PullActivitiesTest extends TestCase
@ -46,7 +47,7 @@ class PullActivitiesTest extends TestCase
public function testThrowErrorWhenSubactivitiesRequestFails(): void
{
$this->expectException(NamiException::class);
$this->expectException(NotSuccessfulException::class);
app(SubactivityFake::class)->fetchFails(4, 'sorry dude');
$subactivities = $this->login()->subactivitiesOf(4);
@ -54,10 +55,9 @@ class PullActivitiesTest extends TestCase
public function testContinueIfSubactivitiesRequestReturnsHtml(): void
{
$this->expectException(NoJsonReceivedException::class);
app(SubactivityFake::class)->fetchFailsWithoutJson(4);
$subactivities = $this->login()->subactivitiesOf(4);
$this->assertCount(0, $subactivities);
$this->login()->subactivitiesOf(4);
}
}

View File

@ -3,8 +3,8 @@
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Illuminate\Support\Facades\Http;
use Zoomyboy\LaravelNami\Exceptions\NotSuccessfulException;
use Zoomyboy\LaravelNami\Fakes\SearchFake;
use Zoomyboy\LaravelNami\NamiException;
use Zoomyboy\LaravelNami\Tests\TestCase;
class SearchTest extends TestCase
@ -61,7 +61,7 @@ class SearchTest extends TestCase
public function testItThrowsExceptionWhenSearchFails(): void
{
$this->withoutExceptionHandling()->expectException(NamiException::class);
$this->withoutExceptionHandling()->expectException(NotSuccessfulException::class);
app(SearchFake::class)->fetchFails($page = 1, $start = 0, 'unknown error');
$this->login()->search([])->first();