feat: add Pinterest, Bluesky, Mastodon publish exceptions
This commit is contained in:
parent
6587c76bc2
commit
9999335da6
6 changed files with 503 additions and 0 deletions
77
app/Exceptions/Social/BlueskyPublishException.php
Normal file
77
app/Exceptions/Social/BlueskyPublishException.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Social;
|
||||
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class BlueskyPublishException extends SocialPublishException
|
||||
{
|
||||
public static function fromApiResponse(mixed $response): static
|
||||
{
|
||||
/** @var Response $response */
|
||||
$status = $response->status();
|
||||
$body = $response->json();
|
||||
$rawResponse = $response->body();
|
||||
|
||||
$error = data_get($body, 'error', '');
|
||||
$errorMessage = data_get($body, 'message', 'An unknown Bluesky error occurred.');
|
||||
|
||||
if (in_array($error, ['ExpiredToken', 'InvalidToken'], true)) {
|
||||
throw new TokenExpiredException(
|
||||
message: $errorMessage,
|
||||
platformErrorCode: $error,
|
||||
);
|
||||
}
|
||||
|
||||
if (str_contains($rawResponse, 'BlobTooLarge') || $status === 413) {
|
||||
return new static(
|
||||
userMessage: "Image exceeds Bluesky's 1MB limit.",
|
||||
category: ErrorCategory::MediaFormat,
|
||||
platformErrorCode: $error ?: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($error === 'InvalidRequest') {
|
||||
return new static(
|
||||
userMessage: 'Invalid post data.',
|
||||
category: ErrorCategory::ContentPolicy,
|
||||
platformErrorCode: $error,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 429) {
|
||||
return new static(
|
||||
userMessage: 'Rate limit exceeded. Please try again later.',
|
||||
category: ErrorCategory::RateLimit,
|
||||
platformErrorCode: $error ?: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status >= 500) {
|
||||
return new static(
|
||||
userMessage: 'Bluesky server error. Please try again.',
|
||||
category: ErrorCategory::ServerError,
|
||||
platformErrorCode: $error ?: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
userMessage: $errorMessage,
|
||||
category: ErrorCategory::Unknown,
|
||||
platformErrorCode: $error ?: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public function platform(): string
|
||||
{
|
||||
return 'bluesky';
|
||||
}
|
||||
}
|
||||
94
app/Exceptions/Social/MastodonPublishException.php
Normal file
94
app/Exceptions/Social/MastodonPublishException.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Social;
|
||||
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class MastodonPublishException extends SocialPublishException
|
||||
{
|
||||
public static function fromApiResponse(mixed $response): static
|
||||
{
|
||||
/** @var Response $response */
|
||||
$status = $response->status();
|
||||
$body = $response->json();
|
||||
$rawResponse = $response->body();
|
||||
|
||||
$errorMessage = data_get($body, 'error', 'An unknown Mastodon error occurred.');
|
||||
|
||||
if ($status === 401) {
|
||||
throw new TokenExpiredException(
|
||||
message: $errorMessage,
|
||||
platformErrorCode: (string) $status,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 403) {
|
||||
return new static(
|
||||
userMessage: 'This action is not allowed.',
|
||||
category: ErrorCategory::Permission,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 422 && str_contains(strtolower($rawResponse), "text can't be blank")) {
|
||||
return new static(
|
||||
userMessage: 'Post text is required when no media is attached.',
|
||||
category: ErrorCategory::ContentPolicy,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 422) {
|
||||
return new static(
|
||||
userMessage: 'Media validation failed.',
|
||||
category: ErrorCategory::MediaFormat,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 413) {
|
||||
return new static(
|
||||
userMessage: 'File is too large for this Mastodon instance.',
|
||||
category: ErrorCategory::MediaFormat,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 429) {
|
||||
return new static(
|
||||
userMessage: 'Rate limit exceeded. Please try again later.',
|
||||
category: ErrorCategory::RateLimit,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 503) {
|
||||
return new static(
|
||||
userMessage: 'Mastodon server error. Please try again.',
|
||||
category: ErrorCategory::ServerError,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
userMessage: $errorMessage,
|
||||
category: ErrorCategory::Unknown,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public function platform(): string
|
||||
{
|
||||
return 'mastodon';
|
||||
}
|
||||
}
|
||||
93
app/Exceptions/Social/PinterestPublishException.php
Normal file
93
app/Exceptions/Social/PinterestPublishException.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Social;
|
||||
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class PinterestPublishException extends SocialPublishException
|
||||
{
|
||||
public static function fromApiResponse(mixed $response): static
|
||||
{
|
||||
/** @var Response $response */
|
||||
$status = $response->status();
|
||||
$body = $response->json();
|
||||
$rawResponse = $response->body();
|
||||
|
||||
if ($status === 401) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($body, 'message', 'Access token has expired or been revoked'),
|
||||
platformErrorCode: (string) $status,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 403) {
|
||||
return new static(
|
||||
userMessage: 'Not authorized to create pins on this board.',
|
||||
category: ErrorCategory::Permission,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 429) {
|
||||
return new static(
|
||||
userMessage: 'Rate limit exceeded. Please try again later.',
|
||||
category: ErrorCategory::RateLimit,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status === 400 && str_contains(strtolower($rawResponse), 'board')) {
|
||||
return new static(
|
||||
userMessage: 'Invalid board. Please select a valid board.',
|
||||
category: ErrorCategory::ContentPolicy,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if ($status >= 500) {
|
||||
return new static(
|
||||
userMessage: 'Pinterest server error. Please try again.',
|
||||
category: ErrorCategory::ServerError,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
userMessage: $rawResponse,
|
||||
category: ErrorCategory::Unknown,
|
||||
platformErrorCode: (string) $status,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromProcessingStatus(string $status, ?string $rawResponse = null): static
|
||||
{
|
||||
if ($status === 'failed') {
|
||||
return new static(
|
||||
userMessage: 'Media processing failed. Please try a different file.',
|
||||
category: ErrorCategory::MediaFormat,
|
||||
platformErrorCode: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
userMessage: $rawResponse ?? 'An unknown Pinterest error occurred.',
|
||||
category: ErrorCategory::Unknown,
|
||||
platformErrorCode: null,
|
||||
rawResponse: $rawResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public function platform(): string
|
||||
{
|
||||
return 'pinterest';
|
||||
}
|
||||
}
|
||||
81
tests/Unit/Exceptions/Social/BlueskyPublishExceptionTest.php
Normal file
81
tests/Unit/Exceptions/Social/BlueskyPublishExceptionTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Exceptions\Social\BlueskyPublishException;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
test('ExpiredToken error throws TokenExpiredException', function () {
|
||||
$response = Http::response(['error' => 'ExpiredToken', 'message' => 'Token has expired.'], 401);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('InvalidToken error throws TokenExpiredException', function () {
|
||||
$response = Http::response(['error' => 'InvalidToken', 'message' => 'Token is invalid.'], 401);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('BlobTooLarge in body maps to MediaFormat category', function () {
|
||||
$response = Http::response(['error' => 'BlobTooLarge', 'message' => 'Blob too large.'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::MediaFormat)
|
||||
->and($exception->userMessage)->toBe("Image exceeds Bluesky's 1MB limit.");
|
||||
});
|
||||
|
||||
test('HTTP 429 maps to RateLimit category', function () {
|
||||
$response = Http::response(['error' => 'RateLimitExceeded', 'message' => 'Too many requests.'], 429);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::RateLimit)
|
||||
->and($exception->userMessage)->toBe('Rate limit exceeded. Please try again later.');
|
||||
});
|
||||
|
||||
test('unknown error maps to Unknown category with error message', function () {
|
||||
$response = Http::response(['error' => 'SomeUnknownError', 'message' => 'Something went wrong.'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Unknown)
|
||||
->and($exception->userMessage)->toBe('Something went wrong.');
|
||||
});
|
||||
|
||||
test('InvalidRequest error maps to ContentPolicy category', function () {
|
||||
$response = Http::response(['error' => 'InvalidRequest', 'message' => 'Post data is invalid.'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ContentPolicy)
|
||||
->and($exception->userMessage)->toBe('Invalid post data.');
|
||||
});
|
||||
|
||||
test('HTTP 500 maps to ServerError category', function () {
|
||||
$response = Http::response(['error' => 'InternalServerError', 'message' => 'Server error.'], 500);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ServerError)
|
||||
->and($exception->userMessage)->toBe('Bluesky server error. Please try again.');
|
||||
});
|
||||
|
||||
test('platform returns bluesky', function () {
|
||||
$response = Http::response(['error' => 'SomeError', 'message' => 'Error.'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://bsky.social/xrpc/test');
|
||||
|
||||
$exception = BlueskyPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->platform())->toBe('bluesky');
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\MastodonPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
test('HTTP 401 throws TokenExpiredException', function () {
|
||||
$response = Http::response(['error' => 'The access token is invalid'], 401);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('HTTP 403 maps to Permission category', function () {
|
||||
$response = Http::response(['error' => 'This action is not allowed'], 403);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Permission)
|
||||
->and($exception->userMessage)->toBe('This action is not allowed.');
|
||||
});
|
||||
|
||||
test('HTTP 422 with text can\'t be blank maps to ContentPolicy category', function () {
|
||||
$response = Http::response(['error' => "text can't be blank"], 422);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ContentPolicy)
|
||||
->and($exception->userMessage)->toBe('Post text is required when no media is attached.');
|
||||
});
|
||||
|
||||
test('HTTP 413 maps to MediaFormat category', function () {
|
||||
$response = Http::response(['error' => 'File too large'], 413);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::MediaFormat)
|
||||
->and($exception->userMessage)->toBe('File is too large for this Mastodon instance.');
|
||||
});
|
||||
|
||||
test('HTTP 429 maps to RateLimit category', function () {
|
||||
$response = Http::response(['error' => 'Too many requests'], 429);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::RateLimit)
|
||||
->and($exception->userMessage)->toBe('Rate limit exceeded. Please try again later.');
|
||||
});
|
||||
|
||||
test('HTTP 422 without text blank maps to MediaFormat category', function () {
|
||||
$response = Http::response(['error' => 'Validation failed'], 422);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::MediaFormat)
|
||||
->and($exception->userMessage)->toBe('Media validation failed.');
|
||||
});
|
||||
|
||||
test('unknown status maps to Unknown category with error message', function () {
|
||||
$response = Http::response(['error' => 'Something went wrong'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Unknown)
|
||||
->and($exception->userMessage)->toBe('Something went wrong');
|
||||
});
|
||||
|
||||
test('platform returns mastodon', function () {
|
||||
$response = Http::response(['error' => 'Some error'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://mastodon.social/api/v1/statuses');
|
||||
|
||||
$exception = MastodonPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->platform())->toBe('mastodon');
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\PinterestPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
test('HTTP 401 throws TokenExpiredException', function () {
|
||||
$response = Http::response(['message' => 'Access token expired.'], 401);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');
|
||||
|
||||
PinterestPublishException::fromApiResponse($fakeResponse);
|
||||
})->throws(TokenExpiredException::class);
|
||||
|
||||
test('HTTP 429 maps to RateLimit category', function () {
|
||||
$response = Http::response(['message' => 'Too many requests.'], 429);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');
|
||||
|
||||
$exception = PinterestPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::RateLimit)
|
||||
->and($exception->userMessage)->toBe('Rate limit exceeded. Please try again later.');
|
||||
});
|
||||
|
||||
test('HTTP 403 maps to Permission category', function () {
|
||||
$response = Http::response(['message' => 'Forbidden.'], 403);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');
|
||||
|
||||
$exception = PinterestPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Permission)
|
||||
->and($exception->userMessage)->toBe('Not authorized to create pins on this board.');
|
||||
});
|
||||
|
||||
test('HTTP 400 with board in body maps to ContentPolicy category', function () {
|
||||
$response = Http::response(['message' => 'Invalid board_id provided.'], 400);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');
|
||||
|
||||
$exception = PinterestPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ContentPolicy)
|
||||
->and($exception->userMessage)->toBe('Invalid board. Please select a valid board.');
|
||||
});
|
||||
|
||||
test('processing status failed maps to MediaFormat category', function () {
|
||||
$exception = PinterestPublishException::fromProcessingStatus('failed');
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::MediaFormat)
|
||||
->and($exception->userMessage)->toBe('Media processing failed. Please try a different file.');
|
||||
});
|
||||
|
||||
test('processing status unknown maps to Unknown category', function () {
|
||||
$exception = PinterestPublishException::fromProcessingStatus('pending', 'raw response');
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::Unknown);
|
||||
});
|
||||
|
||||
test('HTTP 500 maps to ServerError category', function () {
|
||||
$response = Http::response(['message' => 'Internal server error.'], 500);
|
||||
$fakeResponse = Http::fake(['*' => $response])->post('https://api.pinterest.com/test');
|
||||
|
||||
$exception = PinterestPublishException::fromApiResponse($fakeResponse);
|
||||
|
||||
expect($exception->category)->toBe(ErrorCategory::ServerError)
|
||||
->and($exception->userMessage)->toBe('Pinterest server error. Please try again.');
|
||||
});
|
||||
|
||||
test('platform returns pinterest', function () {
|
||||
$exception = PinterestPublishException::fromProcessingStatus('unknown');
|
||||
|
||||
expect($exception->platform())->toBe('pinterest');
|
||||
});
|
||||
Loading…
Reference in a new issue