diff --git a/app/Exceptions/InvalidTokenScopeException.php b/app/Exceptions/InvalidTokenScopeException.php deleted file mode 100644 index 5966bccd..00000000 --- a/app/Exceptions/InvalidTokenScopeException.php +++ /dev/null @@ -1,7 +0,0 @@ -handlerRegistry = $handlerRegistry; + protected HEntryService $hentryService; + + protected HCardService $hcardService; + + protected UpdateService $updateService; + + public function __construct( + TokenService $tokenService, + HEntryService $hentryService, + HCardService $hcardService, + UpdateService $updateService + ) { + $this->tokenService = $tokenService; + $this->hentryService = $hentryService; + $this->hcardService = $hcardService; + $this->updateService = $updateService; } /** - * Respond to a POST request to the micropub endpoint. - * - * The request is initially processed by the MicropubRequest form request - * class. The normalizes the data, so we can pass it into the handlers for - * the different micropub requests, h-entry or h-card, for example. + * This function receives an API request, verifies the authenticity + * then passes over the info to the relevant Service class. */ - public function post(MicropubRequest $request): JsonResponse + public function post(Request $request): JsonResponse { - $type = $request->getType(); - - if (! $type) { - return response()->json([ - 'error' => 'invalid_request', - 'error_description' => 'Microformat object type is missing, for example: h-entry or h-card', - ], 400); - } - try { - $handler = $this->handlerRegistry->getHandler($type); - $result = $handler->handle($request->getMicropubData()); + $tokenData = $this->tokenService->validateToken($request->input('access_token')); + } catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) { + $micropubResponses = new MicropubResponses; - // Return appropriate response based on the handler result - return response()->json([ - 'response' => $result['response'], - 'location' => $result['url'] ?? null, - ], 201)->header('Location', $result['url']); - } catch (\InvalidArgumentException $e) { - return response()->json([ - 'error' => 'invalid_request', - 'error_description' => $e->getMessage(), - ], 400); - } catch (MicropubHandlerException) { - return response()->json([ - 'error' => 'Unknown Micropub type', - 'error_description' => 'The request could not be processed by this server', - ], 500); - } catch (InvalidTokenScopeException) { - return response()->json([ - 'error' => 'invalid_scope', - 'error_description' => 'The token does not have the required scope for this request', - ], 403); - } catch (\Exception) { - return response()->json([ - 'error' => 'server_error', - 'error_description' => 'An error occurred processing the request', - ], 500); + return $micropubResponses->invalidTokenResponse(); } + + if ($tokenData->claims()->has('scope') === false) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->tokenHasNoScopeResponse(); + } + + $this->logMicropubRequest($request->all()); + + if (($request->input('h') === 'entry') || ($request->input('type.0') === 'h-entry')) { + $scopes = $tokenData->claims()->get('scope'); + if (is_string($scopes)) { + $scopes = explode(' ', $scopes); + } + + if (! in_array('create', $scopes)) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->insufficientScopeResponse(); + } + $location = $this->hentryService->process($request->all(), $this->getCLientId()); + + return response()->json([ + 'response' => 'created', + 'location' => $location, + ], 201)->header('Location', $location); + } + + if ($request->input('h') === 'card' || $request->input('type.0') === 'h-card') { + $scopes = $tokenData->claims()->get('scope'); + if (is_string($scopes)) { + $scopes = explode(' ', $scopes); + } + if (! in_array('create', $scopes)) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->insufficientScopeResponse(); + } + $location = $this->hcardService->process($request->all()); + + return response()->json([ + 'response' => 'created', + 'location' => $location, + ], 201)->header('Location', $location); + } + + if ($request->input('action') === 'update') { + $scopes = $tokenData->claims()->get('scope'); + if (is_string($scopes)) { + $scopes = explode(' ', $scopes); + } + if (! in_array('update', $scopes)) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->insufficientScopeResponse(); + } + + return $this->updateService->process($request->all()); + } + + return response()->json([ + 'response' => 'error', + 'error_description' => 'unsupported_request_type', + ], 500); } /** @@ -83,6 +130,12 @@ class MicropubController extends Controller */ public function get(Request $request): JsonResponse { + try { + $tokenData = $this->tokenService->validateToken($request->input('access_token')); + } catch (RequiredConstraintsViolated|InvalidTokenStructure) { + return (new MicropubResponses)->invalidTokenResponse(); + } + if ($request->input('q') === 'syndicate-to') { return response()->json([ 'syndicate-to' => SyndicationTarget::all(), @@ -114,17 +167,36 @@ class MicropubController extends Controller ]); } - // the default response is just to return the token data - /** @var Token $tokenData */ - $tokenData = $request->input('token_data'); - + // default response is just to return the token data return response()->json([ 'response' => 'token', 'token' => [ - 'me' => $tokenData['me'], - 'scope' => $tokenData['scope'], - 'client_id' => $tokenData['client_id'], + 'me' => $tokenData->claims()->get('me'), + 'scope' => $tokenData->claims()->get('scope'), + 'client_id' => $tokenData->claims()->get('client_id'), ], ]); } + + /** + * Determine the client id from the access token sent with the request. + * + * @throws RequiredConstraintsViolated + */ + private function getClientId(): string + { + return resolve(TokenService::class) + ->validateToken(app('request')->input('access_token')) + ->claims()->get('client_id'); + } + + /** + * Save the details of the micropub request to a log file. + */ + private function logMicropubRequest(array $request): void + { + $logger = new Logger('micropub'); + $logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log'))); + $logger->debug('MicropubLog', $request); + } } diff --git a/app/Http/Controllers/MicropubMediaController.php b/app/Http/Controllers/MicropubMediaController.php index fc804ea2..430ba3ae 100644 --- a/app/Http/Controllers/MicropubMediaController.php +++ b/app/Http/Controllers/MicropubMediaController.php @@ -7,8 +7,10 @@ namespace App\Http\Controllers; use App\Http\Responses\MicropubResponses; use App\Jobs\ProcessMedia; use App\Models\Media; +use App\Services\TokenService; use Exception; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Http\File; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -16,20 +18,43 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; use Intervention\Image\ImageManager; +use Lcobucci\JWT\Token\InvalidTokenStructure; +use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use Ramsey\Uuid\Uuid; class MicropubMediaController extends Controller { + protected TokenService $tokenService; + + public function __construct(TokenService $tokenService) + { + $this->tokenService = $tokenService; + } + public function getHandler(Request $request): JsonResponse { - $tokenData = $request->input('token_data'); + try { + $tokenData = $this->tokenService->validateToken($request->input('access_token')); + } catch (RequiredConstraintsViolated|InvalidTokenStructure) { + $micropubResponses = new MicropubResponses; - $scopes = $tokenData['scope']; + return $micropubResponses->invalidTokenResponse(); + } + + if ($tokenData->claims()->has('scope') === false) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->tokenHasNoScopeResponse(); + } + + $scopes = $tokenData->claims()->get('scope'); if (is_string($scopes)) { $scopes = explode(' ', $scopes); } - if (! in_array('create', $scopes, true)) { - return (new MicropubResponses)->insufficientScopeResponse(); + if (! in_array('create', $scopes)) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->insufficientScopeResponse(); } if ($request->input('q') === 'last') { @@ -80,14 +105,28 @@ class MicropubMediaController extends Controller */ public function media(Request $request): JsonResponse { - $tokenData = $request->input('token_data'); + try { + $tokenData = $this->tokenService->validateToken($request->input('access_token')); + } catch (RequiredConstraintsViolated|InvalidTokenStructure) { + $micropubResponses = new MicropubResponses; - $scopes = $tokenData['scope']; + return $micropubResponses->invalidTokenResponse(); + } + + if ($tokenData->claims()->has('scope') === false) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->tokenHasNoScopeResponse(); + } + + $scopes = $tokenData->claims()->get('scope'); if (is_string($scopes)) { $scopes = explode(' ', $scopes); } - if (! in_array('create', $scopes, true)) { - return (new MicropubResponses)->insufficientScopeResponse(); + if (! in_array('create', $scopes)) { + $micropubResponses = new MicropubResponses; + + return $micropubResponses->insufficientScopeResponse(); } if ($request->hasFile('file') === false) { @@ -122,7 +161,7 @@ class MicropubMediaController extends Controller } $media = Media::create([ - 'token' => $request->input('access_token'), + 'token' => $request->bearerToken(), 'path' => $filename, 'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()), 'image_widths' => $width, diff --git a/app/Http/Middleware/LogMicropubRequest.php b/app/Http/Middleware/LogMicropubRequest.php deleted file mode 100644 index a04e80de..00000000 --- a/app/Http/Middleware/LogMicropubRequest.php +++ /dev/null @@ -1,24 +0,0 @@ -pushHandler(new StreamHandler(storage_path('logs/micropub.log'))); - $logger->debug('MicropubLog', $request->all()); - - return $next($request); - } -} diff --git a/app/Http/Middleware/VerifyMicropubToken.php b/app/Http/Middleware/VerifyMicropubToken.php index 33d2cb12..813350cf 100644 --- a/app/Http/Middleware/VerifyMicropubToken.php +++ b/app/Http/Middleware/VerifyMicropubToken.php @@ -4,78 +4,31 @@ declare(strict_types=1); namespace App\Http\Middleware; -use App\Http\Responses\MicropubResponses; use Closure; use Illuminate\Http\Request; -use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Encoding\CannotDecodeContent; -use Lcobucci\JWT\Token; -use Lcobucci\JWT\Token\InvalidTokenStructure; -use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use Symfony\Component\HttpFoundation\Response; class VerifyMicropubToken { /** * Handle an incoming request. - * - * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next): Response { - $rawToken = null; - if ($request->input('access_token')) { - $rawToken = $request->input('access_token'); - } elseif ($request->bearerToken()) { - $rawToken = $request->bearerToken(); + return $next($request); } - if (! $rawToken) { - return response()->json([ - 'response' => 'error', - 'error' => 'unauthorized', - 'error_description' => 'No access token was provided in the request', - ], 401); + if ($request->bearerToken()) { + return $next($request->merge([ + 'access_token' => $request->bearerToken(), + ])); } - try { - $tokenData = $this->validateToken($rawToken); - } catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) { - $micropubResponses = new MicropubResponses; - - return $micropubResponses->invalidTokenResponse(); - } - - if ($tokenData->claims()->has('scope') === false) { - $micropubResponses = new MicropubResponses; - - return $micropubResponses->tokenHasNoScopeResponse(); - } - - return $next($request->merge([ - 'access_token' => $rawToken, - 'token_data' => [ - 'me' => $tokenData->claims()->get('me'), - 'scope' => $tokenData->claims()->get('scope'), - 'client_id' => $tokenData->claims()->get('client_id'), - ], - ])); - } - - /** - * Check the token signature is valid. - */ - private function validateToken(string $bearerToken): Token - { - $config = resolve(Configuration::class); - - $token = $config->parser()->parse($bearerToken); - - $constraints = $config->validationConstraints(); - - $config->validator()->assert($token, ...$constraints); - - return $token; + return response()->json([ + 'response' => 'error', + 'error' => 'unauthorized', + 'error_description' => 'No access token was provided in the request', + ], 401); } } diff --git a/app/Http/Requests/MicropubRequest.php b/app/Http/Requests/MicropubRequest.php deleted file mode 100644 index d931f139..00000000 --- a/app/Http/Requests/MicropubRequest.php +++ /dev/null @@ -1,106 +0,0 @@ -micropubData; - } - - public function getType(): ?string - { - // Return consistent type regardless of input format - return $this->micropubData['type'] ?? null; - } - - protected function prepareForValidation(): void - { - // Normalize the request data based on content type - if ($this->isJson()) { - $this->normalizeMicropubJson(); - } else { - $this->normalizeMicropubForm(); - } - } - - private function normalizeMicropubJson(): void - { - $json = $this->json(); - if ($json === null) { - throw new \InvalidArgumentException('`isJson()` passed but there is no json data'); - } - - $data = $json->all(); - - // Convert JSON type (h-entry) to simple type (entry) - if (isset($data['type']) && is_array($data['type'])) { - $type = current($data['type']); - if (strpos($type, 'h-') === 0) { - $this->micropubData['type'] = substr($type, 2); - } - } - // Or set the type to update - elseif (isset($data['action']) && $data['action'] === 'update') { - $this->micropubData['type'] = 'update'; - } - - // Add in the token data - $this->micropubData['token_data'] = $data['token_data']; - - // Add h-entry values - $this->micropubData['content'] = Arr::get($data, 'properties.content.0'); - $this->micropubData['in-reply-to'] = Arr::get($data, 'properties.in-reply-to.0'); - $this->micropubData['published'] = Arr::get($data, 'properties.published.0'); - $this->micropubData['location'] = Arr::get($data, 'location'); - $this->micropubData['bookmark-of'] = Arr::get($data, 'properties.bookmark-of.0'); - $this->micropubData['like-of'] = Arr::get($data, 'properties.like-of.0'); - $this->micropubData['mp-syndicate-to'] = Arr::get($data, 'properties.mp-syndicate-to'); - - // Add h-card values - $this->micropubData['name'] = Arr::get($data, 'properties.name.0'); - $this->micropubData['description'] = Arr::get($data, 'properties.description.0'); - $this->micropubData['geo'] = Arr::get($data, 'properties.geo.0'); - - // Add checkin value - $this->micropubData['checkin'] = Arr::get($data, 'checkin'); - $this->micropubData['syndication'] = Arr::get($data, 'properties.syndication.0'); - } - - private function normalizeMicropubForm(): void - { - // Convert form h=entry to type=entry - if ($h = $this->input('h')) { - $this->micropubData['type'] = $h; - } - - // Add some fields to the micropub data with default null values - $this->micropubData['in-reply-to'] = null; - $this->micropubData['published'] = null; - $this->micropubData['location'] = null; - $this->micropubData['description'] = null; - $this->micropubData['geo'] = null; - $this->micropubData['latitude'] = null; - $this->micropubData['longitude'] = null; - - // Map form fields to micropub data - foreach ($this->except(['h', 'access_token']) as $key => $value) { - $this->micropubData[$key] = $value; - } - } -} diff --git a/app/Providers/MicropubServiceProvider.php b/app/Providers/MicropubServiceProvider.php deleted file mode 100644 index 1002a26d..00000000 --- a/app/Providers/MicropubServiceProvider.php +++ /dev/null @@ -1,26 +0,0 @@ -app->singleton(MicropubHandlerRegistry::class, function () { - $registry = new MicropubHandlerRegistry; - - // Register handlers - $registry->register('card', new CardHandler); - $registry->register('entry', new EntryHandler); - - return $registry; - }); - } -} diff --git a/app/Services/ArticleService.php b/app/Services/ArticleService.php index 3d5dcc56..195f7051 100644 --- a/app/Services/ArticleService.php +++ b/app/Services/ArticleService.php @@ -6,13 +6,13 @@ namespace App\Services; use App\Models\Article; -class ArticleService +class ArticleService extends Service { - public function create(array $data): Article + public function create(array $request, ?string $client = null): Article { return Article::create([ - 'title' => $data['name'], - 'main' => $data['content'], + 'title' => $this->getDataByKey($request, 'name'), + 'main' => $this->getDataByKey($request, 'content'), 'published' => true, ]); } diff --git a/app/Services/BookmarkService.php b/app/Services/BookmarkService.php index 9cbc0714..32ec7260 100644 --- a/app/Services/BookmarkService.php +++ b/app/Services/BookmarkService.php @@ -10,29 +10,28 @@ use App\Models\Bookmark; use App\Models\Tag; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Arr; use Illuminate\Support\Str; -class BookmarkService +class BookmarkService extends Service { /** * Create a new Bookmark. */ - public function create(array $data): Bookmark + public function create(array $request, ?string $client = null): Bookmark { - if (Arr::get($data, 'properties.bookmark-of.0')) { + if (Arr::get($request, 'properties.bookmark-of.0')) { // micropub request - $url = normalize_url(Arr::get($data, 'properties.bookmark-of.0')); - $name = Arr::get($data, 'properties.name.0'); - $content = Arr::get($data, 'properties.content.0'); - $categories = Arr::get($data, 'properties.category'); + $url = normalize_url(Arr::get($request, 'properties.bookmark-of.0')); + $name = Arr::get($request, 'properties.name.0'); + $content = Arr::get($request, 'properties.content.0'); + $categories = Arr::get($request, 'properties.category'); } - if (Arr::get($data, 'bookmark-of')) { - $url = normalize_url(Arr::get($data, 'bookmark-of')); - $name = Arr::get($data, 'name'); - $content = Arr::get($data, 'content'); - $categories = Arr::get($data, 'category'); + if (Arr::get($request, 'bookmark-of')) { + $url = normalize_url(Arr::get($request, 'bookmark-of')); + $name = Arr::get($request, 'name'); + $content = Arr::get($request, 'content'); + $categories = Arr::get($request, 'category'); } $bookmark = Bookmark::create([ @@ -55,7 +54,6 @@ class BookmarkService * Given a URL, attempt to save it to the Internet Archive. * * @throws InternetArchiveException - * @throws GuzzleException */ public function getArchiveLink(string $url): string { diff --git a/app/Services/LikeService.php b/app/Services/LikeService.php index e688561d..dd08e25b 100644 --- a/app/Services/LikeService.php +++ b/app/Services/LikeService.php @@ -8,19 +8,19 @@ use App\Jobs\ProcessLike; use App\Models\Like; use Illuminate\Support\Arr; -class LikeService +class LikeService extends Service { /** * Create a new Like. */ - public function create(array $data): Like + public function create(array $request, ?string $client = null): Like { - if (Arr::get($data, 'properties.like-of.0')) { + if (Arr::get($request, 'properties.like-of.0')) { // micropub request - $url = normalize_url(Arr::get($data, 'properties.like-of.0')); + $url = normalize_url(Arr::get($request, 'properties.like-of.0')); } - if (Arr::get($data, 'like-of')) { - $url = normalize_url(Arr::get($data, 'like-of')); + if (Arr::get($request, 'like-of')) { + $url = normalize_url(Arr::get($request, 'like-of')); } $like = Like::create(['url' => $url]); diff --git a/app/Services/Micropub/CardHandler.php b/app/Services/Micropub/CardHandler.php deleted file mode 100644 index 12e283be..00000000 --- a/app/Services/Micropub/CardHandler.php +++ /dev/null @@ -1,34 +0,0 @@ -createPlace($data)->uri; - - return [ - 'response' => 'created', - 'url' => $location, - ]; - } -} diff --git a/app/Services/Micropub/EntryHandler.php b/app/Services/Micropub/EntryHandler.php deleted file mode 100644 index 9cdbe789..00000000 --- a/app/Services/Micropub/EntryHandler.php +++ /dev/null @@ -1,41 +0,0 @@ - resolve(LikeService::class)->create($data)->url, - isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri, - isset($data['name']) => resolve(ArticleService::class)->create($data)->link, - default => resolve(NoteService::class)->create($data)->uri, - }; - - return [ - 'response' => 'created', - 'url' => $location, - ]; - } -} diff --git a/app/Services/Micropub/HCardService.php b/app/Services/Micropub/HCardService.php new file mode 100644 index 00000000..ead22a5b --- /dev/null +++ b/app/Services/Micropub/HCardService.php @@ -0,0 +1,32 @@ +createPlace($data)->uri; + } +} diff --git a/app/Services/Micropub/HEntryService.php b/app/Services/Micropub/HEntryService.php new file mode 100644 index 00000000..5f19156c --- /dev/null +++ b/app/Services/Micropub/HEntryService.php @@ -0,0 +1,34 @@ +create($request)->url; + } + + if (Arr::get($request, 'properties.bookmark-of') || Arr::get($request, 'bookmark-of')) { + return resolve(BookmarkService::class)->create($request)->uri; + } + + if (Arr::get($request, 'properties.name') || Arr::get($request, 'name')) { + return resolve(ArticleService::class)->create($request)->link; + } + + return resolve(NoteService::class)->create($request, $client)->uri; + } +} diff --git a/app/Services/Micropub/MicropubHandlerInterface.php b/app/Services/Micropub/MicropubHandlerInterface.php deleted file mode 100644 index 82040be9..00000000 --- a/app/Services/Micropub/MicropubHandlerInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -handlers[$type] = $handler; - - return $this; - } - - /** - * @throws MicropubHandlerException - */ - public function getHandler(string $type): MicropubHandlerInterface - { - if (! isset($this->handlers[$type])) { - throw new MicropubHandlerException("No handler registered for '{$type}'"); - } - - return $this->handlers[$type]; - } -} diff --git a/app/Services/Micropub/UpdateHandler.php b/app/Services/Micropub/UpdateService.php similarity index 79% rename from app/Services/Micropub/UpdateHandler.php rename to app/Services/Micropub/UpdateService.php index ee018f19..f806361c 100644 --- a/app/Services/Micropub/UpdateHandler.php +++ b/app/Services/Micropub/UpdateService.php @@ -4,33 +4,21 @@ declare(strict_types=1); namespace App\Services\Micropub; -use App\Exceptions\InvalidTokenScopeException; use App\Models\Media; use App\Models\Note; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Http\JsonResponse; use Illuminate\Support\Arr; use Illuminate\Support\Str; -/* - * @todo Implement this properly - */ -class UpdateHandler implements MicropubHandlerInterface +class UpdateService { /** - * @throws InvalidTokenScopeException + * Process a micropub request to update an entry. */ - public function handle(array $data) + public function process(array $request): JsonResponse { - $scopes = $data['token_data']['scope']; - if (is_string($scopes)) { - $scopes = explode(' ', $scopes); - } - - if (! in_array('update', $scopes, true)) { - throw new InvalidTokenScopeException; - } - - $urlPath = parse_url(Arr::get($data, 'url'), PHP_URL_PATH); + $urlPath = parse_url(Arr::get($request, 'url'), PHP_URL_PATH); // is it a note we are updating? if (mb_substr($urlPath, 1, 5) !== 'notes') { @@ -42,7 +30,7 @@ class UpdateHandler implements MicropubHandlerInterface try { $note = Note::nb60(basename($urlPath))->firstOrFail(); - } catch (ModelNotFoundException) { + } catch (ModelNotFoundException $exception) { return response()->json([ 'error' => 'invalid_request', 'error_description' => 'No known note with given ID', @@ -50,8 +38,8 @@ class UpdateHandler implements MicropubHandlerInterface } // got the note, are we dealing with a “replace” request? - if (Arr::get($data, 'replace')) { - foreach (Arr::get($data, 'replace') as $property => $value) { + if (Arr::get($request, 'replace')) { + foreach (Arr::get($request, 'replace') as $property => $value) { if ($property === 'content') { $note->note = $value[0]; } @@ -71,14 +59,14 @@ class UpdateHandler implements MicropubHandlerInterface } $note->save(); - return [ + return response()->json([ 'response' => 'updated', - ]; + ]); } // how about “add” - if (Arr::get($data, 'add')) { - foreach (Arr::get($data, 'add') as $property => $value) { + if (Arr::get($request, 'add')) { + foreach (Arr::get($request, 'add') as $property => $value) { if ($property === 'syndication') { foreach ($value as $syndicationURL) { if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) { diff --git a/app/Services/NoteService.php b/app/Services/NoteService.php index d8c55507..b101498c 100644 --- a/app/Services/NoteService.php +++ b/app/Services/NoteService.php @@ -14,52 +14,49 @@ use App\Models\SyndicationTarget; use Illuminate\Support\Arr; use Illuminate\Support\Str; -class NoteService +class NoteService extends Service { /** * Create a new note. */ - public function create(array $data): Note + public function create(array $request, ?string $client = null): Note { - // Get the content we want to save - if (is_string($data['content'])) { - $content = $data['content']; - } elseif (isset($data['content']['html'])) { - $content = $data['content']['html']; - } else { - $content = null; - } - $note = Note::create( [ - 'note' => $content, - 'in_reply_to' => $data['in-reply-to'], - 'client_id' => $data['token_data']['client_id'], + 'note' => $this->getDataByKey($request, 'content'), + 'in_reply_to' => $this->getDataByKey($request, 'in-reply-to'), + 'client_id' => $client, ] ); - if ($published = $this->getPublished($data)) { - $note->created_at = $note->updated_at = $published; + if ($this->getPublished($request)) { + $note->created_at = $note->updated_at = $this->getPublished($request); } - $note->location = $this->getLocation($data); + $note->location = $this->getLocation($request); - if ($this->getCheckin($data)) { - $note->place()->associate($this->getCheckin($data)); - $note->swarm_url = $this->getSwarmUrl($data); + if ($this->getCheckin($request)) { + $note->place()->associate($this->getCheckin($request)); + $note->swarm_url = $this->getSwarmUrl($request); + } + + $note->instagram_url = $this->getInstagramUrl($request); + + foreach ($this->getMedia($request) as $media) { + $note->media()->save($media); } - // - // $note->instagram_url = $this->getInstagramUrl($request); - // - // foreach ($this->getMedia($request) as $media) { - // $note->media()->save($media); - // } $note->save(); dispatch(new SendWebMentions($note)); - $this->dispatchSyndicationJobs($note, $data); + if (in_array('mastodon', $this->getSyndicationTargets($request), true)) { + dispatch(new SyndicateNoteToMastodon($note)); + } + + if (in_array('bluesky', $this->getSyndicationTargets($request), true)) { + dispatch(new SyndicateNoteToBluesky($note)); + } return $note; } @@ -67,10 +64,14 @@ class NoteService /** * Get the published time from the request to create a new note. */ - private function getPublished(array $data): ?string + private function getPublished(array $request): ?string { - if ($data['published']) { - return carbon($data['published'])->toDateTimeString(); + if (Arr::get($request, 'properties.published.0')) { + return carbon(Arr::get($request, 'properties.published.0')) + ->toDateTimeString(); + } + if (Arr::get($request, 'published')) { + return carbon(Arr::get($request, 'published'))->toDateTimeString(); } return null; @@ -79,13 +80,12 @@ class NoteService /** * Get the location data from the request to create a new note. */ - private function getLocation(array $data): ?string + private function getLocation(array $request): ?string { - $location = Arr::get($data, 'location'); - + $location = Arr::get($request, 'properties.location.0') ?? Arr::get($request, 'location'); if (is_string($location) && str_starts_with($location, 'geo:')) { preg_match_all( - '/([0-9.\-]+)/', + '/([0-9\.\-]+)/', $location, $matches ); @@ -99,9 +99,9 @@ class NoteService /** * Get the checkin data from the request to create a new note. This will be a Place. */ - private function getCheckin(array $data): ?Place + private function getCheckin(array $request): ?Place { - $location = Arr::get($data, 'location'); + $location = Arr::get($request, 'location'); if (is_string($location) && Str::startsWith($location, config('app.url'))) { return Place::where( 'slug', @@ -113,12 +113,12 @@ class NoteService ) )->first(); } - if (Arr::get($data, 'checkin')) { + if (Arr::get($request, 'checkin')) { try { $place = resolve(PlaceService::class)->createPlaceFromCheckin( - Arr::get($data, 'checkin') + Arr::get($request, 'checkin') ); - } catch (\InvalidArgumentException) { + } catch (\InvalidArgumentException $e) { return null; } @@ -142,47 +142,34 @@ class NoteService /** * Get the Swarm URL from the syndication data in the request to create a new note. */ - private function getSwarmUrl(array $data): ?string + private function getSwarmUrl(array $request): ?string { - $syndication = Arr::get($data, 'syndication'); - if ($syndication === null) { - return null; - } - - if (str_contains($syndication, 'swarmapp')) { - return $syndication; + if (str_contains(Arr::get($request, 'properties.syndication.0', ''), 'swarmapp')) { + return Arr::get($request, 'properties.syndication.0'); } return null; } /** - * Dispatch syndication jobs based on the request data. + * Get the syndication targets from the request to create a new note. */ - private function dispatchSyndicationJobs(Note $note, array $request): void + private function getSyndicationTargets(array $request): array { - // If no syndication targets are specified, return early - if (empty($request['mp-syndicate-to'])) { - return; - } - - // Get the configured syndication targets - $syndicationTargets = SyndicationTarget::all(); - - foreach ($syndicationTargets as $target) { - // Check if the target is in the request data - if (in_array($target->uid, $request['mp-syndicate-to'], true)) { - // Dispatch the appropriate job based on the target service name - switch ($target->service_name) { - case 'Mastodon': - dispatch(new SyndicateNoteToMastodon($note)); - break; - case 'Bluesky': - dispatch(new SyndicateNoteToBluesky($note)); - break; - } + $syndication = []; + $mpSyndicateTo = Arr::get($request, 'mp-syndicate-to') ?? Arr::get($request, 'properties.mp-syndicate-to'); + $mpSyndicateTo = Arr::wrap($mpSyndicateTo); + foreach ($mpSyndicateTo as $uid) { + $target = SyndicationTarget::where('uid', $uid)->first(); + if ($target && $target->service_name === 'Mastodon') { + $syndication[] = 'mastodon'; + } + if ($target && $target->service_name === 'Bluesky') { + $syndication[] = 'bluesky'; } } + + return $syndication; } /** diff --git a/app/Services/Service.php b/app/Services/Service.php new file mode 100644 index 00000000..cb480d7c --- /dev/null +++ b/app/Services/Service.php @@ -0,0 +1,30 @@ +toString(); } + + /** + * Check the token signature is valid. + */ + public function validateToken(string $bearerToken): Token + { + $config = resolve('Lcobucci\JWT\Configuration'); + + $token = $config->parser()->parse($bearerToken); + + $constraints = $config->validationConstraints(); + + $config->validator()->assert($token, ...$constraints); + + return $token; + } } diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 24821d29..4e3b4407 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,5 +3,4 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\HorizonServiceProvider::class, - App\Providers\MicropubServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 063e895a..e4ea6123 100644 --- a/composer.json +++ b/composer.json @@ -49,8 +49,7 @@ "openai-php/client": "^0.10.1", "phpunit/php-code-coverage": "^11.0", "phpunit/phpunit": "^11.0", - "spatie/laravel-ray": "^1.12", - "spatie/x-ray": "^1.2" + "spatie/laravel-ray": "^1.12" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 730017c5..a7521ac8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1076b46fccbfe2c22f51fa6e904cfedf", + "content-hash": "cd963bfd9cfb41beb4151e73ae98dc98", "packages": [ { "name": "aws/aws-crt-php", @@ -10079,133 +10079,6 @@ ], "time": "2024-11-12T20:51:16+00:00" }, - { - "name": "permafrost-dev/code-snippets", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/permafrost-dev/code-snippets.git", - "reference": "639827ba7118a6b5521c861a265358ce5bd2b0c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/permafrost-dev/code-snippets/zipball/639827ba7118a6b5521c861a265358ce5bd2b0c5", - "reference": "639827ba7118a6b5521c861a265358ce5bd2b0c5", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "spatie/phpunit-snapshot-assertions": "^4.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Permafrost\\CodeSnippets\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Patrick Organ", - "email": "patrick@permafrost.dev", - "role": "Developer" - } - ], - "description": "Easily work with code snippets in PHP", - "homepage": "https://github.com/permafrost-dev/code-snippets", - "keywords": [ - "code", - "code-snippets", - "permafrost", - "snippets" - ], - "support": { - "issues": "https://github.com/permafrost-dev/code-snippets/issues", - "source": "https://github.com/permafrost-dev/code-snippets/tree/1.2.0" - }, - "funding": [ - { - "url": "https://permafrost.dev/open-source", - "type": "custom" - }, - { - "url": "https://github.com/permafrost-dev", - "type": "github" - } - ], - "time": "2021-07-27T05:15:06+00:00" - }, - { - "name": "permafrost-dev/php-code-search", - "version": "1.12.0", - "source": { - "type": "git", - "url": "https://github.com/permafrost-dev/php-code-search.git", - "reference": "dbbca18f7dc2950e88121bb62f8ed2c697df799a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/permafrost-dev/php-code-search/zipball/dbbca18f7dc2950e88121bb62f8ed2c697df799a", - "reference": "dbbca18f7dc2950e88121bb62f8ed2c697df799a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^5.0", - "permafrost-dev/code-snippets": "^1.2.0", - "php": "^7.4|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "spatie/phpunit-snapshot-assertions": "^4.2" - }, - "type": "library", - "autoload": { - "files": [ - "src/Support/helpers.php" - ], - "psr-4": { - "Permafrost\\PhpCodeSearch\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Patrick Organ", - "email": "patrick@permafrost.dev", - "homepage": "https://permafrost.dev", - "role": "Developer" - } - ], - "description": "Search PHP code for function & method calls, variable assignments, and more", - "homepage": "https://github.com/permafrost-dev/php-code-search", - "keywords": [ - "code", - "permafrost", - "php", - "search", - "sourcecode" - ], - "support": { - "issues": "https://github.com/permafrost-dev/php-code-search/issues", - "source": "https://github.com/permafrost-dev/php-code-search/tree/1.12.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/permafrost-dev", - "type": "github" - } - ], - "time": "2024-09-03T04:33:45+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -12296,78 +12169,6 @@ ], "time": "2025-03-21T08:56:30+00:00" }, - { - "name": "spatie/x-ray", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/x-ray.git", - "reference": "c1d8fe19951b752422d058fc911f14066e4ac346" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/x-ray/zipball/c1d8fe19951b752422d058fc911f14066e4ac346", - "reference": "c1d8fe19951b752422d058fc911f14066e4ac346", - "shasum": "" - }, - "require": { - "permafrost-dev/code-snippets": "^1.2.0", - "permafrost-dev/php-code-search": "^1.10.5", - "php": "^8.0", - "symfony/console": "^5.3|^6.0|^7.0", - "symfony/finder": "^5.3|^6.0|^7.0", - "symfony/yaml": "^5.3|^6.0|^7.0" - }, - "require-dev": { - "phpstan/phpstan": "^2.0.0", - "phpunit/phpunit": "^9.5", - "spatie/phpunit-snapshot-assertions": "^4.2" - }, - "bin": [ - "bin/x-ray" - ], - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\XRay\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Patrick Organ", - "email": "patrick@permafrost.dev", - "homepage": "https://permafrost.dev", - "role": "Developer" - } - ], - "description": "Quickly scan source code for calls to Ray", - "homepage": "https://github.com/spatie/x-ray", - "keywords": [ - "permafrost", - "ray", - "search", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/x-ray/issues", - "source": "https://github.com/spatie/x-ray/tree/1.2.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/permafrost-dev", - "type": "github" - }, - { - "url": "https://github.com/sponsors/spatie", - "type": "github" - } - ], - "time": "2024-11-12T13:23:31+00:00" - }, { "name": "staabm/side-effects-detector", "version": "1.0.5", diff --git a/routes/web.php b/routes/web.php index 86e5dc7e..21f5848e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,6 @@ use App\Http\Controllers\PlacesController; use App\Http\Controllers\SearchController; use App\Http\Controllers\WebMentionsController; use App\Http\Middleware\CorsHeaders; -use App\Http\Middleware\LogMicropubRequest; use App\Http\Middleware\MyAuthMiddleware; use App\Http\Middleware\VerifyMicropubToken; use Illuminate\Support\Facades\Route; @@ -198,9 +197,7 @@ Route::post('token', [IndieAuthController::class, 'processTokenRequest'])->name( // Micropub Endpoints Route::get('api/post', [MicropubController::class, 'get'])->middleware(VerifyMicropubToken::class); -Route::post('api/post', [MicropubController::class, 'post']) - ->middleware([LogMicropubRequest::class, VerifyMicropubToken::class]) - ->name('micropub-endpoint'); +Route::post('api/post', [MicropubController::class, 'post'])->middleware(VerifyMicropubToken::class)->name('micropub-endpoint'); Route::get('api/media', [MicropubMediaController::class, 'getHandler'])->middleware(VerifyMicropubToken::class); Route::post('api/media', [MicropubMediaController::class, 'media']) ->middleware([VerifyMicropubToken::class, CorsHeaders::class]) diff --git a/tests/Feature/MicropubControllerTest.php b/tests/Feature/MicropubControllerTest.php index 9c095174..0e7abdfc 100644 --- a/tests/Feature/MicropubControllerTest.php +++ b/tests/Feature/MicropubControllerTest.php @@ -11,9 +11,9 @@ use App\Models\Media; use App\Models\Note; use App\Models\Place; use App\Models\SyndicationTarget; +use Carbon\Carbon; use Faker\Factory; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Queue; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -106,16 +106,16 @@ class MicropubControllerTest extends TestCase { $faker = Factory::create(); $note = $faker->text; - $response = $this->post( '/api/post', [ 'h' => 'entry', 'content' => $note, + 'published' => Carbon::now()->toW3CString(), + 'location' => 'geo:1.23,4.56', ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] ); - $response->assertJson(['response' => 'created']); $this->assertDatabaseHas('notes', ['note' => $note]); } @@ -223,13 +223,14 @@ class MicropubControllerTest extends TestCase $response = $this->post( '/api/post', [ - 'h' => 'entry', - 'content' => 'A random note', + 'h' => 'card', + 'name' => 'The Barton Arms', + 'geo' => 'geo:53.4974,-2.3768', ], ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] ); - $response->assertStatus(403); - $response->assertJson(['error' => 'invalid_scope']); + $response->assertStatus(401); + $response->assertJson(['error' => 'insufficient_scope']); } /** @@ -423,10 +424,10 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson([ - 'error' => 'invalid_scope', - 'error_description' => 'The token does not have the required scope for this request', + 'response' => 'error', + 'error' => 'insufficient_scope', ]) - ->assertStatus(403); + ->assertStatus(401); } #[Test] @@ -435,7 +436,7 @@ class MicropubControllerTest extends TestCase $response = $this->postJson( '/api/post', [ - 'type' => ['h-unsupported'], // a request type I don’t support + 'type' => ['h-unsopported'], // a request type I don’t support 'properties' => [ 'content' => ['Some content'], ], @@ -444,8 +445,8 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson([ - 'error' => 'Unknown Micropub type', - 'error_description' => 'The request could not be processed by this server', + 'response' => 'error', + 'error_description' => 'unsupported_request_type', ]) ->assertStatus(500); } @@ -459,8 +460,8 @@ class MicropubControllerTest extends TestCase [ 'type' => ['h-card'], 'properties' => [ - 'name' => [$faker->name], - 'geo' => ['geo:' . $faker->latitude . ',' . $faker->longitude], + 'name' => $faker->name, + 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude, ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] @@ -479,8 +480,8 @@ class MicropubControllerTest extends TestCase [ 'type' => ['h-card'], 'properties' => [ - 'name' => [$faker->name], - 'geo' => ['geo:' . $faker->latitude . ',' . $faker->longitude . ';u=35'], + 'name' => $faker->name, + 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude . ';u=35', ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] @@ -493,8 +494,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_updates_existing_note(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -515,8 +514,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_updates_note_syndication_links(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -544,8 +541,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_adds_image_to_note(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -569,8 +564,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_trying_to_update_non_note_model(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $response = $this->postJson( '/api/post', [ @@ -590,8 +583,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_trying_to_update_non_existing_note(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $response = $this->postJson( '/api/post', [ @@ -611,8 +602,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_when_trying_to_update_unsupported_property(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -633,8 +622,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_with_token_with_insufficient_scope_returns_error(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $response = $this->postJson( '/api/post', [ @@ -654,8 +641,6 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_can_replace_note_syndication_targets(): void { - $this->markTestSkipped('Update requests are not supported yet'); - $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -710,8 +695,8 @@ class MicropubControllerTest extends TestCase [ 'type' => ['h-entry'], 'properties' => [ - 'name' => [$name], - 'content' => [$content], + 'name' => $name, + 'content' => $content, ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] diff --git a/tests/Feature/OwnYourGramTest.php b/tests/Feature/OwnYourGramTest.php new file mode 100644 index 00000000..b2edaf97 --- /dev/null +++ b/tests/Feature/OwnYourGramTest.php @@ -0,0 +1,52 @@ +json( + 'POST', + '/api/post', + [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => ['How beautiful are the plates and chopsticks'], + 'published' => [Carbon::now()->toIso8601String()], + 'location' => ['geo:53.802419075834,-1.5431942917637'], + 'syndication' => ['https://www.instagram.com/p/BVC_nVTBFfi/'], + 'photo' => [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'https://scontent-sjc2-1.cdninstagram.com/t51.2885-15/e35/18888604_425332491185600_326487281944756224_n.jpg', + ], + ], + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $response->assertStatus(201)->assertJson([ + 'response' => 'created', + ]); + $this->assertDatabaseHas('media_endpoint', [ + // phpcs:ignore Generic.Files.LineLength.TooLong + 'path' => 'https://scontent-sjc2-1.cdninstagram.com/t51.2885-15/e35/18888604_425332491185600_326487281944756224_n.jpg', + ]); + $this->assertDatabaseHas('notes', [ + 'note' => 'How beautiful are the plates and chopsticks', + 'instagram_url' => 'https://www.instagram.com/p/BVC_nVTBFfi/', + ]); + } +} diff --git a/tests/Feature/TokenServiceTest.php b/tests/Feature/TokenServiceTest.php index 685e30a7..55024adf 100644 --- a/tests/Feature/TokenServiceTest.php +++ b/tests/Feature/TokenServiceTest.php @@ -8,6 +8,7 @@ use App\Services\TokenService; use DateTimeImmutable; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -18,7 +19,7 @@ class TokenServiceTest extends TestCase * the APP_KEY, to test, we shall create a token, and then verify it. */ #[Test] - public function tokenservice_creates_valid_tokens(): void + public function tokenservice_creates_and_validates_tokens(): void { $tokenService = new TokenService; $data = [ @@ -27,22 +28,20 @@ class TokenServiceTest extends TestCase 'scope' => 'post', ]; $token = $tokenService->getNewToken($data); - - $response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer ' . $token]); - - $response->assertJson([ - 'response' => 'token', - 'token' => [ - 'me' => $data['me'], - 'client_id' => $data['client_id'], - 'scope' => $data['scope'], - ], - ]); + $valid = $tokenService->validateToken($token); + $validData = [ + 'me' => $valid->claims()->get('me'), + 'client_id' => $valid->claims()->get('client_id'), + 'scope' => $valid->claims()->get('scope'), + ]; + $this->assertSame($data, $validData); } #[Test] - public function tokens_with_different_signing_key_are_not_valid(): void + public function tokens_with_different_signing_key_throws_exception(): void { + $this->expectException(RequiredConstraintsViolated::class); + $data = [ 'me' => 'https://example.org', 'client_id' => 'https://quill.p3k.io', @@ -60,12 +59,7 @@ class TokenServiceTest extends TestCase ->getToken($config->signer(), InMemory::plainText(random_bytes(32))) ->toString(); - $response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer ' . $token]); - - $response->assertJson([ - 'response' => 'error', - 'error' => 'invalid_token', - 'error_description' => 'The provided token did not pass validation', - ]); + $service = new TokenService; + $service->validateToken($token); } }